What can I say.
Anyone claiming that F# is good mostly for finance and data processing and C# for everything else, has probably never written a single line of practical F# code.
In previous two parts of the article, I tried to demonstrate that with F# you can achieve the same goals as with C#, but with less verbose, repetitive, structural code.
How it started.
At some point, developers realized that global state with unrestricted data access causes many side effects, producing insecure, error-prone, and hard-to-maintain code as software grows larger. That is when the idea emerged to bring data and the code operating on it together into a single unit, restricting direct access to the unit’s internal state and making software more secure and predictable. This is how data encapsulation was born.
Alongside encapsulation, abstraction was introduced — the process of hiding how behavior works.
Encapsulation (hiding data) and abstraction (hiding behavior) remain two foundational pillars of Object-Oriented Programming.
And that is how OOP has worked ever since — developers bring data and behavior together (classes) and define abstractions for them (interfaces).
For example, for C# developers — including myself — this has become a daily routine. And we rarely question it, because OOP languages like C# leave us little choice but to structure code this way.
But if you ask yourself whether this repetitive routine is always necessary, the answer is — no.
You don’t need OOP concepts to build stateless, streamlined request–response, data-processing pipelines, because in such systems there is no long-lived state to hide and protect. You have a request, and almost immediately you have a response. After that, everything is gone.
That is what I tried to demonstrate in the first two parts of this article by applying FP concepts.
And even if you have a classical desktop application, you don’t always need to approach it in an OOP way. Functional programming handles side effects not by hiding state, as encapsulation does, but by eliminating mutable state altogether.
So, no mutable state:
- no need for data encapsulation
- data is naturally tightened to the FP code operating on it
- no side effects — calling the same function with the same input always produces the same output
Ironically, these FP concepts existed before the rise of OOP, but they did not initially find their way into the mainstream. Only now, after many years, are they becoming more widely adopted.
This explains the emergence of patterns such as Functional Core, Imperative Shell and functional-style OOP, which attempt to combine FP and OOP.
Just think of the amount of FP features Microsoft added to C# to make it more FP like — delegates, lambda expressions, LINQ, expression-bodied members, init-only setters and records (with unions to come next) to mimic immutability, advanced pattern matching, etc.
But:
- Generic delegates with
FuncandActioncombined with lambda expressions are verbose, boilerplate-heavy and often hard to read and write LINQ is another attempt to introduce FP concepts into C#, but it is still relatively verbose - Expression-bodied members and init-only setters are attempts to introduce immutability, but they remain verbose and often boilerplate-heavy
- Tuples, records, and upcoming unions are attempts to introduce algebraic types. In case of record type, it is just syntactic sugar compiled as a class under the hood, automatically generating init-only properties for you, making positional records strictly immutable by default. Yet, if you add a new non-positional property to it, without initializing it, the compiler won’t complain and your application will potentially crash with a
NullReferenceException. Not so with true algebraic data, with compiler strictly enforcing that all data pieces exist and are fully initialized.
This is where F#, a multi-paradigm but functional-first language, comes into play.
By multi-paradigm, I mean you can use both FP and OOP.
However, functions are the core of F#. Defining a function is as simple as: let add a b = a + b.
In F#, immutability and strong type checking come naturally.
So again, F# is FP-first. That is its primary purpose, and you should stick to it. Don’t misuse it to fall back into old OOP habits. You can still use OOP when necessary, mainly for interoperability with .NET or for grouping related functionality.
F# is business-logic-first, with structural code pushed to the background. When used properly, you can often avoid structural overhead altogether, as shown in the first two parts of this article.
I mentioned earlier that Copilot can generate repetitive and verbose code. What I didn’t know at the time is that, starting June 1, 2026, Copilot is moving toward usage-based billing based on token consumption.
More verbose and boilerplate-heavy code means more syntactic overhead. More syntactic overhead means lower token efficiency. And lower token efficiency means higher AI token consumption.
When comparing both solutions, it becomes clear that F# is significantly more token-efficient than C#, thanks to its functional nature, concise syntax, and strong type system with type inference.
So, with all the examples and conclusions in mind, I still wonder why Microsoft treats F# like a stepchild. Why is it still a niche language? Why isn’t it promoted more?
It is also up to us developers to promote F# by using it in real-world applications. The more people use it, the more visible and accepted it becomes.
Of course, moving from OOP to FP is not trivial. It requires a shift in thinking. But there are many excellent resources available.
One of the most useful is F# for Fun and Profit.
If I had to suggest a starting path, I would recommend:
I hope this article helps you better understand F#.
So instead of asking whether it’s worth learning F#, just give it a try — as I did.
Top comments (0)