Curried functions combined with that magnificent pipe operator, overlaid on the .NET runtime. Don Syme et al knocked it out of the park.
It's the one programming language that changed how I think about programming.
I'm only talking about the version before type providers. Then it got messy.
Before that, we could (and I did) recompile fsi.exe to do some custom prompt manipulation. It was a slog, but it worked, but then Microsoft faded from my life. Still, that early version (I believe 2.0) F# is just magnificent.
F# is up to version 9 now, and has only improved over time, IMHO. Type providers are a very small part of the story and can be avoided entirely if you want.
I tried F# when it was first released and was not a fan, but it sounds like that impression is a little outdated. C# has come so far in that time it’s almost a new language. I’ll have to take another look.
I don't know what C# has for an interactive prompt nowadays, but F#'s commandline environment, via its fsi.exe, was a revelation back then. It prevented having to have entire solutions to contain test projects to explore different areas of the vast .NET framework, especially when just learning how to use specific methods or objects.
Type providers can be extremely brittle IME. Altough, I guess if it is referring to version controlled example data that probably works better than referring to a DB or something like that directly that the dev has to provide.
Type providers are very powerful but involve running arbitrary external software at compile-time (e.g. a SQL Server or Postgres database). This can be difficult to set up and configure reliably in a multi-person project.
You're not entirely wrong, but when it comes to SQL, the trade-offs are unavoidable. You either embrace a conventional, not-SQL approach with all its limitations, or you treat the database as the single source of truth (SSoT). Both have downsides, but configuring a designated local or shared database for this purpose is no less reliable than conventional approaches.
As for type providers in general, I don't think databases are the best example of their typical use case. Most type providers don’t interact with external systems; they usually parse schemas, configuration files, or other structured data to generate strongly typed representations. The database-backed approach is just one variant, not the norm.
Plus, I'm not going to be downloading, configuring, or running any separate code at runtime. The project is the project, it's going to process some files, communicate with some services, and communicate with the UI, if any.
If I need to consume a service, it should be defined such that I manifest the interface module (perhaps via WCF) and then connect to it progressively from stub to ever greater functionality in test to final implementation. Trying to write a program to do all that at runtime is not sensible, IMO.
Metaprogramming via reflection, however, was useful for exploring the vast .NET framework, and I used those to great effect, especially in exploring .NET's various UI frameworks (WinForms and Silverlight), but never to create code at runtime via the emit functionality. No, that's my job: to emit code that is tested and works and is comprehensible.
Yeah, they're terrifying. It's often not that hard to generate the code (e.g. https://github.com/Smaug123/WoofWare.Myriad/ is where I pump out these things) for a bunch of what you would want to do with a type provider, and that's much less existentially terrifying if it's possible.
I don't doubt it, but I don't run Microsoft software any more. I've seen enough embrace, extend, and extinguish in my lifetime to not depend on them for my code's execution environment.
My current work needs nothing the .NET environment provides that I can't use python's standard libraries to get done, or bash and C if I need to.
But I'm lucky to no longer be in a corporate environment, so I don't need to consume commercial services, which was much easier using WCF within .NET. Back in my previous life, constructing n-tiered services on top of SqlServer using WCF was slick, indeed.
To any who are interested in how to construct such n-tiered applications simply but securely and precisely, I highly suggest Juval Lowy's IDesign system. He had three specific videos that I watched three or four times each until I understood his distillation of his vast expertise. Of course, Mr. Lowy is one of the co-designers of WCF, which was an excellent bit of tech.
No, but if it came in the standard install there's nothing I can do about it save spending hours and hours auditing my install. I don't do that kind of thing nowadays.
It has great ideas but because of all these conveniences it is very bad for performance based programming making it slower than C#. I like the ideas in Roc language to make functional programming as fast as imperative by controlling allocations in things like closures
As far as I can tell F# is one of those things where every single user is extremely happy. This happens rarely and I really am curious about the thing but never had time to get into it. I'm also pretty well versed in the .net ecosystem so it's probably gonna be easy.
Any tips? What kind of workflows might benefit the most if I were to incorporate it (to learn..)?
"As far as I can tell F# is one of those things where every single user is extremely happy" Isn't it because language has rather small community of passionate people, who are devoted to their language of choice?
F# popularity is somewhere between CHILL, Clipper and Raku langs, that are probably as obscure as F# for typical software dev.
I know Raku from Perl fame, and F# because it’s Microsoft, but CHILL and Clipper are totally new to me, so in my own humble experience these two latter look far more obscure. :D
F# shines on the back end, where its functional-first style is very adept at crunching data. Think about data flows in your system: Any place where you use LINQ in C# today to select/filter/transform data might be even better in F#. Parsing is also a great F# use case (e.g. parser combinators), although a fairly narrow niche.
The funny thing is that you can write very similar code in C#, so maybe you don't need to switch which language you're using as a CLR frontend.
using System.Linq;
using System;
var names = new string[] {"Peter", "Julia", "Xi" };
names.Select(name => $"Hello, {name}").ToList().ForEach(greeting => Console.WriteLine($"{greeting}! Enjoy your C#"));
LINQ is such a good library that I miss it in other languages. The Java stream equivalent just doesn't feel as fluent.
You can write a vast majority of your C# codebase in a functional style if you prefer to.
All the good stuff has been pirated from F# land by now: First-class functions, pattern matching, expression-bodied members, async functional composition, records, immutable collections, optional types, etc.
I don't know if there's a name for it but essentially F# is where the language designers can push the boundaries and try extremely new things that 99% of users will not want or need, but eventually some of them are such good ideas that they feed back into C#.
Maybe that's just research, and I'm glad that Microsoft hasn't killed F# (I do work there, but I don't write F# at work.)
> F# is where the language designers can push the boundaries
It really isn't, not anymore. F# now evolves conservatively, just trying to remove warts and keep up with C# interop.
And even then some C# features were considered too complex/powerful to implement (e.g. variance, scoped refs) or implemented in weaker, incompatible ways when C#'s design is considered messy (e.g. F#'s non-nullable constraints disallow value-types, which breaks for some generic methods written in C#, sadly even part of the System libs).
I assume you are implying that too many choices could confuse a junior developer, which I agree with. However, I don't think this is a concern in the bigger picture when talking about the space of all languages.
My 2p's worth is that the whole of F# is more than the some of its parts. When you say in your previous comment "All the good stuff has been pirated from F#" it misses the point of what it's actually like to use F#. The problem is, it's almost impossible to communicate what it's like. You have to try it and you have to keep going until you get over the initial "WTF!?" hump. There will be a WTF hump.
For example, C# may have cribbed the language features, but F# is expression based and immutable by default. Try using the same features in this context and the whole game changes.
I wouldn't say "all" - C# doesn't have discriminated unions yet, which is kind of a big one, especially when you're also looking at pattern matching. A
There's also going to be quite a big ecosystem / standard library difference between languages that had fundamental type system features since the beginning vs. languages that added fundamental features 23 years later.
Imagine all the functions that might return one thing or another, which was inexpressible in C# (until this proposal is shipped), will all these functions release new versions to express what they couldn't express before? Will there be an ecosystem split between static typing and dynamic typing?
Having reviewed the proposal (of course no guarantee that's what discriminated unions (aka product types aka algebraic data types) will look like), and it appears that it very much integrates nicely with the language.
I don't suspect they'll make too many changes (if any) to the existing standard library itself, but rather will put functions into their own sub-namespace moving forward like they did with concurrent containers and the like.
Given their penchant for backwards compatibility, I think old code is safe. Will it create a schism? In some codebases, sure. Folks always want to eat the freshest meat. But if they do it in a non-obtrusive way, it could integrate nicely. It reminds me of tuples, which had a similar expansion of capabilities, but the integration went pretty well.
>that's what discriminated unions (aka product types aka algebraic data types)
Just an FYI, discriminated unions are not product types, they are sum types. It's named so because the total number of possibilities is the sum of the number of possibilities for each variant.
Ex. A sum type Color is a PrimaryColor (Red, Blue, Yellow) OR a SecondaryColor (Purple, Green, Orange), the total number of possibilities is 3 + 3 = 6.
For a product type, if a ColorPair is a PrimaryColor AND a SecondaryColor, the total number of possibilities is 3 * 3 = 9
Both sum types and product types are algebraic types, in the same way that algebra includes both sums and products.
For the standard library, I'm curious for the family of TryParse kind of functions, since those are well modeled by a sum type. Maybe adding an overload without an `out` argument would give you back a DU to `switch` on
I make that mistake more often than I care to, but you are 100% spot-on. They are sum types, not product types. Thank you for making me walk the walk of shame!
Yeah, I hit the problem that there isn't a null-type equivalent of Select() for Action<T>, nor is there a IEnumerable.ForEach (controversial), so that's a bit of a hack. But I wanted to make it as close to the original example as possible.
> if we are concerned about the underlying data shifting when the collection is actually enumerated
I’m not sure what you mean by this. You can fulfill the IEnumerable contract without allowing multiple enumerations, but that doesn’t really have to do with the data shifting around. Doing ToList can be an expensive and unnecessary allocation
I love LINQ, maybe a little too much. I can end up writing monster oneliners to manipulate data in just the right way. I love list comprehensions in python too, since they can work in similar ways
Modern C# collection expressions make the definition of names closer to F#:
string[] names = ["Peter", "Julia", "Xi"];
I know working on "natural type" of collections is something the C# team is working on, so it feels possible in the future that you'll be able to do this:
<source>(5,1): error CS9176: There is no target type for the collection expression.
.. which I took to mean that, because .Select is an extension method on IEnumerable, the engine was unable to infer whether the collection should be a list, array, or some other type of collection.
It seems reasonable to have it default to Array if it's ambiguous, maybe there's a downside I'm not aware of.
As far as fluency goes, that’s not very impressive.
%w{Peter Julia Xi}.map{"Hello, #{it}"}.each{puts "#{it}! Enjoy your Ruby"}
That’s of course trivial examples. And while Ruby now have RBS and Sorbet, it’s yet another tradeoff compared to a syntax that has upfront static analysis as first class citizen in mind.
That is, each language will have its strong and weak points, but so far on "fluency" I’m not aware of anything that really beat Ruby far beyond as Ruby does compared to other mainstream programming languages.
Ruby is dynamically typed, which makes "fluent" API design that much easier at the cost of maintainability elsewhere. If you want to compare apples to apples, you need to compare F# to other statically typed languages.
Nothing is that much a big deal on a small selected sample, on the one hand on the other. That is, maybe some will prefer mandatory explicit type for every single variable, and some other will prefer type inference whenever possible, and both have pros and cons.
To jump in a REPL (or any debug breakpoint observation facility), having optional type inference is a great plus to my mind.
Note that Crystal does allow to make type explicit, and keep the fluent interface on track doing so:
Array(String).new.push("Peter", "Julia", "Xi").map{|name| "Hello, #{name}"}.each{|greeting| puts "#{greeting}! Enjoy your Crystal"}
Let’s remark by the way that, like with C# lambda parameters, block parameters are not explicitly typed in that case.
You're being way too nice. Java stream is nowhere near as easy to use as LINQ. I'd say that LINQ is easily one of the top 10 coding features that Microsoft has ever created.
I think LINQ is inspired by SQL. You can do whatever you can with SQL, it's just that the data source might differ IEnumerable with some in memory data, IQueryable with some DB. Or you can use async enumerable and your data source can be whatever web API or protocol.
Personally I think F# is excellent for writing ye olde CRUD applications, especially as the business logic becomes more complex. F# is really good at domain modeling, as creating types comes with minimal overhead. C# has improved a lot in this area (eg record types) but it’s still got a long way to go.
My experience was that it was a surprisingly nice language with a surprisingly warty user experience: papercuts ranging from naming conventions and function call styles (`|> List.map` vs `.Select`), basic syntax (`foo.[0]` to lookup arrays), type system features (F# doesn't have covariance/contravariance even though C# does), IDE support (back then was only Visual Studio, whose support for F# was inferior to C#).
Ended up settling on Scala after that, as a language with its own Warts, but one that somehow managed to feel a more cohesive than F# did despite having largely the same featureset and positioning.
F# was my first functional language and one that changed how I look at programming, but at the same time I'm happy to not actually have to use it for serious programming!
F# supports both functional and OO call styles. That's why you have both `|> List.map` and `.Select`. It can be a bit confusing at first, but the interoperability with C# is worth it.
Array lookup in modern F# is just `foo[0]`.
Subtyping is much less common in F# than in C#, so the need for covariance/contravariance is correspondingly lower. Personally, I've never needed it.
F# support in Visual Studio is now excellent. You can also develop F# in VS Code.
I think these days F# is probably a big more polished than what you remember, so perhaps it's worth giving it another shot.
Being a hosted language always requires certain compromises (something that was also apparent in Scala). I used to do Scala professionally in its early days, but for me it felt it added just as much complexity as it addressed. I focused on Clojure back then (on the FP side at least), and I do think that F# probably brings more to the table than Scala. (if one is not constrained to Java, that is)
The tooling story is not great, but I've almost never seen great tooling for a language that's not super popular. I'm guessing what you get today with Rider is more or less as good as what VS has to offer.
Interesting - I'm curious where your thoughts are now on using FP/Scala in 2025?
I've always looked at F# with envy as it is a hosted ML that will have extremely battle tested bindings to the important day to day stuff via C# (Darklang's stories of struggling with postgres and AWS when using OCaml was a good cautionary tale on the risks of using less common langs as a startup)
Never had a chance to try out Scala but am a seasoned Clojurian, as an outsider it seemed Scala suffered a little from being not opinionated enough so the ML family has been more appealing to tinker with even though Scala supports type classes out the box and will also have great ecosystem support via the JVM
It's never been a better time to try Scala if you're interested in the FP side. It's still very much "not opinionated" but the community and ecosystem have benefited from a certain convergence and given up on the "better Java" front which is served by Kotlin more adequately. Of course you can still consume any Java library when needed but it's best to avoid it if possible. Today you can pick between several ecosystems:
- Typelevel libraries: modern and more welcoming take on ScalaZ ideas, rich and mature, extensible libraries that are written in "tagless final" style.
- ZIO: concrete "super monad" with 3 type parameters, shuns the Haskell baggage and category theory lingo but pretty much the same concepts, compile-time autowiring dependency injection, a bit less mature.
- Kyo: new effects system on the block, pushing Scala 3's type system to the limit to stack effects using an "auto-flattening" monad (sorry if I butchered the description).
- Li Haoyi's own ecosystem that sticks to the standard library and JVM built-in mechanisms whenever possible, focused on Python style expressiveness, only more functional and with stronger types.
- I'd skip Akka/Pekko libraries but it's still an interesting piece of software if you need actor based, stateful cluster sharding.
Martin Odersky and the LAMP are focused on "capabilities" and we should eventually see something like direct-style algebraic effects, or like Kyo but without monads.
Also we have much better build systems than before (Scala-CLI, Mill, sbt has improved a lot too), binary backwards compatibility since Scala 3, and a very capable LSP backend if you don't like IntelliJ IDEA.
So, there is a gazillion ways to do things and the community didn't settle on one? I don't find that very attractive when trying to learn a new language.
FWIW I think writing F# is a really cohesive experience in day-to-day work. While there are usually at least two ways to do things, due to .NET interoperability requirements, it’s usually pretty clear which way is the “right” way to do something.
F# feels kind of similar to Python in this regard, where there might be more than one way to do it, but there is community and ecosystem consensus on what is the right way.
I think a lot of credit should go to Don Syme for this; he seems to have a very clear vision of what F# should and should not be, and the combination of features ends up being very tasteful and well composed.
> Static typing removes the downside of whitespace.
How so?
> Oh, and every language with line comments (so most of them) has significant whitespace.
Technically true, but that's not what people mean by "significant whitespace" in this context. So you're being pedantic rather than saying anything meaningful.
But you made me think. The ultimate nightmare would be significant trailing whitespace - the spaces and/or tabs after all the visible characters change the meaning of the line.
Well I don’t know what issue you have with whitespace, but the usual complaint is that programs with small typos are syntactically valid but logically incorrect. This is why CoffeeScript became so disliked. A static type checker makes this scenario much less likely since it won’t compile.
I would also add that F# has some indentation rules (“offside rules”) and tabs are disallowed, further shrinking the input space.
In f#'s case, there is the trifecta of everything being an expression, static typing and default immutability. This means you often write code like this:
let foo =
if bar then
baz
else
someDefault
Due to it being an expression you assign what the if evaluates to to foo.
Due to static typing, the compiler checks that both branches return the same type.
Due to the default immutability you don't declare and assign in separate steps.
What this results in, is that accidentally using the wrong indentation for something usually results in an error at compile time, at the latest.
Compared to how python does significant whitespace is that it's dynamic typing + statement based, which means you can easily end up assigning different types to the same variable in different branches of an if, for example.
I don't think being whitespace-significant is a "hard pass" dealbreaker, but as someone who's not a fan of it, I'd say this only goes a small way towards alleviating that - like most "choose your preferred syntax" designs. Even if you're a lone-wolf developer, you're gonna end up reading a lot of example code and other material that's in the ugly whitespace-sensitive style, given that:
> The verbose syntax is not as commonly used ... The default syntax is the lightweight syntax.
And most people in practice are not lone-wolf devs, and so the existence of this syntax helps them even less.
It's pretty baseless to claim that modern dotnet is inherently shitty. They've made tremendous strides in the dotnet core era.
F# making you unemployable is debateable, but I don't see what makes F# any less employable than most other FP languages. They have some niche applications that make it useful rarely, but when they're useful its a terrific tool for the job. F#'s ability to interop with the rest of the dotnet ecosystem positions it better than most functional languages for business usecases.
I don't have any problem with the idea that Microsoft's technologies are "shitty" after all the response of my work laptop to its mandatory Windows 11 upgrade was a non-copyable diagnostic message with an opaque code in it which, as I understand it, is basically the equivalent of "Huh, oops, maybe try again?" and my colleague spent a week trying to uh, share data from a "Sharepoint" table.
But .NET's CLR doesn't seem especially shitty. It's not awesome, I don't feel that RIIR urge when I work with C# and I probably wouldn't with F# either but it's fine, it's like Java again, or maybe Go or Python. It's fine. I don't hate it and that's enough.
I'm sure Microsoft has developed a lot of bad tech, but by and large their languages have been great. msvc c++ compiler as a possible exception, but f# in particular is excellent.
It can do GUIs well, although it takes some finesse to manage user state in an immutable-first language. Check out Fable for building web apps: https://fable.io/
I don't have much experience with Scala, but I think the two languages are pretty comparable in their respective ecosystems. The biggest difference I'm aware of is that Scala has typeclasses and F# does not.
F# is not really that strong on immutability, though. Sure, variables and struct fields are immutable by default, but making them mutable is one keyword away. Similarly with classes - declaring readonly properties is more concise, but when you need a read/write one, it's readily available.
Because it's a CLR language, it has access to all the same technologies as C#. That is, all the Microsoft native ones plus cross-platform with Avalonia.
I don't know if I would say 'well'. For simple GUIs it's OK but, for non-trivial GUIs I would use the approach to write the GUI frontend code in C# and have it call the F# 'backend'. If for no other reason than that the support and documentation for doing GUIs in C# is much better.
How about mobile?
Never tried, but I'm guessing more or less the same story as above. I would probably start by looking into .Net MAUI for that.
And how does it compare to e.g. Scala?
The biggest difference is that Scala is a much bigger and more of a multi-paradigm language. F# feels smaller and more focused and on its ML roots and functional programming. Not saying that Scala is less 'functional' than F#, but Scala supports you writing your code in a much more OOP way if you want. Yes you can (and sometimes have to) do OOP in F#, but it doesn't feel natural.
.NET for iOS and Android is very robust, I’ve been developing with it since 2018. Just avoid using MAUI for interfaces because it's still quite unfinished after the rewrite from Xamarin.Forms.
Which is what I am doing with my personal website for the most part. Elmish for the subpages where a lot of interactivity is actually needed. Otherwise just using Feliz.ViewEngine with htmx purely for enhancements
For web UI we have Fable and it's integration with well-known js frameworks, also WebSharper (although it's less well-known) and Bolero (on top of Blazor)
For mobile we have FuncUI (on top of Avalonia) and Fabulous (on top of Avalonia, Xamarin and Maui).
Most of these frameworks use Elm architecture, but some do not. For example I use Oxpecker.Solid which has reactive architecture.
Why would Lispers feel at home with its (whitespace delimited) syntax? Quite the strange claim.
I know this isn't a common rant, but I hate so-called functional language still bowing to the "infix mathematical operator special case" dogma, when those are just binary (variadic in Lisp) functions.
Always found it pretty appealing, otherwise. And no ";;"!
F# is beautiful, but I could never crack the nut and get fluent in it. I think the big problem is I only know a little C#, so it is difficult to figure out the object oriented methods that F# depends on. It was the same thing with Clojure and Scala for the JVM. I have zero interest in first learning C# or Java, just to use those platforms.
I'm aware, but you need to understand the .NET ecosystem to get anything practical done (at least when I was using it in 2017). All the books written on it (I own 3) are also the same way and assume you're a skilled C# dev.
100% this, I spent many months going through the most recent books on F# including one which the latest version was only released last year I think.
They all seem to try and shield you from the fact that you are much better placed if coming from C# (which everyone seems to refer to as .net these days) and have a solid understanding of the .net class library.
All the main web frameworks sit on top of asp.net and pretty much all official documentation for that is in c#
Such a shame because I learnt so much about types from trying to crack f# for real world application. fsharpforfunandprofit taught me heaps which I apply to other languages, but I don't want to become a c# developer which comes with all the years of changing best practices to be able to really be productive in f#.
Sorry if I am coming across as bitter but I just can't see learning f# in isolation from c# which is an absolute shame.
Except that there's a huge number of libraries written in C# that are available to F# (since both run on the CLR), and you have to do OOP in F# to use them.
One way to look at it, but consuming OOP libraries doesn't turn code into OOP.
Also, FSharp.Core (which most F# code leans heavily on) is not OOP at all.
F# promotes object programming, doesn't proscribe mutability, encourages function and data approach.
It offers simple access to the different paradigms, with some opinionated choices (e.g. preventing leaning on OOP beyond an arbitrary stretch, like no "protected", only explicit interface implementation, etc.).
oop is just another tool in your toolbox; if f# provides it and it's the best way to express a given algorithm you should definitely go ahead and use it. oop got a bad name due to people trying to shoehorn it into places it was not the best way to express something, and it seems like you're making the inverse mistake here.
> It was the same thing with Clojure and Scala for the JVM. I have zero interest in first learning C# or Java, just to use those platforms.
FWIW, I managed to learn Clojure without knowing anything from Java/JVM before diving into it. I did have plenty of JS experience which made ClojureScript easier to pick up I suppose, but Java works more or less the same as every other Algol-like and/or OOP language out there so if you have any previous experience with something like that, basic Java is trivial to pick up even accidentally.
I did try F#, but I was new to .NET ecosystem. For 1 "hello world" I was quite surprised by how many project files and boilerplate was generated by .NET, which put me off.
I am all for FP, immutable, and modern languages. But then where are the jobs and which companies care if you write good code?
Now everyone wants languages which are easy to use with AI, while reducing workforce and "increased productivity". I have been programming for 20 years and know 4-5 languages, in India it was worse but in EU at-least I can make a sustainable living by writing Java / TypeScript. I cannot even find jobs with Kotlin + TypeScript which pay well, forget getting jobs in Elixir / Clojure / F# (there maybe a handful of opportunities if I will relocate for around 70K/year). That is why I have mostly given up on learning niche languages.
I learn them as a fun hobby, with no salary expectations. It keeps the dream alive, and I learn a lot from the Common Lisp community that I do use in my job.
I hear you on the opportunity side and I can't see that changing. The good news is in recent releases there's a lot less boilerplate - "dotnet new console -lang F#" results in two files, a short fsproj file and a single line of Hello World.
Opportunities do exist, even when they’re few and far between. I learned Rust in my spare time because I was really interested in it. Then we stumbled across something that would have really benefitted from a cross platform library and lo and behold, I got to use my Rust knowledge, even though the vast majority of my day job doesn’t use it.
As sibling comment pointed out, it's just .fsproj manifest and Program.fs file. What boilerplate do you speak of? It's on the opposite end boilerplate-wise to projects made in e.g. Java or TypeScript.
For F#, projects are needed to make full applications or libraries. Otherwise, you can simply write F# scripts with .fsx and execute them via 'dotnet fsi {SomeScript.fsx}'.
(obviously you can also specify dotnet fsi as shebang and integrate these scripts into general scripting on Unix systems - it's very productive)
I suspect they were either referring to pre-.NET Core days before the new project formats came out or they're creating projects in Visual Studio and checking all the optional boxes. There indeed did used to be a lot more required boilerplate to get some code running. Now you can run a .NET project quite nicely in VS Code with 2 total files.
Well also if you're using Visual Studio it will generate solution files as well, not just fsproj. I grew up doing C/C++ so boilerplate project/IDE/make files as well as build objects are something I expect to see. I think people who work in primarily JIT'd/interpreted languages are used to just having a directory tree full of source files and having some CLI tool manage everything for them. Maybe a dependency list file as well, but that's about it. Python is like this, and javascript CAN be like this
Yeah, overall I agree. And I honestly can't imagine anyone who works with any popular javascript framework would flinch at the addition of a .sln file or /Properties folder lol...
I understand your perspective. I like to view niche languages as a medium for learning. For instance, I enjoy using Rust in my personal projects—even if many of these projects may never be released—because the lessons on immutability, functional programming constructs, and trait-oriented programming significantly enhance my day-to-day work. Therefore, I believe that learning niche languages, even in the absence of a robust job market, is worthwhile.
F# will likely remain niche forever. It’s likely that Rust will not given its growing and accelerating adoption by Microsoft, Google and the Linux Kernel.
It just takes time to defeat the 40+ years of c and c++ dominance.
I find Rust vastly simpler than C. If the code compiles, it's probably a valid expression of the business logic I encoded. I might've screwed up that logic, of course, and no language can prevent me from messing that up. I know! Many have tried, and I've defeated them with my ability to misrepresent my ideas! But at least with Rust, I'm reasonably confident that the code will actually do the thing I asked it to do. I'm never confident like that with C until I've run it a few hundred times without crashing.
(Yes, I'm familiar with the rich ecosystem around helping devs not write crummy C. I worked at Coverity at one point. If anything, that gave me enormous fear and respect of the hoops you have to jump through to be reasonably sure C code isn't completely broken.)
This seems...a very contrarian sentiment. Imho while C might lead you to create slightly fragile code once in a while, Rust is something like two orders of magnitude more complex.
No I know what he’s saying. C is not “slightly fragile code once in a while”. When you up the complexity of the code and the amount of code and the people working on the code the fragility becomes pervasive.
That's a huge part of it. If I stumble across random Rust code, I can assume that it's using typed data correctly, that it's not accessing freed memory, that it's not allocating but never freeing, that length checks are being enforced, etc. If they weren't, it wouldn't even compile (and the compiler would explain why).
Glancing at random C code tells you nothing about what happens with the data flowing into and out of it.
In my experience with it, rustc has been insistent on making me write code that's actually correct. I could translate that code back to C and have better C code than I would likely have written on my own. If there were something similar to `gcc -Werror -Weverything-rust-would-complain-about` — and if that thing were even possible — I very well might stick with C. Oh, and something as fast and ergonomic and informative as rust-analyzer would be hugely welcome.
Rust is like two orders of magnitude more simple (if you're not going to delve into its darker corners).
You have a single line serialization into/from absolutely anything. You have logging, tracing, cli libraries, error handling - most of those are one liners.
You have enums. Enums are business logic. Enums are often the way the world works.
You press enter and it builds, no pre setups, sub modules, cmake files and whatnot.
Rust like any language can be as simple or as complex as you want it to be. The complexity raises with the performance of the code you write but that’s also true of C. Unlike C that complexity is combined with guarantees that your code won’t crash in weird and unpredictable ways.
I will take C, C++ or Zig over Rust any day. For some people, like me, the Rust way of doing things isn't a good fit. It's not a model I enjoy working with.
According to Stack Overflow developer survey [0] Rust is at 12.5%, roughly a half of C# or Java and a quarter of Python. Also more than twice Ruby. So definitely not niche.
In my mind not niche means having jobs, and Rust has no jobs, not in any meaningful amount at least, and none at all in most countries. That puts it deep in the niche category for me.
It's popular in the "let's rewrite X in Rust" community which are very actively posting on HN, Reddit and wherever they can. That gives the impression it is not niche.
But the moment you search Rust on LinkedIn, you can see the truth.
> Which programming, scripting, and markup languages have you done extensive development work in over the past year, and which do you want to work in over the next year?
It does not ask if you are gainfully employed and using this language for your job.
Also, in the same results, just above Rust, I see:
> PowerShell 13.8%
<sarcasm>
So, I guess that we can safely say that Microsoft PowerShell is still more popular than Rust.
</sarcasm>
> According to Stack Overflow developer survey [0] Rust is at 12.5%, ... So definitely not niche.
The annual survey is very popular in the Rust community. Its results are often used for advocacy. Participation by Rust developers is very high. So what you have is a classic case of a selection bias.
It is possible to start your project with the script possibilities offered by F# (as mentioned in the blog post).
It is absolutely a viable approach and I even blogged about it a couple of months ago: https://www.asfaload.com/blog/fsharp-fsx-starting-point/
I like F#'s syntax when all you're doing is pure logic. But when you have to interface with any IO like a database or REST call or something, you have to abandon the elegance of ML syntax and use these ugly computation blocks. In C# you can do something like this:
var post = await _postService.getById(id);
in F# the equivalent is basically
let getPostById id = async {
let! post = blogPostService.getPostById id
return post
}
let post = getPostById 42 |> Async.RunSynchronously
But not really, because RunSynchronously isn't the same thing as `await`. Realistically if you wanted to handle the result of an async computation you would need to create continuations. F# isn't the only ML-family language to suffer from this; Ocaml does as well. It always seemed to me like the pattern with any asynchronous operations in F# is to either:
1. Do all logic in ML-syntax, then pass data into a computation block and handle I/O operations as the last part of your function, then return unit OR
2. Return a C#-style Task<> and handle all I/O in C#
Either way, ML-style languages don't seem like they're designed for the kind of commercial CRUD-style applications that 90% of us find ourselves paid to do.
I don't see how that changes things. You'd have to async it all the way to the top but the syntax is still cleaner than F#. If you're using an Asp.Net controller you just declare the handler as async Task<IActionResult> and it's fine. Even program main methods can be async these days
It's absolutely not exactly the same; let! is only available within a computation block. If you want to return some value from the computation block and return to Functional land without having to pause the thread you need to use a continuation, which C# has built in syntactic sugar for in async/await and F# does not.
It's actually sort of the other way round. C# has hardcoded syntax for async/await. F#'s syntax for async/await is a fully-general user-accessible mechanism.
They're not so different in that regard. C# `await` can be adapted by making an awaitable and awaiter type[1], which isn't to dissimilar to how a computation expression in F# needs to implement methods `Bind`, `Return`, `Yield`, etc.
In both languages these patterns make up for the absence of typeclasses to express things like a functor, applicative, monad, comonad, etc.
F# is a big language, it is a ML multi paradigm language that interoperates with C# so there is a lot of necessary complexity and many ways to do the same thing. A strong benefit of this is the ability to create a working functional paradigm prototype that can iteratively be refined to a faster version of itself by hot spot optimizing the slower parts with equivalent highly mutable functions while staying within the same language. Similar how one would use python and C++ and over time replace the python code with C++ code where performance is important.
For the specific case of C# use of await it is unfortunate that C# didn't design this feature with F# interop in mind so it does require extra steps. F# did add the task builder to help with this so the 'await' is replaced with a 'let!' within a task builder block.
let getById(id:int) : Task<string> = failwith "never"
let doWork(post:string) : unit = failwith "never"
let doThing() = task {
let! post = getById(42);
doWork(post); }
Alternatively the task can be converted to a normal F# async with the Async.AwaitTask function.
let getPostById1(id:int) : Async<string> = async { return! getById(id) |> Async.AwaitTask }
let getPostById2(id:int) : Async<string> = getById(id) |> Async.AwaitTask
let getPostById3 : int -> Async<string> = getById >> Async.AwaitTask
F# is a big language so I think it is to be expected that beginners will not know these things. I don't think the fix is to simplify F# we should just understand that F# is not for everyone and that is ok.
This is perfectly fine, but I think it's better to be unsure about specific language feature than confidently state something that is not correct (anymore).
Personally, I'm just annoyed by never-ending cycle of ".NET is bad because {reason x}", "When was this the case?", "10 years ago", "So?".
Like in the example above, chances are you just won't see new F# code do this.
Are you saying you prefer Ocaml to F# or C# to F#? Your example was indeed inelegant but it is also poorly designed as you take 4 lines to reproduce a function that is already built in, people can poorly design code in any language.
I'm saying that I wish computation blocks looked better in F#. Instead of:
let foo id = async {
let! bar = getBar id
return bar
}
I would prefer
let async foo id =
let! bar = getBar id
bar
or even something like
let async foo id =
getBar! id
So that computation blocks don't feel like you're switching to an entirely different language. Just wrap the ugliness in the same syntactic sugar that C# does. As it is, C# can achieve arrow syntax with async methods more elegantly than F# can:
Your criticism is rather incoherent and it is difficult for me to make sense of it.
You don't even have to use the computation block for that and can use the built in functions as I mentioned earlier and gave 3 examples of.
You're both complaining about extra keywords while trying to make the case of adding yet another one. Thus your complaint boils down to F# not picking the exact keywords that you like - that the language is not specialized to exactly how you want to use it. In language design there are always tradeoffs but I'm unable to see how your suggestions would improve the language in the general case or even in your specific case.
Computation expressions are a generalized concept which are there to add the exact kind of syntactic sugar that you're after. It's better than C# in that you can create your own as a first class concept in addition to using the built in ones. It's there for the exact purpose of creating mini-embedded DSLs, the very thing you're complaining about is the exact point of it.
> You're both complaining about extra keywords while trying to make the case of adding yet another one
I did no such thing. async is already a keyword in F#, I'm just saying they should drop the brackets and remove the required return statement.
> In language design there are always tradeoffs but I'm unable to see how your suggestions would improve the language in the general case or even in your specific case
It would make the language easier to read, for one, and would reduce the amount of specialized syntax needed for specific features. It would preserve the original ML-style syntax for an extremely common operation and not force users into wrapping anything upstream of an async call in a computation block, which is the ugliest syntax feature of F#
> Computation expressions are a generalized concept which are there to add the exact kind of syntactic sugar that you're after
I understand that, and my argument is they failed to do so. The syntax looks bad. They could keep it for all I care, but they should add even more sugar on top to make it not look so bad.
'async' is not a keyword in F#, it's a builder instance no different to the ones that you can create. It's just built in to the standard library.
The return statement is only required if you want to return something form the computation expression. In your example you use async { let! x = f(); return x}, which can be reduced to async { return! f()}, which can be reduced to f().
The distinction in this case is utterly meaningless. This is about the ergonomics of the language. Which are lacking the minute to break out of pure functional land
None of this is possible in C#, at least without jumping through many hoops and ending up with extra boilerplate (and this is okay, C# has enough of its own complexity). As cjbgkagh noted, providing this type of control is the whole point and what makes F# so powerful.
I find it personally better for CRUD applications than C# and I've written my share in both languages. Your syntax comparisons aren't exactly comparable in the sense that you haven't put in the wrapping/boilerplate around the C# code - you can't just await anywhere. You are also using an async which to run needs to know which context - this can be nice when you don't want to run the composed Task/Async on the current sync context. These days you stick to tasks if you want C# like behavior - and there's libraries to take away some SyncContext overload via custom F# CE's if you want.
The equivalent C# to your F# would be
task { return! _postService.getById(id) }
Which is somewhat pointless anyway - just return the task from postService directly. There's also no need to run the async synchronously then - Async allow you to run the logic on task, thread, sync over and over - a very different model than tasks.
To make C# comparable to your F# code (tasks are not the same so not quite true) you would need to define a method around it, and find a way if you want to run the resulting Task synchronously to do that safely.
public async Task<Post> GetPostById(id) => await blogPostService.getPostById(id);
// This is not entirely eq - since tasks are hot
this.GetPostById(42).Result
F# is quite usable with AI. All AI models are perfectly capable of generating idiomatic F# code. In fact, because it has a nice type system, if you ask the AI to model the problem well with types before implementing, hallucinated bugs are also easier caught.
Same with Nim. It works surprisingly well with AI tools. I think both have more straightforward syntax so it’s easy to generate. I’m curious how more complex languages do like C++ / Rust.
Last time I tried C++ with Copilot it was terrible.
I find F# easy to use with AI, mainly because it's statically typed (which results in compiler errors when the LLM generates non-working code) and it's very expressive, which allow me to more easily comprehend what the LLM is trying to do.
I tried F# some years ago (after I was fired from a shop that decided they will go all-in on Java and F# and dropping everything else overnight) and I was not impressed. I mean the language is really nice but the C# baggage and runtime was just a bit much. And I was not left convinced that immutability alone is worth the switch. I suppose we can call F# an FP gateway drug?
Now arguably you get a runtime and some baggage from the Erlang runtime (the BEAM VM, where Elixir also runs), but the guarantees and features you get are invaluable, and have proven themselves many times over the last literal three decades.
I wish they told me. They cut ties with most of their previous devs overnight, as if we were some enemies. Weirdest firing in my life and career to this day.
Elixir's ecosystem is much farther ahead than Gleam, so if you want to actually achieve stuff without pauses to fill the gaps yourself, then Elixir is the way to go.
And don't get me wrong, I love the idea of Gleam, a lot (Rust syntax, strong static typing, what's not to love?). But my PL early adopter days are over.
For me, it's Erlang. I just really like its horn clause syntax, it's so clean and readable. I know a common complaint is lack of piping (and even though you can implement it trivially, the order of arguments for some functions makes it of dubious use) but it's a small price to pay.
> I mean the language is really nice but the C# baggage and runtime was just a bit much
This was my experience with F#. Frankly, I've never been happy with my experience with CLI on Linux, and the toolchain inherits a lot of baggage from its C# heritage. Microsoft's toolchains have a very distinct workflow to them. F# has some interesting aspects to it like active patterns (something I wish was more common in the ML-family), but tbh I'm more than happy with ocaml.
I can't remember which version of Erlang was the most current when I started learning Elixir, but I do know that it was years before OTP 20, which fixed its string issues. Prior to that, Erlang didn't have very good string support for internationalization (I do a lot of Japanese language programming). Elixir, on the other hand, did. Otherwise, I may have gone with Erlang back then.
I like Erlang a lot, too. Both are great languages.
Elixir made me more productive and gave me back my love for programming, and I work with it professionally for 9 years now. But the lack of static typing is getting so irritating that I started upping my efforts to get [even] better at Rust lately.
So I agree. It's one of the very top drawbacks of Elixir.
Our shop converted 6 years ago, from C# to exclusively F#. I also author and maintain some packages (falco, donald, validus and others). The language is tough to learn if you're coming from a C-style language. But worth the effort and experience. It's extremely concise a true delight to build programs in that are fast, robust and durable.
There are a few drawbacks, depending on your perspective:
- compilation is slower than c# and hot reload isn't supported (it's in progress)
- there are very few opportunities to use it professionally
F# is a statically typed language with gradual typing and full type inference.
Given
let hello value =
printfn "%A" value
hello "world"
hello 2
The binding "hello" has "'a -> unit" signature where 'a is a generic argument it accepts because the "printfn" binding with a given format specifier is generalized the same way and an unconstrained 'T (here 'a) is the most narrow type inferred for "hello".
With regards to your example, the print/printfn (equivalent of Write/WriteLine) functions are a bit funny in F#. They don't actually take bound string values directly. You need to specify the type (which could be a string, a number, obj, etc)
As a proficient Elm developer with industry experience, I’m wondering what are the biggest challenges in hiring devs? Is it the paradigm, learning the ecosystem, lack of interest?
Are you currently hiring?
As someone who quite likes f#: It seems like a chicken and egg problem, not many companies doing f# because not many devs know it and not many devs learning it because not many companies are doing it.
I certainly wish I were doing f# professionally, but I only ever found 1 job listing for it, and that was in vienna while I am located like 200km away from it :(
Speaking of elm: I really like elmish for the frontend, when I need to make a dynamic page in the first place. Maybe that could be to your interest? (It transpiles to react under the hood via fable, which you can webpack into a drop in bundle. But I digress)
Jobs are kind of rare. I have to learn F# hoping maybe I will find a job in one or two years. And if I find it, they might want someone with F# work experience.
Hiring devs is perfectly fine if you don't look for F# skills - just hire generally smart people, and allow them 1-2 weeks to get comfortable with F#. Make them just solve problems from project euler or something.
For those who have already done functional programming, they wont take more than 2 days to start getting productive. For those who have written a lot of code, it will take them ~2 weeks to pick up functional thinking.
Anyone who is still uncomfortable with F# after 1 month - well that's a strong signal that the dev isn't a fast learner.
Additionally, I've never had anyone reject our job offer because we do F#. I'm sure a whole bunch of people might only be looking for python or javascript jobs, but that's fine because I'm not looking for them. I always have more people who I want to hire but I can't due to budget constraints.
Source: direct experience - I run a pure F# company with a team size of ~80.
> Anyone who is still uncomfortable with F# after 1 month - well that's a strong signal that the dev isn't a fast learner.
I think you may be reading this wrong. Agree with sibling post that even teaching folks C# -- which isn't far off of TypeScript, Java, etc. -- is never so straightforward if the individual wants a full grasp of the tool.
For myself, I feel that I have "full" command of C# as a programming language, but also how to structure projects, how to isolate modules, how to decouple code, how to set up a build system from scratch for C#, how do deploy and scale applications built with C#, what the strengths and weaknesses are, etc. My definition of "comfort" would entail a broader understanding of not just the syntax, but of the runtime ecosystem in which that code operates.
For me, my discomfort with F# is due to not knowing if what I’m doing is the correct/idiomatic way of doing things. With C# I have learned all the ways I should not do things…so it’s easier/faster to just use C#.
The problem is, many recruiters don't work with this mindset. If they're hiring a Java developer, and they get a CV from someone who has 1 year of Java experience and 5 years of C# experience, they see 1 year of experience, and immediately put it on the "unqualified" pile.
As a 20+ year C# developer I’ve tried several times to learn/use F#. Despite being interested in FP, my brain is having trouble figuring out how to structure my code in F#. In C# I’d either build a service (or use the mediator pattern) for the domain and a repository for data access. With F# it’s functions all the way down and it feels unnatural (as silly as that may sound).
along with what jcmontx said: F# is structured bottom from top. As in you can't reference something that is later defined earlier. I find that naturally leads to getting a decent enough structure "for free" because it forces you to have your basic functionality early on and build on that later.
That also, IMO, makes untangling/splitting up parts of the codebase easier as well.
That makes sense when one is used to the Visual Studio organization of solutions and projects, with some main method somewhere being the entry point, unless it's a WCF service or somesuch that gets run via a service manager.
I only used F# at its command line, fsi.exe, to give me commandline access to .NET for exploration, testing, and munging data. Over time, I built up quite a library of usable functions that I'd have the fsi.exe program pre-load when I kicked it off, leaving me at the prompt with all .NET namespaces and my code ready and accessible.
Once you get access to your database's data, it's easy to write queries against it and then play with the data. I could then port the F# processing bits that worked into my C# projects as necessary, but it was far easier to do it that way than to write the logic deep within complex multi-project solution files, where the various classes are spread throughout the projects' files.
I've worked with .net professionally for almost 20 years. At the beginning with C# while last decade almost exclusively with F#.
F# is just a better language. Simpler, more concise, more readable with stronger type safety. I will never go back to writing C# as I'm finding it too frustrating at times and unproductive.
Everything is an expression (i.e. its an actual functional programming language), and along with it comes a different way of thinking about problems. Coupled with a really good type system which has discriminated unions, you'll have much fewer bugs.
Pro tip: don't write F# like you would write C# - then you might as well write C#. Take the time to learn the functional primitives.
Ah yes that is definitely a nice addition to the C# language, albeit still with a couple of shortcomings compared to F#:
1. It doesn’t support code blocks, so if you need multiple lines or statements you have to define a function elsewhere.
2. To get exhaustiveness checking on int-backed enums you have to fiddle with compiler preprocessor directives.
And for #2 any data associated with each enum variant is left implied by C# and has to be inferred from a reading of the surrounding imperative code, whereas in F# the union data structure makes the relationship explicit, and verifiable by the compiler.
If you've had C# and F# co-exist in the same codebase, how do they co-exist? Is it like C# and VB.Net where a project (dll) is either C# or VB.Net, and they can reference each other?
Or: Is it more like the Swift / Objective C ecosystem where Swift, Objective C, and even straight C can co-exist in the same library?
In a mixed C# and F# codebase, generally when do you favor C# versus F#?
Coming from a C# background, what are the areas where F# is a better language?
Any success stories for F#, especially if it co-exists with C#? Any horror stories?
You have likely heard "functional core, imperative shell". This refers to having IO-heavy code that favors imperative patterns be written in C# and then have the actual domain logic core written in F# which is much better at expressing it. Because both languages are hosted on .NET, you simply achieve it by having two projects and having one reference another. It is very seamless F# and C# types are visible to each other if marked to be so.
The biggest advantage of F# is its gradual typing and full type inference which allows to massively reduce the amount of text required to describe application or domain logic. It is also extremely composable and I find doing async in F# somewhat nicer than in C# too. F# also has better nullability (or, rather, lack of thereof) assurances and, in my opinion, better UX for records.
You don't really need to split 1 & 2, since F# can define .NET interfaces and abstract classes just fine.
For that matter, you don't even need the interfaces if you wouldn't have had them in a C#-only solution. Just define the class in F# and use it directly from C#.
You still need a separate assembly for F#, but that doesn't imply dependency injection - again, just reference it and use it.
F# excels in writing domain logic where main domain entities are defined as records and discriminated unions and logic is written in pure functions. Given that product is preexisting and domain entities must already be defined, I wonder what use case do you have in mind?
The basic decision making logic needs to be very simple and easy to follow by scientists and people who aren't day-to-day software engineers: Basically, input some data, such as sensor readings, run it through an algorithm that makes decisions, and then output the decisions.
When mixing, you often write business logic as self-contained F# libraries with a C#-friendly API; and use C# to integrate them with whatever imperative, reflection-heavy and DI-heavy frameworks .NET is promoting the current year, since those are filled with interop edge cases for F# anyway.
You really want to avoid a language sandwich though (e.g. C#/F#/C#), because you'll keep wishing to remove the middle layer. Sadly, the addition of source generators make this mistake even more appealing.
Last time i tried F# i got bit by the weird concurrency story. There was async/task and somwhow they did not play well together. Also the dev tooling (for vim) was subpar, compared to ocaml (lsp). Compile times also was on the slower side.
If you use task { } CE's you will get really good UX (it is recommended to use them over async CE's). They were introduced in F# 6.0 to address outstanding interoperability issues. As of now, writing asynchronous code is more convenient than C# (which is more convenient than Go or other languages which are less expressive w.r.t. writing highly concurrent code).
`async` is F#'s original implementation of async programming. It is the precursor to C#'s await/async.
`task` targets the .NET TPL instead, which is also what C#'s await/async and all of .NET *Async methods use.
While the `async` implementation still offers some benefits over `task` (cold vs. hot starts [0]), my advice is - if you're doing backend code on .NET, you should use task. The tigher integration with the .NET ecosystem & runtime results in better exception stack traces, easier debugging and faster performance.
In the case of F#, the use cases are diminishing with every new C# release, since C# is getting better and better at the things F# is supposed to be strong at (record types, pattern-matching, etc.). Better to write the thing in C# using modern features of the more popular and capable language.
You may start to get a point when C# gets a two-directional type inference system. As it's now, any functional-looking code requires so much boiler plate that it's shorter and less bug-prone to copy your functions code everywhere you want to use them.
Unions remain the killer F# feature missing from C#.
Also, basic object initialization in C# has turned into a nightmare with recent versions. You need a flowchart to select among the 18 syntax options which suite your current needs.
With F# (and other newer languages), record fields are either `T` or `T option`. No need to worry about whether the value needs to be computed in a constructor and then remain immutable, whether it needs to be initialized by an object initializer and/or a constructor or not, whether it needs to remain interior-ly mutable throughout the life of the record, and so on. (Although as I recall you do still need to consider null values assigned to non-nullable references in your F# code that consumes C#.)
There's a draft spec out there for discrete unions in C# fwiw. I wouldn't be surprised to see it in another version or two.
And while I agree that I don't love some of the object initialization patterns C# allows--I respect that other people might have different style than me and don't mind ignoring that those styles exist when writing my own stuff :)
My general rule goes something like:
1. Use record types for any simple data structure
2. Avoid using primary constructors (even on record types).
3. Use { get; init; } properties for everything unless there's a good reason not to.
4. For things that need to carry internal state, have different methods for mutations, emit events, etc., use a class with regular old constructors and either { get; } (for immutable) or { get; private set; } (mutable) properties as needed.
Not sure that C# is the more capable language - its still that F# is mostly a superset of C# although C# is catching up. I think they are on par w.r.t capability (i.e. CLR compatible). In terms of conciseness, and expression F# still wins IMO; and given some of the extra features can be more performant at times (e.g code templating/inlining vs just JIT attributes). Custom CE's (Async/TaskSeq), nested recursive seq's great for algorithm dev, DU's, etc. There's a lot of little features C# doesn't have (or don't quite fit) that when I write C# I'm forced to implement still more cruft code around.
IMO its generally more readable as well to non-dev than C# and to dev's outside the Java/C# ecosystem (e.g. Node, Go, etc). I've shown F# code back in the day to non-tech stakeholders and they typically understand it (e.g. data modelling).
I want to ask a weird question. I'd love to learn ASP.net but i can't bring myself to deal with Microsoft Windows and their tech. Is F# a way for me to learn enough NET to make some money?
The problem with F#, Clojure and Elixir (hosted languages)
For F# , you need some basic C# knowledge
For Clojure, you need some basic Java knowledge
For Elixir, you need some basic Erlang knowledge
I like all 3 languages but usually each vm have a primary language, and each hosted language eventually become hosted on that primary language not the vm
I understand that for many task simple, to medium complexity, you might not need that, but it seem as you try to be more advanced you hit the wall of having to learn you host vm primary language
> For Elixir, you need some basic Erlang knowledge
As an Elixir programmer, this does not resonate. Basically the only thing I've ever felt I needed to understand Erlang for was ets, but let's be honest, that's not really proper Erlang but just the terrible ets query syntax. And all this requires is "ability to read enough erlang term syntax to be able to understand the ets manual". I don't think I could write a single line of correct Erlang by heart.
I feel like that's different in F#, where you still need to know lots of .NET internals which are all documented in C#y terms with C# examples etc. Elixir wraps pretty much all good Erlang/OTP internals in nice Elixiry modules, which solves that quite nicely.
Elixir has its warts but this really isn't one of them.
Thanks. I know a bit of Erlang (put some effort into learning, but never used it in real life, probably forgotten what I learned) and want to learn Elixir which seems better suited to what I want to do in the short to medium term.
Erlang is not a difficult language. Unusual, but actually quite sane syntax. If you are already familiar with Elixir it would probably be pretty easy to learn the basics of Erlang.
I worked with Elixir for over five years without knowing anything about Erlang. I know Erlang now, but only because I was interested in learning it, not because I needed to do so to write Elixir code.
That’s a reassuring thing to hear as a new clojure learner who has little interest in java.
What bits of java have you ended up needing? Like do you often use java libraries that don’t have clojure wrappers?
I feel like I’m often running up against little things. I’ll google “how to do xyz in clojure” and the top SO answer is to use a java library that apparently everybody already knows about, cause so many clojurists came from java first!
The same with Clojurescript and JS (and probably with Clojure-Dart) - you have nice interop with the hosting platform. The need for learning anything about Java (while writing Clojure) basically boils down to finding API documentation for a specific class and simply using it. That's all. That's all you'd ever need.
> For F# , you need some basic C# knowledge For Clojure, you need some basic Java knowledge For Elixir, you need some basic Erlang
Honestly, none of this really rings a bell. Having used all three options, I never felt that. Well, I already knew C# before getting into F#, but honestly, it felt like my C# knowledge at the time was more of a distraction. Been using Clojure for nine years, never done any serious Java and have not felt any need for it. Not knowing Erlang wasn't a problem with Elixir, like at all.
I've been using F# professionally for the past seven years across different contexts. First in a small software shop and now while bootstrapping a SaaS company. Some observations:
* It’s easier to attract smart developers to an F# project than to a [mainstream language] project. This was one of my driving beliefs when I introduced F# seven years ago. https://www.paulgraham.com/pypar.html. This is probably just as true for languages like Elixir, Clojure, ... But F# is what we went with.
Small Software Shop Context
* We operated in a small market where customers eventually dictated our tech stack (.NET & React). In that market, F# was a major advantage—it allowed junior developers to build apps that "just worked" with minimal regressions. Even with mediocre code quality, I felt confident that we could refactor safely at any time.
* I constantly had to justify F# to clients, which was exhausting. We always delivered decent results, so it worked out, but my partners were never as confident in defending F#.
Bootstrapping a SaaS Company
* F# has been invaluable for shipping features quickly and taking shortcuts when needed.
* Three years in, our codebase is large and contains its fair share of messy parts. But we can still develop new features at high speed with minimal regressions. Refactoring is relatively safe and straightforward.
* Compilation speed is the Achilles’ heel. If you don’t monitor it, the compiler slows down to the point where it impacts productivity. Earlier this year, waiting over a minute for feedback after a small change became unbearable. A lot of our "clean-up" work focuses on optimizing compilation times. We're still learning, but we’re optimistic that we can restructure the project to significantly improve build performance.
EDIT: maybe one more point. I see a lot of C# vs F# popping up here. Yes, C# has all the features that F# has. But do not underestimate how well designed F# is. It is an extremely simple language to learn compared to C#. There is a very limited amount of keywords to learn. And they compose extremely well. If you learned F# 7 years ago, took a break, and came back today, you'd simply write the same boring code that you would have written 7 years ago. And along the way you'd find out that some things have gotten a bit nicer over time.
As a 20+ year C# dev...where do I learn how to structure apps in F#? In C# my ASP.NET Controller might use a service (or a mediator) to execute some domain logic and that in turn will use a repository pattern (or EF DbContext) to update/query a database. How are dependencies injected in? It seems like there are multiple ways of going about it, but I don't have enough knowledge of F# to know 'the proper way' to do it.
Just start. Use whatever style you are used to. Use controllers. Adapt your style as F# pulls you deeper inside the pit of success. You'll struggle the first couple of features, but you'll reach a sweet spot between your current style and functions relatively fast.
I suspect you're right. I just have to get over the hump of being uncomfortable/fumbling my way through things that I would otherwise know how to do in C#. Thanks!
I’ll add that features isn’t really a constructive way to think about the differences between C# and F#. C# has more ‘features’ than most other programming languages. One of the core features of F# is less features and churn. Also, I really appreciate the way F# maintainers agonize over how to maintain the language and keep it coherent. You don’t get the feel they’re chasing features at all.
I tried F# some years back when I was searching for a language to port my OCaml project in... It felt too much .NET-y and too much MicroSoft-y. And back then .net for linux had just been released and was somewhat unpolished.
It seemed that I had to learn C# in order to use F# properly and it seemed that porting it to C# was the saner option. I went with Rust after all and it seems to have been the right choice.
Having written a moderately big project in OCaml which I tried porting to F# and later did port to Rust, to me Rust feels much faster to develop than either OCaml or F#, especially once you figure out the "core", adding more features is a breeze. Refactoring is also easier. Not to mention that reading Rust is much easier than reading OCaml and coming back to the project after a year feels very easy. I think that I have less bugs with Rust than with OCaml. And the end product's core ended up being ~3-4 times faster to execute in Rust.
That was kind of my main problem with dotnet stack in general, although that was some years ago. I've tried sneaking F# into our project by building set of tests (the main codebase was in C#), but some of my teammates would have none of that.
My personal perception of the dotnet community was that developers either were too pigheaded about doing things "the Microsoft way", or hyped about "innovation" — "Oooh... check this out, Scott Hanselman made a whole new conference talk about it...", and then you'd look into this "innovation", and it often turns out to be some decades old, well-known thing, only "wrapped into MSFT packaging", which is not really bad by itself - it just seemed that people didn't actually use them in a practical way. I just never found that sweet-spot between pragmatism and excitement. You have to find a middle ground between: "This works! Sure, yeah, but isn't it darn ugly and probably not scalable?" and "This is so beautiful, and cool, but nobody besides Jeff really understands it." And my personal experience was that .net programmers would always want to be on one of those sides.
423 comments
[ 4.2 ms ] story [ 222 ms ] threadIt's the one programming language that changed how I think about programming.
I'm only talking about the version before type providers. Then it got messy.
Before that, we could (and I did) recompile fsi.exe to do some custom prompt manipulation. It was a slog, but it worked, but then Microsoft faded from my life. Still, that early version (I believe 2.0) F# is just magnificent.
As for type providers in general, I don't think databases are the best example of their typical use case. Most type providers don’t interact with external systems; they usually parse schemas, configuration files, or other structured data to generate strongly typed representations. The database-backed approach is just one variant, not the norm.
Plus, I'm not going to be downloading, configuring, or running any separate code at runtime. The project is the project, it's going to process some files, communicate with some services, and communicate with the UI, if any.
If I need to consume a service, it should be defined such that I manifest the interface module (perhaps via WCF) and then connect to it progressively from stub to ever greater functionality in test to final implementation. Trying to write a program to do all that at runtime is not sensible, IMO.
Metaprogramming via reflection, however, was useful for exploring the vast .NET framework, and I used those to great effect, especially in exploring .NET's various UI frameworks (WinForms and Silverlight), but never to create code at runtime via the emit functionality. No, that's my job: to emit code that is tested and works and is comprehensible.
My current work needs nothing the .NET environment provides that I can't use python's standard libraries to get done, or bash and C if I need to.
But I'm lucky to no longer be in a corporate environment, so I don't need to consume commercial services, which was much easier using WCF within .NET. Back in my previous life, constructing n-tiered services on top of SqlServer using WCF was slick, indeed.
To any who are interested in how to construct such n-tiered applications simply but securely and precisely, I highly suggest Juval Lowy's IDesign system. He had three specific videos that I watched three or four times each until I understood his distillation of his vast expertise. Of course, Mr. Lowy is one of the co-designers of WCF, which was an excellent bit of tech.
It is controlled by Microsoft. It's not going on my Linux or BSD boxes.
I know how they work, and I want nothing to do with them.
Separately installed software? Not a bit of it.
Any tips? What kind of workflows might benefit the most if I were to incorporate it (to learn..)?
F# popularity is somewhere between CHILL, Clipper and Raku langs, that are probably as obscure as F# for typical software dev.
All the good stuff has been pirated from F# land by now: First-class functions, pattern matching, expression-bodied members, async functional composition, records, immutable collections, optional types, etc.
Maybe that's just research, and I'm glad that Microsoft hasn't killed F# (I do work there, but I don't write F# at work.)
It really isn't, not anymore. F# now evolves conservatively, just trying to remove warts and keep up with C# interop.
And even then some C# features were considered too complex/powerful to implement (e.g. variance, scoped refs) or implemented in weaker, incompatible ways when C#'s design is considered messy (e.g. F#'s non-nullable constraints disallow value-types, which breaks for some generic methods written in C#, sadly even part of the System libs).
I assume you are implying that too many choices could confuse a junior developer, which I agree with. However, I don't think this is a concern in the bigger picture when talking about the space of all languages.
For example, C# may have cribbed the language features, but F# is expression based and immutable by default. Try using the same features in this context and the whole game changes.
F# makes mutable code harder to do, so you tend to write immutable code by default.
In the interim, MS demonstrates how C# 8.0+ can fake it pretty well with recursive pattern matching: https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
Not the same I know, and I would love me a true ADT in C#.
Edit (a formal proposal): https://github.com/dotnet/csharplang/blob/18a527bcc1f0bdaf54...
Imagine all the functions that might return one thing or another, which was inexpressible in C# (until this proposal is shipped), will all these functions release new versions to express what they couldn't express before? Will there be an ecosystem split between static typing and dynamic typing?
I don't suspect they'll make too many changes (if any) to the existing standard library itself, but rather will put functions into their own sub-namespace moving forward like they did with concurrent containers and the like.
Given their penchant for backwards compatibility, I think old code is safe. Will it create a schism? In some codebases, sure. Folks always want to eat the freshest meat. But if they do it in a non-obtrusive way, it could integrate nicely. It reminds me of tuples, which had a similar expansion of capabilities, but the integration went pretty well.
Just an FYI, discriminated unions are not product types, they are sum types. It's named so because the total number of possibilities is the sum of the number of possibilities for each variant.
Ex. A sum type Color is a PrimaryColor (Red, Blue, Yellow) OR a SecondaryColor (Purple, Green, Orange), the total number of possibilities is 3 + 3 = 6.
For a product type, if a ColorPair is a PrimaryColor AND a SecondaryColor, the total number of possibilities is 3 * 3 = 9
Both sum types and product types are algebraic types, in the same way that algebra includes both sums and products.
For the standard library, I'm curious for the family of TryParse kind of functions, since those are well modeled by a sum type. Maybe adding an overload without an `out` argument would give you back a DU to `switch` on
In this case, I would move it to the very end if we are concerned about the underlying data shifting when the collection is actually enumerated.
Forgetting to materialize LINQ results can cause a lot of trouble, oftentimes in ways that happily evade detection while a debugger is attached.
I’m not sure what you mean by this. You can fulfill the IEnumerable contract without allowing multiple enumerations, but that doesn’t really have to do with the data shifting around. Doing ToList can be an expensive and unnecessary allocation
It seems reasonable to have it default to Array if it's ambiguous, maybe there's a downside I'm not aware of.
That is, each language will have its strong and weak points, but so far on "fluency" I’m not aware of anything that really beat Ruby far beyond as Ruby does compared to other mainstream programming languages.
>Crystal is a general-purpose, object-oriented programming language. With syntax inspired by Ruby, it's a compiled language with static type-checking.
But this time, one can probably say that Crystal will lake the benefits of ecosystem that only a large popular language enjoy.
I guess on that side F#, relying on .Net, is closer to Kotlin with Java ecosystem.
List<string> names = ["Peter", "Julia", "Xi"]; names.Select(name => $"Hello, {name}").ForEach(greeting => Console.WriteLine($"{greeting}! Enjoy your C#"))
or
new List<string> { "Peter", "Julia", "Xi" }.Select(name => $"Hello, {name}").ForEach(greeting => Console.WriteLine($"{greeting}! Enjoy your C#"))
To jump in a REPL (or any debug breakpoint observation facility), having optional type inference is a great plus to my mind.
Note that Crystal does allow to make type explicit, and keep the fluent interface on track doing so:
Let’s remark by the way that, like with C# lambda parameters, block parameters are not explicitly typed in that case.names.ForEach(name=>Console.WriteLine($"Hello, {name}! Enjoy your C#"));
I wrote a tutorial about how to get up and running with web dev in F# that might be of interest: https://functionalsoftware.se/posts/building-a-rest-api-in-g...
I see you use Giraffe but I wonder how hard would it be to use Web API or to mix F# projects with C# projects in the same solution.
My experience was that it was a surprisingly nice language with a surprisingly warty user experience: papercuts ranging from naming conventions and function call styles (`|> List.map` vs `.Select`), basic syntax (`foo.[0]` to lookup arrays), type system features (F# doesn't have covariance/contravariance even though C# does), IDE support (back then was only Visual Studio, whose support for F# was inferior to C#).
Ended up settling on Scala after that, as a language with its own Warts, but one that somehow managed to feel a more cohesive than F# did despite having largely the same featureset and positioning.
F# was my first functional language and one that changed how I look at programming, but at the same time I'm happy to not actually have to use it for serious programming!
Array lookup in modern F# is just `foo[0]`.
Subtyping is much less common in F# than in C#, so the need for covariance/contravariance is correspondingly lower. Personally, I've never needed it.
F# support in Visual Studio is now excellent. You can also develop F# in VS Code.
Being a hosted language always requires certain compromises (something that was also apparent in Scala). I used to do Scala professionally in its early days, but for me it felt it added just as much complexity as it addressed. I focused on Clojure back then (on the FP side at least), and I do think that F# probably brings more to the table than Scala. (if one is not constrained to Java, that is)
The tooling story is not great, but I've almost never seen great tooling for a language that's not super popular. I'm guessing what you get today with Rider is more or less as good as what VS has to offer.
I've always looked at F# with envy as it is a hosted ML that will have extremely battle tested bindings to the important day to day stuff via C# (Darklang's stories of struggling with postgres and AWS when using OCaml was a good cautionary tale on the risks of using less common langs as a startup)
Never had a chance to try out Scala but am a seasoned Clojurian, as an outsider it seemed Scala suffered a little from being not opinionated enough so the ML family has been more appealing to tinker with even though Scala supports type classes out the box and will also have great ecosystem support via the JVM
- Typelevel libraries: modern and more welcoming take on ScalaZ ideas, rich and mature, extensible libraries that are written in "tagless final" style.
- ZIO: concrete "super monad" with 3 type parameters, shuns the Haskell baggage and category theory lingo but pretty much the same concepts, compile-time autowiring dependency injection, a bit less mature.
- Kyo: new effects system on the block, pushing Scala 3's type system to the limit to stack effects using an "auto-flattening" monad (sorry if I butchered the description).
- Li Haoyi's own ecosystem that sticks to the standard library and JVM built-in mechanisms whenever possible, focused on Python style expressiveness, only more functional and with stronger types.
- I'd skip Akka/Pekko libraries but it's still an interesting piece of software if you need actor based, stateful cluster sharding.
Martin Odersky and the LAMP are focused on "capabilities" and we should eventually see something like direct-style algebraic effects, or like Kyo but without monads.
Also we have much better build systems than before (Scala-CLI, Mill, sbt has improved a lot too), binary backwards compatibility since Scala 3, and a very capable LSP backend if you don't like IntelliJ IDEA.
F# feels kind of similar to Python in this regard, where there might be more than one way to do it, but there is community and ecosystem consensus on what is the right way.
I think a lot of credit should go to Don Syme for this; he seems to have a very clear vision of what F# should and should not be, and the combination of features ends up being very tasteful and well composed.
hard pass
Oh, and every language with line comments (so most of them) has significant whitespace.
How so?
> Oh, and every language with line comments (so most of them) has significant whitespace.
Technically true, but that's not what people mean by "significant whitespace" in this context. So you're being pedantic rather than saying anything meaningful.
But you made me think. The ultimate nightmare would be significant trailing whitespace - the spaces and/or tabs after all the visible characters change the meaning of the line.
Well I don’t know what issue you have with whitespace, but the usual complaint is that programs with small typos are syntactically valid but logically incorrect. This is why CoffeeScript became so disliked. A static type checker makes this scenario much less likely since it won’t compile.
I would also add that F# has some indentation rules (“offside rules”) and tabs are disallowed, further shrinking the input space.
let foo = if bar then baz else someDefault
Due to it being an expression you assign what the if evaluates to to foo. Due to static typing, the compiler checks that both branches return the same type. Due to the default immutability you don't declare and assign in separate steps. What this results in, is that accidentally using the wrong indentation for something usually results in an error at compile time, at the latest.
Compared to how python does significant whitespace is that it's dynamic typing + statement based, which means you can easily end up assigning different types to the same variable in different branches of an if, for example.
I hope I explained it in understandable terms
https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...
I don't think being whitespace-significant is a "hard pass" dealbreaker, but as someone who's not a fan of it, I'd say this only goes a small way towards alleviating that - like most "choose your preferred syntax" designs. Even if you're a lone-wolf developer, you're gonna end up reading a lot of example code and other material that's in the ugly whitespace-sensitive style, given that:
> The verbose syntax is not as commonly used ... The default syntax is the lightweight syntax.
And most people in practice are not lone-wolf devs, and so the existence of this syntax helps them even less.
The default F# autoformatter is bundled/supported by VS Code, VS and Rider [0].
[0]: https://fsprojects.github.io/fantomas/docs/end-users/StyleGu...
F# making you unemployable is debateable, but I don't see what makes F# any less employable than most other FP languages. They have some niche applications that make it useful rarely, but when they're useful its a terrific tool for the job. F#'s ability to interop with the rest of the dotnet ecosystem positions it better than most functional languages for business usecases.
I'm quite employable.
If we go by the joke in gp, this is you.
But .NET's CLR doesn't seem especially shitty. It's not awesome, I don't feel that RIIR urge when I work with C# and I probably wouldn't with F# either but it's fine, it's like Java again, or maybe Go or Python. It's fine. I don't hate it and that's enough.
I don't have much experience with Scala, but I think the two languages are pretty comparable in their respective ecosystems. The biggest difference I'm aware of is that Scala has typeclasses and F# does not.
https://fsharp.org/use/desktop-apps/
I don't know if I would say 'well'. For simple GUIs it's OK but, for non-trivial GUIs I would use the approach to write the GUI frontend code in C# and have it call the F# 'backend'. If for no other reason than that the support and documentation for doing GUIs in C# is much better.
How about mobile?
Never tried, but I'm guessing more or less the same story as above. I would probably start by looking into .Net MAUI for that.
And how does it compare to e.g. Scala?
The biggest difference is that Scala is a much bigger and more of a multi-paradigm language. F# feels smaller and more focused and on its ML roots and functional programming. Not saying that Scala is less 'functional' than F#, but Scala supports you writing your code in a much more OOP way if you want. Yes you can (and sometimes have to) do OOP in F#, but it doesn't feel natural.
For mobile we have FuncUI (on top of Avalonia) and Fabulous (on top of Avalonia, Xamarin and Maui). Most of these frameworks use Elm architecture, but some do not. For example I use Oxpecker.Solid which has reactive architecture.
Can't help with Scala comparison, but at least DeepSeek V3 prefers F# for UI https://whatisbetter.ai/result/F%23-vs-Scala-9eaede00c7e4485...
The flagship editor is Visual Studio, not vs code.
I know this isn't a common rant, but I hate so-called functional language still bowing to the "infix mathematical operator special case" dogma, when those are just binary (variadic in Lisp) functions.
Always found it pretty appealing, otherwise. And no ";;"!
They all seem to try and shield you from the fact that you are much better placed if coming from C# (which everyone seems to refer to as .net these days) and have a solid understanding of the .net class library.
All the main web frameworks sit on top of asp.net and pretty much all official documentation for that is in c#
Such a shame because I learnt so much about types from trying to crack f# for real world application. fsharpforfunandprofit taught me heaps which I apply to other languages, but I don't want to become a c# developer which comes with all the years of changing best practices to be able to really be productive in f#.
Sorry if I am coming across as bitter but I just can't see learning f# in isolation from c# which is an absolute shame.
I think if I ever have time for another go I would learn enough to be proficient in c# before diving back in.
Also, FSharp.Core (which most F# code leans heavily on) is not OOP at all.
F# promotes object programming, doesn't proscribe mutability, encourages function and data approach.
It offers simple access to the different paradigms, with some opinionated choices (e.g. preventing leaning on OOP beyond an arbitrary stretch, like no "protected", only explicit interface implementation, etc.).
FWIW, I managed to learn Clojure without knowing anything from Java/JVM before diving into it. I did have plenty of JS experience which made ClojureScript easier to pick up I suppose, but Java works more or less the same as every other Algol-like and/or OOP language out there so if you have any previous experience with something like that, basic Java is trivial to pick up even accidentally.
I am all for FP, immutable, and modern languages. But then where are the jobs and which companies care if you write good code?
Now everyone wants languages which are easy to use with AI, while reducing workforce and "increased productivity". I have been programming for 20 years and know 4-5 languages, in India it was worse but in EU at-least I can make a sustainable living by writing Java / TypeScript. I cannot even find jobs with Kotlin + TypeScript which pay well, forget getting jobs in Elixir / Clojure / F# (there maybe a handful of opportunities if I will relocate for around 70K/year). That is why I have mostly given up on learning niche languages.
For F#, projects are needed to make full applications or libraries. Otherwise, you can simply write F# scripts with .fsx and execute them via 'dotnet fsi {SomeScript.fsx}'.
(obviously you can also specify dotnet fsi as shebang and integrate these scripts into general scripting on Unix systems - it's very productive)
It just takes time to defeat the 40+ years of c and c++ dominance.
(Yes, I'm familiar with the rich ecosystem around helping devs not write crummy C. I worked at Coverity at one point. If anything, that gave me enormous fear and respect of the hoops you have to jump through to be reasonably sure C code isn't completely broken.)
Glancing at random C code tells you nothing about what happens with the data flowing into and out of it.
In my experience with it, rustc has been insistent on making me write code that's actually correct. I could translate that code back to C and have better C code than I would likely have written on my own. If there were something similar to `gcc -Werror -Weverything-rust-would-complain-about` — and if that thing were even possible — I very well might stick with C. Oh, and something as fast and ergonomic and informative as rust-analyzer would be hugely welcome.
You have a single line serialization into/from absolutely anything. You have logging, tracing, cli libraries, error handling - most of those are one liners.
You have enums. Enums are business logic. Enums are often the way the world works.
You press enter and it builds, no pre setups, sub modules, cmake files and whatnot.
I like F#, Haskell, Elixir but not Rust.
[0] https://survey.stackoverflow.co/2024/technology#most-popular...
But the moment you search Rust on LinkedIn, you can see the truth.
Also, in the same results, just above Rust, I see:
<sarcasm> So, I guess that we can safely say that Microsoft PowerShell is still more popular than Rust. </sarcasm>The annual survey is very popular in the Rust community. Its results are often used for advocacy. Participation by Rust developers is very high. So what you have is a classic case of a selection bias.
Compared with C/C++, Java, C#, Javascript, Python, Typescript, PHP, all the rest can be considered niche.
Oh man, that is poignant :( They always say they do in the job description, but it always a different story once you get there.
1. Do all logic in ML-syntax, then pass data into a computation block and handle I/O operations as the last part of your function, then return unit OR
2. Return a C#-style Task<> and handle all I/O in C#
Either way, ML-style languages don't seem like they're designed for the kind of commercial CRUD-style applications that 90% of us find ourselves paid to do.
The controller handler is also the same. It will be marked with `async` keyword in C# and `task` CE in F#
In both languages these patterns make up for the absence of typeclasses to express things like a functor, applicative, monad, comonad, etc.
[1]https://ecma-international.org/wp-content/uploads/ECMA-334_7...
the F# equivalent is
`let! post = _postService.getById id`
For the specific case of C# use of await it is unfortunate that C# didn't design this feature with F# interop in mind so it does require extra steps. F# did add the task builder to help with this so the 'await' is replaced with a 'let!' within a task builder block.
Alternatively the task can be converted to a normal F# async with the Async.AwaitTask function.The author of the original comment, however, does not know this nor tried verifying whether F# actually works seamlessly with this nowadays (it does).
Writing asynchronous code in F# involves less syntax noise than in C#. None of that boilerplate is required, F# should not be written that way at all.
Personally, I'm just annoyed by never-ending cycle of ".NET is bad because {reason x}", "When was this the case?", "10 years ago", "So?".
Like in the example above, chances are you just won't see new F# code do this.
It will just use task { ... } normally.
You don't even have to use the computation block for that and can use the built in functions as I mentioned earlier and gave 3 examples of.
You're both complaining about extra keywords while trying to make the case of adding yet another one. Thus your complaint boils down to F# not picking the exact keywords that you like - that the language is not specialized to exactly how you want to use it. In language design there are always tradeoffs but I'm unable to see how your suggestions would improve the language in the general case or even in your specific case.
Computation expressions are a generalized concept which are there to add the exact kind of syntactic sugar that you're after. It's better than C# in that you can create your own as a first class concept in addition to using the built in ones. It's there for the exact purpose of creating mini-embedded DSLs, the very thing you're complaining about is the exact point of it.
F# is not for everyone, nor should it be.
I did no such thing. async is already a keyword in F#, I'm just saying they should drop the brackets and remove the required return statement.
> In language design there are always tradeoffs but I'm unable to see how your suggestions would improve the language in the general case or even in your specific case
It would make the language easier to read, for one, and would reduce the amount of specialized syntax needed for specific features. It would preserve the original ML-style syntax for an extremely common operation and not force users into wrapping anything upstream of an async call in a computation block, which is the ugliest syntax feature of F#
> Computation expressions are a generalized concept which are there to add the exact kind of syntactic sugar that you're after
I understand that, and my argument is they failed to do so. The syntax looks bad. They could keep it for all I care, but they should add even more sugar on top to make it not look so bad.
The return statement is only required if you want to return something form the computation expression. In your example you use async { let! x = f(); return x}, which can be reduced to async { return! f()}, which can be reduced to f().
The rest is your opinion that I don't agree with.
There are multiple alternate asynchronous computation expression implementations which give different ergonomics and behavior (like https://github.com/TheAngryByrd/IcedTasks). There's an entire CE extension to specifically enable this kind of convenience and flexibility too: https://github.com/fsharp/fslang-design/blob/main/FSharp-6.0...
None of this is possible in C#, at least without jumping through many hoops and ending up with extra boilerplate (and this is okay, C# has enough of its own complexity). As cjbgkagh noted, providing this type of control is the whole point and what makes F# so powerful.
The equivalent C# to your F# would be
Which is somewhat pointless anyway - just return the task from postService directly. There's also no need to run the async synchronously then - Async allow you to run the logic on task, thread, sync over and over - a very different model than tasks.To make C# comparable to your F# code (tasks are not the same so not quite true) you would need to define a method around it, and find a way if you want to run the resulting Task synchronously to do that safely.
With which language are you comparing with?
Because there's afaik csproj and maybe .sln
and both of them are let's be frank - foundational for almost all projects that arent just hello world.
Otherwise you end up with some cmakes or something similar that want to achieve something similar
Last time I tried C++ with Copilot it was terrible.
...I mean: pipes, immutability, transparent mega-parallelism... helloooo?
I tried F# some years ago (after I was fired from a shop that decided they will go all-in on Java and F# and dropping everything else overnight) and I was not impressed. I mean the language is really nice but the C# baggage and runtime was just a bit much. And I was not left convinced that immutability alone is worth the switch. I suppose we can call F# an FP gateway drug?
Now arguably you get a runtime and some baggage from the Erlang runtime (the BEAM VM, where Elixir also runs), but the guarantees and features you get are invaluable, and have proven themselves many times over the last literal three decades.
And don't get me wrong, I love the idea of Gleam, a lot (Rust syntax, strong static typing, what's not to love?). But my PL early adopter days are over.
For me, it's Erlang. I just really like its horn clause syntax, it's so clean and readable. I know a common complaint is lack of piping (and even though you can implement it trivially, the order of arguments for some functions makes it of dubious use) but it's a small price to pay.
> I mean the language is really nice but the C# baggage and runtime was just a bit much
This was my experience with F#. Frankly, I've never been happy with my experience with CLI on Linux, and the toolchain inherits a lot of baggage from its C# heritage. Microsoft's toolchains have a very distinct workflow to them. F# has some interesting aspects to it like active patterns (something I wish was more common in the ML-family), but tbh I'm more than happy with ocaml.
I like Erlang a lot, too. Both are great languages.
Elixir made me more productive and gave me back my love for programming, and I work with it professionally for 9 years now. But the lack of static typing is getting so irritating that I started upping my efforts to get [even] better at Rust lately.
So I agree. It's one of the very top drawbacks of Elixir.
There are a few drawbacks, depending on your perspective:
- compilation is slower than c# and hot reload isn't supported (it's in progress)
- there are very few opportunities to use it professionally
- hiring devs can be challenging
From the article, it looks like it's mostly dynamically typed. Or is it inferred? Or is it something else?
Like, if I write
Does that just work?To me, that'd be a point that might steer me away from the language. Deducible types seem vital to larger and long lived projects.
Given
The binding "hello" has "'a -> unit" signature where 'a is a generic argument it accepts because the "printfn" binding with a given format specifier is generalized the same way and an unconstrained 'T (here 'a) is the most narrow type inferred for "hello".Isn't gradual typing widely understood to mean "gradual between static and dynamic", which F# certainly isn't?
With regards to your example, the print/printfn (equivalent of Write/WriteLine) functions are a bit funny in F#. They don't actually take bound string values directly. You need to specify the type (which could be a string, a number, obj, etc)
https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...
I certainly wish I were doing f# professionally, but I only ever found 1 job listing for it, and that was in vienna while I am located like 200km away from it :(
Speaking of elm: I really like elmish for the frontend, when I need to make a dynamic page in the first place. Maybe that could be to your interest? (It transpiles to react under the hood via fable, which you can webpack into a drop in bundle. But I digress)
For those who have already done functional programming, they wont take more than 2 days to start getting productive. For those who have written a lot of code, it will take them ~2 weeks to pick up functional thinking.
Anyone who is still uncomfortable with F# after 1 month - well that's a strong signal that the dev isn't a fast learner.
Additionally, I've never had anyone reject our job offer because we do F#. I'm sure a whole bunch of people might only be looking for python or javascript jobs, but that's fine because I'm not looking for them. I always have more people who I want to hire but I can't due to budget constraints.
Source: direct experience - I run a pure F# company with a team size of ~80.
For myself, I feel that I have "full" command of C# as a programming language, but also how to structure projects, how to isolate modules, how to decouple code, how to set up a build system from scratch for C#, how do deploy and scale applications built with C#, what the strengths and weaknesses are, etc. My definition of "comfort" would entail a broader understanding of not just the syntax, but of the runtime ecosystem in which that code operates.
- hiring devs can be challenging
Those drawbacks are huge, IMO.
That also, IMO, makes untangling/splitting up parts of the codebase easier as well.
I only used F# at its command line, fsi.exe, to give me commandline access to .NET for exploration, testing, and munging data. Over time, I built up quite a library of usable functions that I'd have the fsi.exe program pre-load when I kicked it off, leaving me at the prompt with all .NET namespaces and my code ready and accessible.
Once you get access to your database's data, it's easy to write queries against it and then play with the data. I could then port the F# processing bits that worked into my C# projects as necessary, but it was far easier to do it that way than to write the logic deep within complex multi-project solution files, where the various classes are spread throughout the projects' files.
I also just really enjoyed using F#.
I'm kinda wondering if anyone here with decent C#/.net experience can give their version of the answer?
---
The article really didn't answer its own question. It basically says "How" instead of "Why"...
...Which as someone who's spent over 20 years in C#, and tends to advocate for "functional" style, leaves me with more questions than answers!
F# is just a better language. Simpler, more concise, more readable with stronger type safety. I will never go back to writing C# as I'm finding it too frustrating at times and unproductive.
Pro tip: don't write F# like you would write C# - then you might as well write C#. Take the time to learn the functional primitives.
Whereas in C# you have to do like
And C# will let you know if you omit the `else` on accident ...Except that most C# developers do
to get the red squiggly to go away while they type the rest of the code, unknowingly subverting the compiler's safety check.This doesn't seem like a big deal given these examples. But it becomes a much bigger deal when the if/else grows super large, becomes nested, etc.
bar = foo switch
{
1. It doesn’t support code blocks, so if you need multiple lines or statements you have to define a function elsewhere. 2. To get exhaustiveness checking on int-backed enums you have to fiddle with compiler preprocessor directives.
And for #2 any data associated with each enum variant is left implied by C# and has to be inferred from a reading of the surrounding imperative code, whereas in F# the union data structure makes the relationship explicit, and verifiable by the compiler.
Or: Is it more like the Swift / Objective C ecosystem where Swift, Objective C, and even straight C can co-exist in the same library?
In a mixed C# and F# codebase, generally when do you favor C# versus F#?
Coming from a C# background, what are the areas where F# is a better language?
Any success stories for F#, especially if it co-exists with C#? Any horror stories?
The biggest advantage of F# is its gradual typing and full type inference which allows to massively reduce the amount of text required to describe application or domain logic. It is also extremely composable and I find doing async in F# somewhat nicer than in C# too. F# also has better nullability (or, rather, lack of thereof) assurances and, in my opinion, better UX for records.
1: C# Library with interfaces and/or abstract base classes
2: F# library with implementations of those interfaces and base classes
3: C# program (console, web service, GUI, ect) that specifies the implementations in Dependency Injection
Or is there a simpler way for C# and F# to co-exist in the same project (dll or exe)?
For that matter, you don't even need the interfaces if you wouldn't have had them in a C#-only solution. Just define the class in F# and use it directly from C#.
You still need a separate assembly for F#, but that doesn't imply dependency injection - again, just reference it and use it.
The basic decision making logic needs to be very simple and easy to follow by scientists and people who aren't day-to-day software engineers: Basically, input some data, such as sensor readings, run it through an algorithm that makes decisions, and then output the decisions.
When mixing, you often write business logic as self-contained F# libraries with a C#-friendly API; and use C# to integrate them with whatever imperative, reflection-heavy and DI-heavy frameworks .NET is promoting the current year, since those are filled with interop edge cases for F# anyway.
You really want to avoid a language sandwich though (e.g. C#/F#/C#), because you'll keep wishing to remove the middle layer. Sadly, the addition of source generators make this mistake even more appealing.
`task` targets the .NET TPL instead, which is also what C#'s await/async and all of .NET *Async methods use.
While the `async` implementation still offers some benefits over `task` (cold vs. hot starts [0]), my advice is - if you're doing backend code on .NET, you should use task. The tigher integration with the .NET ecosystem & runtime results in better exception stack traces, easier debugging and faster performance.
[0] https://github.com/TheAngryByrd/IcedTasks?tab=readme-ov-file...
Trying to use a functional pipeline instead of DI.
Also, basic object initialization in C# has turned into a nightmare with recent versions. You need a flowchart to select among the 18 syntax options which suite your current needs.
With F# (and other newer languages), record fields are either `T` or `T option`. No need to worry about whether the value needs to be computed in a constructor and then remain immutable, whether it needs to be initialized by an object initializer and/or a constructor or not, whether it needs to remain interior-ly mutable throughout the life of the record, and so on. (Although as I recall you do still need to consider null values assigned to non-nullable references in your F# code that consumes C#.)
And while I agree that I don't love some of the object initialization patterns C# allows--I respect that other people might have different style than me and don't mind ignoring that those styles exist when writing my own stuff :)
My general rule goes something like:
1. Use record types for any simple data structure
2. Avoid using primary constructors (even on record types).
3. Use { get; init; } properties for everything unless there's a good reason not to.
4. For things that need to carry internal state, have different methods for mutations, emit events, etc., use a class with regular old constructors and either { get; } (for immutable) or { get; private set; } (mutable) properties as needed.
IMO its generally more readable as well to non-dev than C# and to dev's outside the Java/C# ecosystem (e.g. Node, Go, etc). I've shown F# code back in the day to non-tech stakeholders and they typically understand it (e.g. data modelling).
It's not always about the features such as keywords, built-in functionality and types. It's also how language features work together.
C# is more fit for an imperative or OOP style when F# is more fit for a functional style.
C# and F# both work fine with Rider or VS Code on Mac or Linux.
Even in a VM? Why not?
For F# , you need some basic C# knowledge For Clojure, you need some basic Java knowledge For Elixir, you need some basic Erlang knowledge
I like all 3 languages but usually each vm have a primary language, and each hosted language eventually become hosted on that primary language not the vm
I understand that for many task simple, to medium complexity, you might not need that, but it seem as you try to be more advanced you hit the wall of having to learn you host vm primary language
As an Elixir programmer, this does not resonate. Basically the only thing I've ever felt I needed to understand Erlang for was ets, but let's be honest, that's not really proper Erlang but just the terrible ets query syntax. And all this requires is "ability to read enough erlang term syntax to be able to understand the ets manual". I don't think I could write a single line of correct Erlang by heart.
I feel like that's different in F#, where you still need to know lots of .NET internals which are all documented in C#y terms with C# examples etc. Elixir wraps pretty much all good Erlang/OTP internals in nice Elixiry modules, which solves that quite nicely.
Elixir has its warts but this really isn't one of them.
What bits of java have you ended up needing? Like do you often use java libraries that don’t have clojure wrappers?
I feel like I’m often running up against little things. I’ll google “how to do xyz in clojure” and the top SO answer is to use a java library that apparently everybody already knows about, cause so many clojurists came from java first!
The same with Clojurescript and JS (and probably with Clojure-Dart) - you have nice interop with the hosting platform. The need for learning anything about Java (while writing Clojure) basically boils down to finding API documentation for a specific class and simply using it. That's all. That's all you'd ever need.
Honestly, none of this really rings a bell. Having used all three options, I never felt that. Well, I already knew C# before getting into F#, but honestly, it felt like my C# knowledge at the time was more of a distraction. Been using Clojure for nine years, never done any serious Java and have not felt any need for it. Not knowing Erlang wasn't a problem with Elixir, like at all.
- fable would 100% detach from dotnet - keeps up yo the LLM rush, specially vibe coding on cursor
Last LLM experience it generated obsolete grammar (not much but a bit).
Such la gauges are key for vibe coding experience and modeling.
* It’s easier to attract smart developers to an F# project than to a [mainstream language] project. This was one of my driving beliefs when I introduced F# seven years ago. https://www.paulgraham.com/pypar.html. This is probably just as true for languages like Elixir, Clojure, ... But F# is what we went with.
Small Software Shop Context
* We operated in a small market where customers eventually dictated our tech stack (.NET & React). In that market, F# was a major advantage—it allowed junior developers to build apps that "just worked" with minimal regressions. Even with mediocre code quality, I felt confident that we could refactor safely at any time.
* I constantly had to justify F# to clients, which was exhausting. We always delivered decent results, so it worked out, but my partners were never as confident in defending F#.
Bootstrapping a SaaS Company
* F# has been invaluable for shipping features quickly and taking shortcuts when needed.
* Three years in, our codebase is large and contains its fair share of messy parts. But we can still develop new features at high speed with minimal regressions. Refactoring is relatively safe and straightforward.
* Compilation speed is the Achilles’ heel. If you don’t monitor it, the compiler slows down to the point where it impacts productivity. Earlier this year, waiting over a minute for feedback after a small change became unbearable. A lot of our "clean-up" work focuses on optimizing compilation times. We're still learning, but we’re optimistic that we can restructure the project to significantly improve build performance.
EDIT: maybe one more point. I see a lot of C# vs F# popping up here. Yes, C# has all the features that F# has. But do not underestimate how well designed F# is. It is an extremely simple language to learn compared to C#. There is a very limited amount of keywords to learn. And they compose extremely well. If you learned F# 7 years ago, took a break, and came back today, you'd simply write the same boring code that you would have written 7 years ago. And along the way you'd find out that some things have gotten a bit nicer over time.
https://fsharpforfunandprofit.com/posts/dependencies/
FWIW you can do it exactly the same way you do it in C#; it’s not “wrong”, it might just feel a bit out of place.
https://youtu.be/US8QG9I1XW0?si=N07ZsYSYfA12mGVc
It seemed that I had to learn C# in order to use F# properly and it seemed that porting it to C# was the saner option. I went with Rust after all and it seems to have been the right choice.
Speed of development is debatable. I think you can be pretty fast with both.
Easy to learn I concede but it gets easier with time, until it becomes very easy
That was kind of my main problem with dotnet stack in general, although that was some years ago. I've tried sneaking F# into our project by building set of tests (the main codebase was in C#), but some of my teammates would have none of that.
My personal perception of the dotnet community was that developers either were too pigheaded about doing things "the Microsoft way", or hyped about "innovation" — "Oooh... check this out, Scott Hanselman made a whole new conference talk about it...", and then you'd look into this "innovation", and it often turns out to be some decades old, well-known thing, only "wrapped into MSFT packaging", which is not really bad by itself - it just seemed that people didn't actually use them in a practical way. I just never found that sweet-spot between pragmatism and excitement. You have to find a middle ground between: "This works! Sure, yeah, but isn't it darn ugly and probably not scalable?" and "This is so beautiful, and cool, but nobody besides Jeff really understands it." And my personal experience was that .net programmers would always want to be on one of those sides.