Also, you haven't experienced implicit typing until you have used F#'s ridiculously powerful approach to it (relative to C# anyway).
Also, you haven't experienced implicit typing until you have used F#'s ridiculously powerful approach to it (relative to C# anyway).
Pattern matching, for starters. It's not just chained if-statements on steroids; F#'s decision to make partial matching emit a compiler error is a real sea change in terms of the level of compiler support you get in guarding against unintended consequences in an evolving codebase.
Type providers. They let you interact with external data sources with about the level of ceremony you get in a dynamic language, but are still statically typed. The SQL type provider is particularly impressive. Not only does it generate your DTOs for you without ceremony, even when you `SELECT *`, even when the schema changes, but, if you make a mistake in an inline SQL query, you'll get red squigglies under exactly the spot in the string where the error is, just like if it had been a syntax error in the F# or C# code.
Computation expressions. F#'s equivalent of Haskell's do notation. C# accomplishes similar levels of purpose-specific expressivity in some specific areas - LINQ, async/await, etc - by adding syntactic sugar to the compiler, which means only the C# compiler team gets to do it. Computation expressions let you roll your own.
Currying and partial application. You can achieve the same result in C# with higher order programming, but it gets messy quickly. Also, there's an argument to be made that, similar to how objects are heavyweight closures, dependency injection is heavyweight currying.
The reason is that a set of algebraic types in F# is effectively bundle of types that are collectively sealed. In C#, you can really only seal the terminal classes in a cluster of types, which means that anyone else can come along and add a new class to the type at a later date, after the initial module is compiled, possibly even at run time. That undercuts your ability to ensure that a branching statement is airtight.
I believe C# also still lacks active patterns, which are a pretty killer feature if you're looking to do data-oriented programming.
if (num.IsNotPrime(out var a, out var b))
WriteLine($"{num} = {a} × {b}");Much like how the compiler assists you when writing subclasseses so that you've implemented all the required methods, real Pattern Matching (F#) gives you the same guarantees from the compiler that you've handled all the possible cases appropriately.
Exhaustiveness checking is what elevates pattern matching to the other side of the expression problem, which is about getting feedback from the compiler to help you write programs.
a glorified switch statement does not help you do that, and so you still have to write subclasses to get compiler feedback.
This holds for a lot of C# vs F# issues... There is a fundamental difference between "it's valid, but our tooling hides it from us" and "the execution environment guarantees that it can never happen".
The single pass compiler, functional foundation, currying syntax, and other features that provide guarantees simply can't be replaced with equally robust syntactical sugar. Much in the same way that writing imperative programs that follow OOP principles in possible, but will never give you the system level guarantees that a compiled class sytem will.
It makes accidentally creating circular dependency graphs, which can be a source of technical debt, difficult to do. There is still a facility for introducing mutual recursion, but you have to do it explicitly.