The key high level features that F# inherits from Ocaml is a succint syntax due to type inference and 'if it compiles it's bug free' development.
So if you know how to formulate the problem so it leans heavily on the type system, you can implement something like a binary tree container, and presume it is correct when it compiles.
Following the same parameters bestows rest of the codebase with this robustness.
See for example the ACM article "Ocaml for the masses" by Yaron Minsky of Jane Street for very lucid practical examples https://queue.acm.org/detail.cfm?id=2038036
Better bugs in this context is bugs at a higher level, eg. application logic issues.
It's easy to dismiss this as "type theoretic stuff", however these features we are talking about are very natural to use and when dealing with static typing and FP you bump into them quite a lot. And given that OCaml has some of the best type inference in the business, this happens without you even knowing about it.
F# is also missing a means for doing ad-hoc polymorphism like in Haskell. So no type class capabilities, no higher kinded types either. This greatly diminishes what you can express in it and the gymnastics you have to pull off to work around it are not fun.
Here's for example what you need to do in absence of GADTs: http://fssnip.net/mq (n.b. this pattern is called "tagless final", resembling "visitor" from OOP land).
I love this sample because if it looks complex, well it's in fact more complex than it seems, as the type system is actually not fine with this usage either, so you end up with forced type castings anyway.
---
F# is a fine language, all things considering and not having certain features might be considered an advantage in certain circles.
However do learn OCaml and Haskell, because they have plenty to offer and you'll learn useful abstractions that in F# are not very accessible.
There's a substantial diff between OCAML and F# at this point, once you get past "core" programming with functions in both languages. Some of the more prominent things F# has that
* Computation Expressions
* Perf abstractions and compiler analysis for them, namely Span<'T>
* Type Providers
* The .NET generics system
* Anonymous Records
* Slices and ranges for slicing or generating list-like data
And as the parent user said, there's plenty OCAML offers that is missing from F#. I think it's a good idea to consider them quite differently, despite having the same functional core.
* OCaml has now landed monadic and applicative binding syntax ( http://jobjo.github.io/2019/04/24/ocaml-has-some-new-shiny-s... ), which I think makes it a superset of F# computation expressions.
* Regarding Span<'T> (or my preference 'T Span :-), I believe F# and OCaml have developed different optimizations and constraints. For example, in OCaml allocation is really cheap. But there are still well-known techniques for minimizing it.
* OCaml also has the PPX system, which allows to programmatically transform a program's syntax tree into a new tree. I believe this allows somewhere around the same power as type providers and quasiquotations, combined.
* Anonymous records are cool, OCaml objects are pretty similar in that they are structurally typed. Also interestingly neither of them supports pattern matching–if I recall the F# limitation correctly.
* Slices and ranges are cool, in practice there are pretty powerful and well-known OCaml libraries that provide those, e.g. https://ocaml.janestreet.com/ocaml-core/latest/doc/base/Base...
let int3 = int1 + int2
let float3 = float1 +. float2
However OCaml does allow operator redefinition and shadowing, so you can redefine and use any operators you want just by opening their specific modules (local opens, i.e. module opens that last for the scope of a single expression, are preferred): let int3 = Int.Ops.(int1 + int2)
let float 3 = Float.Ops.(float1 + float2)
Note: the above modules Int.Ops and Float.Ops are for illustration purposes only, they don't exist in the standard library. You could write them pretty easily, though.You can do: let ( + ) a b = ...
Just fine
let (+) a b = a#add bFor an ADT that doesn't need the power of GADTs: pattern-matching on a List<'a> gives you either an Empty or a Cons(x, xs), where you know up front before you do the pattern-match that x and xs are of type 'a and List<'a>.
For a GADT that is not an ADT: pattern-matching on an Expression might give you "Const(a) where a is an int", or "Equal(a, b) where a, b are bools", etc. Without doing the pattern-match, you can't necessarily tell what types you've ended up with.