Every time I try to use it I'm left with a feeling it's not worth the hassle over C#.
C# has been quite nice for 10 years and they keep improving consistently with every version.
You're going to have patterns from the .NET Framework era being ported to .NET Core projects. It works, but you'll have two paradigms of doing things mixed into your project.
I envy people who only do hobbyist C# so get to work on code bases that have all the newest language feature usage.
But when you have to work on "diverse" development teams having some sort of patterns established (flawed as they are) brings some order to the insanity.
I think abstraction bloat is one of those things that's a preference. What's bloat to one is organization to someone else. When I hope into a python codebase and it's 900 line file doing computer vision madness, I hope and pray for abstraction bloat. I'm sure there are countless c# bloated codebases but I think that's mostly a function of c# codebases being inside megacorporate or government codebases. The bloat comes from the general inefficiency that those companies run at. I guess the same could be said by the scrappy python startups that put everything in a single file with no types or inversion of control or OOP.
I guess I'm saying that I'd rather deal with the bloat problem than the wild west problem. lol
You kinda have to fight against the IDE to not do a lot of things a certain way. For example, it'll automatically namespace new files according to your folder structure. They've also turned a certain amount of automatic linting on to gently suggest/nag you to write in a particular way.Suggest you write classes in certain ways, use newer language features, declare variables in better ways, etc. You can ignore all the nagging, but it's also incredibly easy for the next programmer to walk in and use the quick actions functionality to 'fix' the code in a few clicks.
And the asp.net core team have been incredibly opinionated, forcing a lot of good coding practices on the community.
So on the plus side, they pretty much forced DI to be the way we all work now. It's worked really well. Most library authors have now embraced it with gusto and you'll have a hard time using new libraries in a code base that's DI incompatible.
The bad side is that sometimes they made bad choices, but they are more minor things. Like they bizarrely went all in on JWT tokens which work really badly for corporate apps. Or the IOptions pattern for global settings which sucks compared to normal env variables in any other language. Lots of confusion over how they work on forums and SO.
This is why I find a .editorconfig file to be incredibly helpful.
( I'm someone who deals all day with legacy .Net framework projects, mixed with the kind of mix of .Net core 3, .Net 6, .net standard 2.0, and .Net 8 projects that you'd expect from a 20+ year old company with 260+ projects. And yes, I too envy hobbyists at time. )
Overriding equals to do memberwise comparisons on POCOs
Any checking of types where pattern matching would be better
Old-style tuples without names (old style is tuple.Item1, tuple.Item2, etc.)
Checking of multiple tuple values at once without pattern matching
Any code that could be refactored with the new LINQ methods
Long ifs/switches that could be replaced by pattern matching
Concatenate a lot of strings (one per line) so there wasn't a ton of horizontal scroll, particularly for SQL in C# code
using statements still needing a level of nesting
I've been spending the past couple years migrating various platforms from Framework to the new .NET and as long as you've got a head on your shoulders it's not too bad. Also, new projects in .NET are fantastic to work with, imo.
It forces you to reconcile your architecture as it gets bigger and before you accidentally turn it into a monolithic nightmare. IMO, It's more than just a preemptive defense against cyclomatic complexity. Going into unfamiliar F# code, the file ordering is usually a huge clue where to start reading first.
F# doesn't really buy you anything if you're already invested in C# (or even VB.net for those poor souls) unless you have a very specific use-case for it, IMO.
Try doing WinForms in F#...
Adding F# to the game would only complicate the scenario, because I would need to connect the F# interfaces somewhere to C# anyway, requiring an additional layer of mappings in many cases.
We've created very large scale applications in it requiring significant throughput. One of its benefits is also its disadvantages - we typically are quite productive in it and so don't feel the need to hire as many people in the team. Before LLM's were a thing we found we were wasting less time with boilerplate in general than our C# code for example. YMMV.
You're right that it's a second-class citizen.
This isn't true, C# has been adding new ABI features that didn't interact correctly with F# until the compiler and tooling catched up. For example, the spanification of C# was a huge a pain point and it still is when it comes to tooling.
Since its introduction that CLR has the notion of CLS, the Common Language Subset, that any language targeting the CLR should be able to understand.
Anything in the MSIL or metadata, that isn't part of that, requires additional effort from the respective language to be able to expose those features, and many of the more recent improvements, are more in the sense of CLR meaning C# Language Runtime, than Common Language Runtime.
For example, F# used to crash the CLR with InvalidProgramException when setting C# `init` properties. [1] It took almost three years, I think, to release the fix.
[1]: https://github.com/fsharp/fslang-suggestions/issues/904
Another rough example would be spans, and their more general feature, byref-like types (ref struct). These required plenty of compiler support, as they've got special lifetime rules (and more pending to implement `scoped`), they are banned as generic type arguments, and they require ignoring a special Obsolete attribute.
While these were added to F# timely, many language features still break when they interact with them: local functions, computation expressions (even the built-in ones), recursive type inference, and generic intrinsic methods such as `raise`, `defaultof` or `typeof`.
This wouldn't be so bad if C# hadn't "spanified" the shared framework and the entire ecosystem without CLS alternatives for many APIs.
So, these natural F# snippets don't compile:
let numbers = [| for span in text.EnumerateLines() -> Int32.Parse span |]
let checkNonEmpty (span: ReadOnlySpan<'T>) =
if span.Length > 0 then span else failwith "Expected non-empty span."
Edit: And I haven't even gotten into the libraries that use reflection expecting you to declare types in ways F# doesn't support.Generally though, it's pretty easy. Sometimes you'll want to make bindings around various things (not that you need to, but it'll make things smoother).
It's not actually awful, it's just that there's no real documentation of "hey today we're going to take Serilog/whatever and show you how to configure it in F#" style videos, so depending on where your weaknesses lie it can feel frustrating.