My Thoughts on OCaml vs Haskell/Rust in 2023

Swift, D, Nim, Crystal, and maybe even C# fit this description.

  • Swift: maybe, but it’s locked into the Apple ecosystem (even though they try to pretend it’s not).
  • Nim: no sum types. Or, really, they have record variants but there isn’t even a check that you’re not using the fields from the wrong case!
  • Crystal: yes, possibly. It’s quite young, and it’s another quite peculiar syntax (ruby-like) that is not to the liking of everybody.
  • C#: more like F#, no? I don’t know much about C# but I think it’s not expression based.

If anything, F# would be the closest, and might gain more momentum now that dotnet core is more portable. See also the migration of the darklang people.

One thing that I think is essential is that this “future” Rust-with-GC is that the language should provide users linear types (in addition to normal types). (Rust has affine types which similar in some ways to a linear system).

Linear types help with you not forgetting to releasing resources. (They are also useful in manual memory management ala Rust. But since this language will have GC that will not be necessary). So if you open a file, linear types can force you to close the file after you’re done with it.

Here I want to point out https://austral-lang.org/ which is written in OCaml (the author has written a blog post which reference by @lambda_foo above ). Austral has many nice features including linear types.

The main problem according to me is that Austral lacks GC :slight_smile: !

BTW linear/affine types are sorely lacking from Nim, C#, D, Swift. So this is one more reason why Rust-with-GC might be viable.

No doubt there are issues with every language, and there’s no clear-cut “rust but with GC” option to point to. But, “type safe GC’d language”, there’s bunches of those (OCaml included), and more I didn’t mention. My point in general though is that that niche is pretty well covered, and adoption among all of them is what it is; the same magic wand that could produce “rust but with GC” could far more easily produce “OCaml but with a 10x larger library ecosystem”, with probably better overall adoption given how hard it is to bootstrap a new language ecosystem.

NB, whatever one thinks of the darklang migration to F#, the project is 1-2 people at this point. Not nothing, but I don’t think one should read much into it as a trend or anything.

2 Likes

I think they’re very important for some crowds. C++ programmers are very traditional, so providing them with a familiar syntax is important if you want to have them try something new. Python and ruby were the ‘cool kids’ of the pre-JS era. In general, I think python does a great job of appealing to non-programmers due to how syntax-light it is. You really can see the algorithm quite clearly without a whole bunch of syntax getting in the way.

1 Like

I think that’s exactly right, aesthetic expectations are strongly correlated with community history, etc. But if we’re stipulating that a tracing GC is an essential aspect of some future ML, then C++ / “systems” programmers are necessarily not a target demographic given their allergy to such things, “application” programmers are. In that context, yes, parens and braces have a strong lineage, esp. via Javascript and such, but it’s hardly a unanimous requirement (again, python, ruby, and even VB remains as popular as e.g. typescript, at least according to available data).

Hello to everyone, I am new at this group. My short background: software developer in several areas (embedded systems, web development, compiler construction, simulation systems, etc.) in many different languages in industry and academia.

(As a new user I was only allowed to mention a maximum of two links in this post. I intended to post links to all interesting topics. Sorry for the inconvenience!)

I have been using OCaml happily for years now, and I understand some of the criticisms. However, in my case a lot of them faded away since I began to use OCaml in a “classic” way, which means to avoid too much terseness, and to use syntactical means to avoid ambiguity (using redundant begin … end or (…) in particular). I also use “;” and “;;” more than actually needed to avoid ambiguity. I favor a “KISSS” principle (KISS with three “S”) in OCaml: “Keep it simple, stupid AND safe”

Now, I would like to add some points to the post and the comments made here:

Regarding PPX, there is an alternative called MetaOCaml, a variant of OCaml which supports (typesafe!) macros like Lisp. It’s really nice and usable. However, I realized that I had much less (actually NO) need of any macro in OCaml so far.

Regarding FFI, ctypes make C-bindings MUCH easier. See Type-safe C bindings #1: Using ocaml-ctypes and stub generation | Skylight Symmetry

Regarding the ecosystems JVM and dotnet, there exist OCaml bindings called OCamlJava and CSML.

There is also JS_of_OCaml which compiles OCaml bytecode to Javascript. That means the WHOLE OCaml language is usable in the Javascript world. There is also a syntax variant called Reason for those who dislike pure OCaml.

Anyone who is seriously interested in OCaml should take a close look at “Awesome OCaml” (at github). It presents a curated list of many things related to OCaml.

For newcomers, I strongly recommend Prof. Michael R. Clarkson’s fabulous lecture “OCaml Programming: Correct + Efficient + Beautiful” at OCaml Programming: Correct + Efficient + Beautiful — OCaml Programming: Correct + Efficient + Beautiful . The book is free and accompanied by a series of 200+ short video clips. They expose how beautiful OCaml code actually can be. They also explain how unit tests and formal verification can be included seemlessly in OCaml programming.

One of the first points newcomers (in particular those who come from C-like languages) MUST understand is the semantics of semicolon in OCaml. They do NOT behave as in C but they are a completely different thing. In OCaml, single semicolons are just delimiters of list elements, and double semicolons are required wherever groups of code need to be separated. Believe it or not, this was my very first stumbling block when I began to dive into OCaml. Thanks to the nice OCaml community I was able to realize and fix the problem very quickly. My general advice: Enclose EVERYTHING in parens (…) or in begin … end which belongs together, otherwise you can get into trouble easily (dangling else etc.).

Happy coding!
Ingo

8 Likes

There is an implementation of Standard ML called MLKit that uses region-based memory management in addition to GC. GC can be turned off for real-time use cases. Home Page
Maybe it’s worth exploring the possibility of adding the region-based memory management to OCaml.

if you’re gonna enforce this, at least make inference possible again with type wildcards.
I hate spelling out everything for Rust. It gets way too verbose sometimes :smiling_face_with_tear:… Sometimes so verbose the signature is taking the same number of lines as the implementation.

making absolute qualification the default and enforcing type signatures takes so much joy out of writing and (more importantly) changing things up. I’d argue even reading! because the “shape” of what’s in front of you gets lost in the noise.

I also feel like this makes toplevel functions too distinct. Perhaps a nicer thing to enforce would be type sigs only for pub functions, because it is a contract between you and whoever is using your code, and contracts should not allow for ambiguity or confusion.

5 Likes

Yeah, can’t agree more. Aside from the tangible reasons why I turned away from Rust towards OCaml N years ago, Rust’s insistence on explicitly typing functions was my #1 problem with it. The sprinkle of toil throughout every task in the language made it extremely unpleasant to work with, at least for me.

1 Like

Yeah I like that. I tend to annotate almost always so that I don’t have
to qualify anything inside a function but I agree that a balance is
useful.

I think a good part of rust’s verbosity is because you have generics
everywhere (because of lifetime annotations) and you can’t open scopes
that bind generic parameters. If I was to write a rust-like language I’d
have something like this:

open<T:Foo, U:Bar> {

  // any toplevel declaration, impl block, etc. can go here
  // and does not have to re-quantify on T and U
  …

}

1 Like

That is exactly ocaml’s take, except the annotations are in another file.

1 Like

On the topic of type annotations, I’m pretty happy with where OCaml is. In Haskell, I used to dread moving functions to the toplevel scope because they’d need annotation. It’s absolutely necessary to make the type system work in Haskell and Rust, but I dislike it.

My complaint on this front in OCaml is mostly aesthetic. Adding type annotations to a function is very parenthesis-heavy, which tends to push me in the direction of wanting to remove type annotations so as to ‘clean up’ the code. With a “classic” function syntax such as Reason’s (e.g. let foo(a, b, c) = ...), the type annotation don’t add much extra weight (let foo(a: int, b: string, c: float) = ...).

4 Likes

Regarding the type annotations for functions, it would be great if one could add a type annotation for a function in a structure without having to modify the declaration of the function itself. This could be achieved by allowing val declarations inside structs. For example:

val string_of_bool : bool -> string
let string_of_bool = function true -> "T" | false -> "F"

The OCaml language server allows you to show the inferred type annotations as code lenses. This is shown just above the function definition – it is basically what you want.

See for example ocaml code in vscode (where this works without any config) or any other editor that supports LSP code lenses.

1 Like

Sure, that helps in many cases, but it still falls short in may others: due to aliases the type shown by the editor may not be the one you want to communicate; also, there are lots of everyday tools which don’t show those hints (e.g. reviewing a code snippet on Github).

In that case you seem to be asking for a Haskell style type annotations. It would require a change to happen in the compiler. Given that you can already (a) annotate a function with its type (maybe in a way that is not as convenient as Haskell) (b) Have other workarounds like code lenses, I doubt that a decision will be made to add this redundant ability.

You’re right about the type alias issue: even when you use the type alias in the mli for a function signature, merlin/ocamllsp will often infer the parameter type and ignore the alias when showing you the hover.

I think it would be more profitable to work on making merlin better. The change to the compiler would be far more difficult to do and get through!

I love this suggestion. In the rare cases when I need to add an explicit type declaration (talking here about .mli-less applications, not libraries), the parameters of the function always need to get shuffled around.

@dario’s example doesn’t demonstrate this, since the string_of_bool function doesn’t enumerate its parameters (the annotated declaration could just be let string_of_bool : bool -> string = function .... Consider:

let find_service cfg region role = ...

(* current annotation options *)
let find_service (cfg : Config.t) (region: Region.t) role : (Service.t, [`Service_not_found of string]) result = ...

let find_service : Config.t -> Region.t -> 'a -> (Service.t, [`Service_not_found of string]) result =
  fun cfg region role -> ...

(* @dario's suggestion, allowing the existing function declaration to remain as-is *)
val find_service : Config.t -> Region.t -> 'a -> (Service.t, [`Service_not_found of string]) result 
let find_service cfg region role = ...

Code lenses and other tooling isn’t a workaround for cases when an existing declaration actually needs some type annotation assistance (to resolve some ambiguity somewhere, etc). But yes, this would require changes to the compiler; making adding such annotations easier / more convenient sounds like a positive change to me.

Maybe I’m an outlier but I really like annotating code with the current
syntax. There are rare cases where I have to write:

let foo : type a b. (a,b) crazy_gadt -> a yolo -> b bar =
  fun x y -> …

and tbh it’s just not as good. What annoys me:

  • harder to find out which argument has which type (you need to count)
  • labels/optional labels must be repeated twice
  • to get the return type you need to parse the type instead of looking
    for the more recognizable pattern … : <type> = … just before the
    definition. The return type is even more important for readability than the
    rest, and I’ll almost always annotate the return type even if I let
    the arguments un-annotated.
4 Likes

I guess this is a bit verbose, but it comes close to what you want:

include (struct
  let string_of_bool = function true -> "T" | false -> "F"
end : sig
  val string_of_bool : bool -> string
end)
4 Likes

I admit to being partial to OCaml’s way of doing things, the language drew me to it that way after all, and here we are on its forums :grin:

3 Likes