30 comments

[ 2.6 ms ] story [ 59.7 ms ] thread
As an engineer writing OCaml (and Rust too), I don't find any of this stuff to be a pain point. The syntax is fine once you get used to it. Anecdotally one of my colleagues who is learning OCaml and contributing to the code is using an AI to help get the syntax right and that works for him. Types in Rust are way more complicated and obscure than the ones in OCaml.

The ecosystem stays out of the way as much as possible - we are mainly interested in interfacing with C code directly, and OCaml (and Rust) make that pretty easy. I've been writing this project for over a decade and the language has been reasonably stable (OCaml 5 made some changes to the C interface, but we coped). Rust actually causes far more code churn - there's virtually a monthly cycle of some Rust compiler update requiring changes to existing code.

> Types in Rust are way more complicated and obscure than the ones in OCaml

For the most common cases, I agree. I always wondered why OCaml remained such a niche language, given that many of its nice features took decades to appear in more mainstream languages. I thought it was because the language was too complex and advanced for the mainstream audience. However, with the rise of Rust, I don't think so anymore. Rust is, in many ways, as complex as OCaml and, in certain cases, even more complicated. Yet, it's becoming very popular.

So my second theory about its lack of popularity must be correct: it has beginner-unfriendly documentation. In fact, I would even say it's unfriendly to engineers (as developers). The OCaml community prefers to see this language as an academic affair and doesn't see the need to attract the masses. Rust is an example of the opposite. It's a complex language, but it's pushing hard to become mainstream.

On ML languages, you can try MLite first (it comes with a great manual and a paper too); and then you can jump into the biggies such as OCaML and maybe Haskell too:

https://t3x.org/mlite/index.html

Ocaml is one of the few languages I think you either really like or really don't.
Having learnt ML via Caml Light, back when OCaml was still going to be the next version, and known by its full name Objective Caml, I never considered any of that a pain point.
The article mentions that global type inference is painful, and Rust made the right call by requiring annotations, but actually I think the OCaml model is closer to the optimum: you can still annotate functions and perhaps you should (in fact the interface files are in my view great, but not everyone enjoys them), but the key benefit is that you can also use holes in such definitions, e.g.

    _ option list
is usually quite descriptive of what data you have, but still doesn't go into some detail you don't want to (sadly "int _ list" isn't supported by the type system). The types in Rust can sometimes get quite long, in particular if you want to avoid boxing and you're dealing with futures.
I second the point that Rust types can get quite long. I usually use aliases where I can but that isn’t always possible. I think that sometimes people use boxing just to avoid these long types even if the code in theory doesn’t require an allocation
Reminded me of this comparison between Ocaml and Sml with regards to the purity vs practicality. [0]

The complaint about syntax is amusing, I get a bit annoyed when I see Rust syntax being inserted into Java/C-ish languages.

[0] http://adam.chlipala.net/mlcomp/

Standard ML was my favorite language 2008-2012, and whenever people criticize the syntax, I shake my head. ML syntax is among the most beautiful, and most readable, to me. OCaml has a few features that bring the readability down a little, like the named fields prefix. The fact that it doesn't look like C or Ruby isn't bad. Erlang can be very readable, it just feels alien, which I presume is what people don't like.

The big downside is the paradigm shift + inscrutable error messages.

It will make you feel dumb and/or unproductive for a very long time.

A lot of the type errors can be fixed by a compiler that cares more about error messages, and OCaml in particular could do well with terminators after e.g. match blocks to improve both ambiguity and error reporting. Some of the type errors can't easily be fixed; wrong function application by omitting some value somewhere has, so far, always led to misleading errors; are there any other examples than Elm that address these?

I also switched from ML to Haskell to Rust.

To me, the main downside of ML is the explicitly instantiated module systems not tied to the filesystem hierarchy.

Once you get used to traits and parameterised trait instances, bringing every module instantiation into scope after separately importing them is just slooow.

The standard library situation is also somewhat not great. Maybe I just never got used to it, but I sense that there are people think Jane Street Base is a must, and people who think it's completely overkill. OCaml programmers have a weird fetish with micro-optimization because the compiler lets you think about the asm output. It's a remarkable property of an otherwise high-level language that you can really care about the asm output.

> there is no equivalent to Java's toString or Rust's #[derive(Debug)]

There are devivers and a Format module for pretty printing.

https://github.com/ocaml-ppx/ppx_deriving

https://ocaml.org/manual/5.4/api/Format.html

That's slightly disingenuous, as in the case of ppx_deriving you always need to state the type of the expression to call it, e.g.

    show_file
or

    [%derive.show: (int * int) list]
from the page. It's basically the same story for Format.

What many other languages do that if you have an object, you can just transform that object into a string or output it with debug formatting, without any particular support from the object. It isn't much of a chore, but it is some; and polymorphic functions now need one extra argument for the formatter.

I do recall there's a module for OCaml that can do "debug level" printing of objects by traversing their runtime structure, but that of course doesn't suite all objects.

At least in Rust it's not automatic that you can print an object in the first place :).

what's always wild to me about these language debates is how much energy people spend defending syntax preferences instead of asking "does this help me ship?"

OCaml's real pain point isn't the syntax - it's that you can't hire for it. if you're building a startup and you pick OCaml, you've just cut your hiring pool by 95%. that's way more painful than learning a different way to write functions.

the whole "academic vs practical" thing is backwards. academic languages often have killer features that would save you real pain, but if your team can't debug it or you can't find Stack Overflow answers at 2am, none of that matters. language choice is a business decision, not a technical purity contest.

I wouldn't hire someone who can't learn such a small and simple language in a week.
> Usually partial function application is a bug

Dude, what?

I would rather have partial function application be explicit rather than just omitting args because right now if you accidentally forget an argument (a bug) it isn’t an error there and instead becomes a type error somewhere else.
My favorite part about Ocaml is the type checker fwiw. Pretty much as soon as I appease it I'm guaranteed a 'working' program that usually does what I intended.
My problem is that when you’re trying to appease it the errors it spits out can be confusing or in the wrong place
I've been writing OCaml for quite some time now and I don't think any of those "pain points" are "pain" or really any problem with the language. I think they're more like a preference. The syntax or no forward references are just decisions that makes the language good in some ways and bad in others, and seems like you just really don't like the cons.

Also, isn't freedom of annotating a function a good thing, instead of having a requirement on it? You can just explicitly annotate some stuff when you have a type error that doesn't feel right!

OCaml is way more simple from the type system perspective than Rust, I think it makes it great when you need a lot of iteration on you code.

One thing I do agree on are problems with the ecosystem. Maybe not quite in the specific area described here but man we got some problems in IO/concurrency/asynchrony space. Making a GOOD standard for it would just improve all our lives as OCaml developers.

I employ some strategies when I work with OCaml to avoid some of these pain points: I avoid libraries with poor documentation, funky types, and preprocessors that don't throw proper error messages. So I skip ocamllex, Menhir and Reasonml because I've found like OP that the DX is too poor..

But otherwise I don't particularly agree with OPs pain points. I personally find OCaml's syntax to be great. Very easy to write and read once you're used to it. And I previously thought that it's very important to annotate all types, as it would help with my thinking, but these days I find I'm more productive when I write out the code without types, maybe add a type here and there. I only annotate if OCaml's deduced type is too complex or generic.

Maybe it’ll grow on me as I get used to it :) but e.g. I was really confused about a nonsense error in a match statement until I realized that it was because of a nested match statement.

If you don’t use ocamllex/Menhir, what do you use for parsing & compiling?

Is the DX of ReasonML poor? (I haven't used it.)

If so, that would be unfortunate... because the whole raison d'etre of ReasonML is DX! Specifically, to provide a more familiar syntax for programmers coming from the curly-brace world.

Im sorry but this reads like a noob trying out ocaml for the first time and beeing annoyed because it does not work like Java.
My main gripe with ML variants is the poor syntax around async (F#). For networked (web) applications, you begin to find that many or most of your functions are async, and wrapping them in computation blocks which come with their own internal syntax that eliminates a lot of the elegance of ML and makes your code look more imperative often feels like it defeats the purpose of the language.

In F#

    let Foo =
        async {
            let! data = getData() |> Async.AwaitTask
            return data.value
        }
Is this the best they can do? C# just has

    async Task<Data> Foo() =>
        (await getData()).value;
Ive always wanted ML syntax like this:

    let async Foo =
        let data = await getData
        data.value
I just dont write a ton of pure functions which have no I/O dependencies
This is outdated.

    let foo = task {
        let! data = getData()
        return data.value
    }
In F#, you interoperate with Task<T> transparently, and there is a number of community libraries to further enhance the experience. It also supports nice combinators like and! out of box.
We have exactly that in OCaml. Check out Eio.