C# has made huge strides in enabling programming with values and data transformation, as opposed to mutable entities. While C# generally embraces mutability, its records are immutable. Their concise syntax eliminate any arguments for primitive obsession. Switch expressions and pattern matching allow more logic to be expressed as a chain of expressions rather than imperative logic. Like early spring flowers poking out of the snow, there's even some traction to break free from classes, with a classless Program.cs or ASP.NET Minimal APIs. The C# compiler team seems determined to eventually add primary constructors and discriminated unions. With C# destined to get full algebraic data type support, one is bound to ask: will F# remain relevant?
Here are some F# features C# will probably never have:
1) Curried functions
Curried functions enable partial application, which enables dependency injection without objects. While C# takes some steps back from object orientation, it has to remain object-based. In contrast, F# offers the flexibility of programming with functions or objects: both are just as powerful. While it's possible to write a curried function in C#, the lack of language support and type inference for it makes it impractical. With no traction for it in the community as far as I'm aware, we'll probably never see this feature in C#.
// Wait what? Func<T, Func<U, Func<V, TResult>>> CurriedFunction(...
2) Global type inference
F# features full Hindley-Milner type inference, which enables writing in a functional style without the inconvenience of spelling out painful type signatures. It also largely contributes to F# programs being much more concise than the C# equivalent. This is heavily dependent on the expression-based nature of F#. Expressions have types; statements do not. While C# could make small improvements on its type inference, there's very little chance it could ever infer argument types or even return types.
3) Automatic generalization
As F# performs type inference, it tries to come up with generic function signatures as much as possible. As C# does not and cannot ever infer function signatures, it cannot do this either.
4) Immutability consistently, by default, everywhere
F# defaults to immutability everywhere: in its
let bindings, its function arguments, constructor arguments, class fields, but also its native collection types:
Map are immutable;
Array is not (it's just the .NET Array), but the
Array module leans heavily towards copy-and-update operations.
C# seems decidedly undecided, with immutable records but mutable tuples, and of course mutable everything pre-records. Immutable collections exist (in
System.Collections.Immutable), but they lack language support (
new T) and amazingly do not provide value equality.
5) Pipe-forward operator
So much C# code is just
var thing = GetThing(); var propertyOfThing = thing.GetProperty() ?? thatOtherThing; var transformedThing = Transform(propertyOfThing); // etc.
While adding named variables can help with clarity, let's just face it, these were added here because a long chain of transforms looks bad as a single expression, e.g.
Transform(GetThing().GetProperty() ?? ... is not super readable. Even spread out over multiple lines, it's still a bit all over the place, with logic sometimes flowing left to right (
thing.GetProperty() ?? thatOtherThing) and sometimes right to left (
Transform(propertyOfThing)). The F# pipe operator fixes all of that:
- it alleviates the need for trivial variable names
- it lets the logic flow left to right, top to bottom, the way we naturally read text
- and it promotes free functions
let thing = GetThing() thing.GetProperty() |> Option.defaultValue thatOtherThing |> Transform |> etc.
The closest thing C# offers is fluent APIs.
Top comments (0)