OCaml -> Scala vs F#

Hypothetically, if you were forced to use a non-ocaml language, and the choice of platform was irrelevant (i.e. please no .Net vs JVM arguments), would you go with F# or Scala ?

Context: I’m really curious about the merits of these two OCaml alternatives, from the perspective of language features, compile time, and IDE support.

Thanks!

1 Like

For my part, I’ve not written anything in either of those languages, but I’ve read a fair amount of the Scala documentation trying to learn how some of its more peculiar features work, and I’m pretty sure that, if I were made to choose between F# and Scala, I’d pick Scala. Mainly because F# would annoy the hell out of me, on account of it being more or less, “OCaml with all the good bits removed.” Whereas, pretty much everything I like about OCaml has a mostly reasonable alternative in Scala. And Scala has ad hoc polymorphism too.

3 Likes

I’ve used scala extensively in the last 7 years at work, but I have never used F#, the only knowledge of F# is from reading online. The experience of Scala is pretty enjoyable to me.

In a short sentence, scala has two advantages. (1) it basically has almost everything Haskell has: type class, for example. (2) Access to the entire JVM ecosystem, scala can call Java libraries pretty effortlessly.

The disadvantage: (1) compiling is much much slower… (2) People used to say scala devs have to deal with Java style of Scala from new coworkers, but it seems all Java style of Scala projects either move to Kotlin or moved back to Java now. The remaining scala project is pretty much functional style.

1 Like

From everything I’ve heard, F# unfortunately still just doesn’t have that great of an IDE experience. The cross-platform open source stuff seems to be buggy and slow. Also F# needs the user to manually sort their project files in compilation order. The order isn’t inferred like it is in OCaml.

As much as I like OCaml and some of that fondness carries over to F#, in reality these issues would drive me towards Scala, which has doesn’t have these issues and also has some banger libraries for very practical, performance-oriented programming, like ZIO.

2 Likes

I’d go for F#
compilation speed and tooling (rider/vs) are much better than scala’s
the language isn’t as flexible/featureful, but has many good practical features (e.g. type providers)
it’s OCaml-ish and much of your OCaml knowledge translates 1:1, scala on the other hand would take a bit more time to get up to speed with.

1 Like

I used F# 2-3 years ago for 2 smartphone apps (Fabolous & Xamarin) and a cross plattform GUI (Avalonia & Avalonia.FuncUI). All on Windows using Visual Studio and Net Core 5.0 with F# 5. As I did not succeed using the F# package manager (Pack?) with Xamarin I used VS to manage the Nuget packages for the Xamarin/Android part. After an update had to delete all Nuget Caches on the whole system to be able to build the project again. The language itself is fine and IDE support was, well usable (not comparable to C#).
My only Scala (2) I did was dabbling a bit in the Flix (another JVM language) compiler. If somebody told me, that that had been modern. Java (last version I have used had been 1.4?, the last Version that worked on Irix) I would have believed that. It feels like the worst features of Haskell, C++ (the “let’s include the feature, no matter if it fits with the rest of the language”) and Java in a single language.

Compile time of .Net and JVM stuff is too long. But with .Net you can at least cross compile to a binary containg the runtime for each of the supported plattforms.

So F#. On the JVM I’d use Flix (which isn’t usable at the moment), Clojure or Kotlin before considering Scala.

Actually I’m using Haskell as the OCaml alternative.

But because of the way MS treats the .Net OSS community, I stay clear of it.

1 Like

I’ve been a fan of F# for many, many years, and I would normally recommend it for it’s simple “functions + immutable data” philosophy over any language for “type-level astronauts” like Scala or Haskell, however I can’t do so anymore:

  1. F# has an strong inherent tension between its FP-first and OO-first language features, and while the ecosystem wants to promote the first, the second are actually more polished and full-featured, and a better match for the platform.
  2. C# is driving a lot of churn to the idioms and features of .NET, and while F# is trying to catch up, it’s not doing so fast enough, leading to many, many edge cases, missing features and increasingly clunky C# interop.
  3. F# development as a whole seems to be stalling, with few features beyond (attempts at) interop being added. Suggestions to solve many pain points of the language become “approved in principle” but consensus on how isn’t reached, or they make it to an RFC that no one actually implements.
  4. The toolchain (compiler and MSBuild) is really slow, and the open source editor tooling (VS Code extension, formatter, language service, etc.) is really fragile, breaking every other release (or .NET release) and hanging or misbehaving every few minutes.

Since most other FP languages are either for “type-level astronauts” or dynamically typed, and I’m seeing that memory access patterns are the new bottleneck for performant code, I admit I’m drifting away from FP entirely. When OCaml isn’t a good fit, I now just consider C#, Go or Rust (too trait heavy for my taste though) for my own projects. I’m not a fan of the JVM, but I wouldn’t mind using Clojure again when performance didn’t matter.

2 Likes

Could you please elaborate on the difficulties you experienced with C# and F# compatibility?

I’m biased, but if

I would use Scala to gain experience with more programming language concepts. There was a brief period when F# was first introduced, where it had some nice features hard to reproduce in OCaml of the time: type providers, syntax for monads. But now, with the disclaimer that I never used it so it’s an uninformed opinion, it’s just a very impoverished sibling of OCaml.

Sure, off the top of my head:

  1. The worst are byref-like types (ref-like in C# parlance), like Span, ReadOnlySpan and some new enumerators, which are spreading through .NET like fire and for good reasons; these types behave somewhat similarly to the local annotation proposed by Jane Street for OCaml, I believe. But the CLR imposes an (overly strict [1]) ban on using these types as generic parameters, which breaks F# in multiple ways:

    • You can’t use typeof<'a>, sizeof<'a> or Unchecked.defaultof<'a> with byref-like types, because byref-like types can’t unify with type parameters.
    • You can’t write inner functions that take or return byref-likes, because those compile to lambdas, which are instances of FSharpFunc<...> (at least conceptually, before inlining) where the byref-like would become a type parameter.
    • You can’t use byref-likes in (almost?) any computation expression, because they desugar into lambda calls; see above. This doesn’t work even when using inline attributes to ensure the resulting, inlined code would respect the byref-like rules. So we don’t get to use the new byref-like enumerators to build collections: [| for line in str.AsSpan().EnumerateLines() -> ... |] fails to compile even if it’s effectively a while loop that calls Add on a builder.
    • You can’t just raise exceptions in expressions that return a byref-like, because raise : exn -> 'a and 'a is a type parameter. This means if condition then str.AsSpan() else invalidArg ... fails to compile unless you also return a dummy value after raising (but not the obvious Unchecked.defaultof<_>!)
    • In most situations, byref-like types and methods returning them simply don’t show up in the autocomplete lists. I suspect this is because the search is type-directed using the same machinery.
    • Additionally, use _ = fixed _ doesn’t support arbitrary byrefs yet, much less the GetPinnableReference pattern, so you can’t pin spans to managed memory at all in F#. (I believe this is implemented and ready for the upcoming F# 8. [2])
  2. Some .NET libraries are becoming increasingly reflection heavy, converting delegates into ASP NET handlers and CLI commands at runtime. This is an issue for multiple reasons:

    • Under multiple generic overloads, F#'s type inference gives up and you need to specify the full type and arity of the delegate for each lambda: Func<int, string, Task<IResult>>(fun productId variantName -> ...).
    • Depending on how you construct your delegates or classes (and the version of the compiler you’re running), F# may generate CIL that doesn’t quite satisfy the unwritten contract that the library expects from equivalent C# code. If I recall correctly, when ASP NET minimal APIs came out delegates didn’t yet preserve the parameter metadata from the inner lambda, so the binding feature was unusable from F#.
  3. Some .NET libraries are becoming very extension heavy, with many different namespaces contributing essential functionality to a small set of types. This is tolerated because when you call a missing member, C# tooling offers a quick fix to import whatever namespace contains the extension method you want, but F# tooling does not, which turns it into a painful search in the documentation.

    Even for C#, however, this became so bad that C# eventually shipped “implicit usings”, where MSBuild SDKs and even libraries can implicitly open a dozen namespaces for you, so you don’t even need to know where the methods are coming from. F# thankfully doesn’t have this (IMO) misfeature, but I don’t expect this trend to make the extension namespace hell any better.

  4. C# is pushing source generators as part of the toolchain, and there’s a growing ecosystem of them. However, these are unusable in F# projects; the best you can do is write a C# project and consume it from F#, but any dependencies back into your F# code need you to split your code into dependency sandwiches of F#/C#/F#. These splits are the main reason people abandon mixed-language solutions in .NET.

    F# has type providers instead, which are very few, can’t actually inspect or augment any existing type and are (were?) clunky to write and ship.

  5. Back when init was added to C#, F# couldn’t safely initialize any such property without crashing at runtime. This is solved by now, but quite surprising at first.

  6. Bonus one, the other way around: When writing F# libraries for C# consumption, the idiomatic ways to write code in F# aren’t really usable from C#, you need to resort to those that generate the same CIL as C#:

    • Optional parameters, instead of ?myParam, they need [<Optional; DefaultParameterValue<(...)>] myParam [3].
    • Extension methods require [<Extension>] on both the class and the method. [4]

  1. Byref-likes in a covariant or phantom type parameter should always be safe, since they can’t leak anything provided by the caller. This would fix so many issues with F# compatibility. ↩︎

  2. It only took 6 years. ↩︎

  3. Sadly, you also need this to avoid allocation of optional parameters in pure F#, despite there being a completed RFC that claimed to support [<Struct>] ?myParam but actually didn’t. ↩︎

  4. These are also more powerful as they allow you to extend generic types with constraints or specific instances, whereas native F# extensions for generic types must apply for all types. ↩︎

3 Likes

What about Swift? I keep thinking that it might be a good language, but I never had time to learn it.

From a snarky thread on social about various programming languages:

Swift: Apple needs you to rewrite this again after you rewrote it last year.