I've dabbled in F# (and aside from the rough setup with little coherent information at the time) had a pretty good time. Actor-based concurrency was easy to grok. The one gotcha was whenever those mutable Arrays entered the picture.
I'd like to hear some practical reasons for preferring OCaml over F#. [Hoping I don't get a lot about MS & .NET which are valid concerns but not what I'm curious about.] I want to know more about day to day usage pros/cons.
It's a matter of trends: F# is losing compatibility with the overall CLR ecosystem due to the churn in C# features with poor F# interop, but F# has already mingled its design with the CLR's, too much to live on its own as a native language. Plus its compiler is slow and the tooling is getting increasingly unstable.
Meanwhile, OCaml got rid of its global lock, got a really fast-compiling native toolchain with stable and improving editor tooling, and has a cleaner language design with some really powerful features unavailable to F#, like modules/functors, GADTs, effects or preprocessors. It somehow got immutable arrays before F#!
F# still has an edge on some domains due to having unboxed types, SIMD, better Windows support and the CLR's overall performance. But the first two are already in the OxCaml fork and will hopefully get upstreamed in the following years, and the third is improving already, now that the opam package manager supports Windows.
> has an edge on some domains due to having unboxed types
If a language makes "unboxed types" a feature, a specific distinction, and has to sell "removing global lock" as something that is a massive breakthrough and not table stakes from 1.0, it can't possibly be compared to F# in favourable light.
I vouched for your dead comment because I get where you're coming from. But OCaml 1.0 was in 1996 in an academic setting. I care about the language I can use now, not the one I didn't use.
Let's not dismiss that their solution to remove the global lock has been both simpler than Python's (better funded?) ongoing work and backwards compatible; and the multicore support came with both a memory model with strong guarantees around data races, and effect handlers which generalize lightweight threads.
I agree that lack of unboxed types is a fatal flaw for several domains (Worse, domains I care about! I usually consider Rust/Go/.NET for those.) But when the comparison with OCaml is favourable to F#, the gap isn't much wider than just unboxed types. F# has an identity crisis between the ML parts and the CLR parts, its C# interop is decaying as the ecosystem evolves, and newer .NET APIs aren't even compatible with F# idioms (here's a .NET 6+ example [1]).
Ref structs are hard to solve because they require, in some ways, a lot more work than the way C# solved the lifetimes as the lifetimes in F# may need to become much closer to what they are in Rust. Some constructs may not be able to consume them still (can’t move ref struct into an object with effective lifetime ‘static) and there really is no easy solution short of language editions and breaking standard library changes. A limited improvement is possible, but we’re still in the realm where the lifetimes may become a part of type identity or somehow generalized over, which is hard.
OCaml predates multicore CPUs. Having a global lock was basically free at the time it was invented. It's totally crazy to dislike a language because the authors made a decision that was obviously correct at the time.
F# uses the CLR term "value types" instead, or sometimes "struct" types as in the C# keyword.
These are usually defined with the [<Struct>] attribute over a regular type definition, or using the struct keyword before tuple/anonymous record types. Also, the original 'T option type now has a value-type successor 'T voption.
We’ve used f# professionally for the computations-intensive parts of our product for a couple of years. Here’s what comes to mind:
1. Interop with C# is great, but interop for C# clients using an F# library is terrible. C# wants more explicit types, which can be quite hard for the F# authors to write, and downright impossible for C# programmers to figure out. You end up maintaining a C#-shell for your F# program, and sooner or later you find yourself doing “just a tiny feature” in the C# shell to avoid the hassle. Now you have a weird hybrid code base.
2. Dotnet ecosystem is comprehensive, you’ve got state-of-the web app frameworks, ORMs, what have you. But is all OOP, state abounds, referential equality is the norm. If you want to write Ocaml/F#, you don’t want to think like that. (And once you’ve used discriminated unions, C# error-handling seems like it belongs in the 1980’ies.)
3. The Microsoft toolchain is cool and smooth when it works, very hard to wrangle when it doesn’t. Seemingly simple things, like copying static files to output folders, require semi-archaic invocations in XML file. It’s about mindset: if development is clicking things in a GUI for you, Visual Studio is great (until it stubbornly refuses to do something) ; if you want more Unix/CLI approach, it can be done, and vscode, will sort of help you, but it’s awkward.
4. Compile-times used to be great, but are deteriorating for us. (This is both F# and C#.)
5. Perf was never a problem.
6. Light syntax (indentation defines block structure) is very nice until it isn’t; then you spend 45 minutes how to indent record updates. (Incidentally, “nice-until-it-isn’t” is a good headline for the whole dotnet ecosystem.
7. Testing is quite doable with dotnet frameworks, but awkward. Moreover. you’ll want something like quickcheck and maybe fuzzing; they exist, but again, awkward.
We’ve been looking at ocaml recently, and I don’t buy the framework/ecosystem argument. On the contrary, all the important stuff is there, and seems sometimes easier to use. Having written some experimental code in Ocaml, I think language ergonomics are better. It sort of makes sense: the Ocaml guys have had 35 years or so to make the language /nice/. I think they succeeded, at least writing feels, somehow, much more natural and much less inhibited than writing F#.
I did get some sense of that where F# sort of (actually is) 2nd class to C# which is slowly gaining some F# features, but F# itself isn't well promoted/supported.
As for missing language features, they can also be a double-edged sword. I slid down that slippery slope in an earlier venture with Scala. (IIRC mostly implicits and compile times).
There are libraries that simulate a lot of these things (e.g. https://github.com/G-Research/TypeEquality for GADTs). You're absolutely right that it's not as first class as in OCaml, but the tools are there if you want them.
I've written type-equality witnesses in F#, they can kinda recover type equalities (with cast methods) but not refute them, so you still need to raise exceptions for those cases.
OCaml is a much more powerful language. Take a look at functors, for example, or modules as first-class values, or GADTs, or its object system that can do type inference from use:
# let foo x = x#frob;;
val foo : < frob : 'a; .. > -> 'a = <fun>
F# is often called "OCaml for .NET", but it is a misrepresentation. It is an ML-family language for .NET, but aside from that ML core they don't have much in common.
Whether those features are more valuable to you than the ability to tap into .NET libraries depends largely on what you're doing.
I'd like to hear some practical reasons for preferring OCaml over F#. [Hoping I don't get a lot about MS & .NET which are valid concerns but not what I'm curious about.] I want to know more about day to day usage pros/cons.