1. 50
    1. 12

      I’m not claiming that OCaml is perfect in any way, even though it’s one of my favorite languages. Still, there are quite a few things in the post that I think are either advantages or non-issues.

      In OCaml, like in C, declaration must appear in dependency order.

      To be fair, I enjoy the fact that any dependency of a function can only be in once place — above its definition.

      And, again, Haskell gets this right: the unit type is the empty tuple, denoted (), and its sole value is ().

      If I were to design a language from scratch, I might want the unit type to be a completely separate token that doesn’t look like anything else. In the type lattice, it is a separate type after all. But I have no strong feelings about product type syntax in any language.

      Haskell is not much better: List Int. This obsession with terseness is a big problem: please give me punctuation.

      And obsession with punctuation causes turbofish-like phenomena. One thing that this debate usually doesn’t account for is that to a completely uninitiated reader, none of those syntax variants is immediately obvious, and experienced readers know what they mean.

      Semicolons Work Sometimes

      The simplest way to do what the author wants to do is to enclose the semicolon-separated sequence of expressions in parens of begin ... end:

      let foo _ =
        if true then
          begin
            print_endline "Hello, world!";
            true
          end
        else
          false
      

      This isn’t too different from other indentation-insensitive languages — in a C-like syntax one would need to write if cond { foo; bar } else baz and I’m still to see anyone complain about it.

      The syntax for a list literal is [1; 2; 3]. This is because the comma is an infix operator, so if you typo this as [1, 2, 3] you don’t get a syntax error, that’s a singleton list with a tuple as its element type.

      It actually saves a lot of space when you need a list of tuples: ["foo", 1; "bar", 2; ...]. I find that syntax unusual but not bad.

      Again, because match statements have no terminating delimiter, you can’t nest them in the obvious way So everything gets slightly out of alignment, and when you have a few nested match statements, the code starts to look like Lisp, with a trailing train of close parentheses on the last line.

      I lean towards using begin ... end instead of parens in nested match expressions and conditionals. I also indent them, while some people and formatters insist on not doing it — it may be my Pascal past and my love for Ada, but I think more people should be using those keywords. I’m certainly glad they are there.

      The drawback is you lose implicit instantiation. You have to manually instantiate modules, and manually refer to them. You can’t write show x, you have to write FooShow.show x. This adds a baseline level of line noise to all code that uses modules.

      There are cases when functors are invaluable. I’m tempted to write that word in bold caps with spaces between letters to emphasize it: I N V A L U A B L E. ;)

      For example, when I was completely done with then-only TOML library for OCaml, I wrote one that allows the user to bring their own bignum and datetime dependencies if they want to, or use the default dependency-free version: https://github.com/dmbaturin/otoml/#bring-your-own-dependencies There is no way that could be done without explicit instantiation, at the type and module system level.

      Another example is Lua-ML — an embeddable interpreter that allows client code to partially or completely replace the runtime library.

      I’m not exactly against modular implicits if they land in the language, but there are cases when there is no replacement for functors.

      It [type inference] is bad because when reading code, you either have to mentally reconstruct the type of each variable binding, or rely on the tooling to tell you the type, and the more obscure the language, the less reliable the tooling.

      Merlin has been perfectly reliable for me. I have no idea what the author is talking about.

      When you don’t have access to the tooling, then yes, reading can be harder. But then you have proponents of dynamically typed languages who never complain about missing type annotations. From my side, well, I find that reading any code without knowing the context can be hard. At least if we are talking about deep reading.

      In C, for example, suppose I have no idea what’s time_t. I can infer (ahem!) that it has something to do with time, but I still need to do quite a bit of reading to learn that it represents UNIX timestamps, how to construct it, what I can do with it, etc.

      One thing I’m surprised the article doesn’t mention is the REPL. I can experiment with any code in the REPL, and I can see the inferred types there, even if the REPL is the only tooling I have on hand. I really miss that in languages that can’t do that and the fact that it has a REPL and can compile to native code is one of the killer features for me.

      1. 1

        but there are cases when there is no replacement for functors.

        From the descriptions you’ve provided, it sounds like functors are perfectly replaceable by a combination of bounded polymorphism and existential types, which are available in many languages (Haskell, Rust, Scala…)? Of course, it’s possible that you simply mean that other solutions are too cumbersome to use in practice, although I personally prefer that tradeoff over modules.

        The features of OCaml’s module system which I think aren’t as easily replicable are things like include, destructive type substitutions, etc., which permit a kind of flexible duck-typing where you take an existing module and update just a few items and reuse it, but I don’t think those features are necessary in your examples.

        1. 1

          Functors allows you to produce a new module specialized for any transparent type. I’m not ready to say that what I’ve done in OTOML is impossible in Haskell because I have no proof of it at the moment but I don’t see a way to do it there.

          1. 1

            Functors allows you to produce a new module specialized for any transparent type

            I don’t see why it matters whether the module is specialized for a transparent type. You’ve introduced the module type and instantiation, so, just as in a typeclass-based approach, you had to introduce a wrapper type to get the behavior you want. A “wrapperless” approach might be with generic types, but you would still need to bound the generic types (or take a C++/Zig-style duck-typing approach).

            I have a draft on encoding ML-style modules in Rust (feedback welcome). I believe you can do the same thing in Haskell with type families as they seem to be a more general version of Rust’s associated types.

            1. 1

              So you’ll need not just the type-system equipment (bounded polymorphism, existential types or typeclasses, newtypes) but also something like Haskell’s guarantee that a newtype is a zero-cost wrapper.

              1. 1

                Can you elaborate on what the zero-cost wrapper guarantee achieves? And what is the alternative you have in mind? I think e.g. Scala would have you box the values involved, but OCaml will also box most aggregate values, so it doesn’t seem appreciably different in terms of the runtime representation.

                You could say that Scala (and maybe Haskell?) will require dynamic dispatches while OCaml and Rust could monomorphize them, but I think that’s orthogonal to the idea of specializing the module for a “transparent” type: the resulting output type will still remain “transparent”/unwrapped, even if you have an intermediate wrapper type that represents the module instantiation itself.

                1. 2

                  The zero-cost guarantee is implicit with functors; you only pay for what’s in the final signature. Otherwise, you have to be aware of caveats, like how Haskell implementations may box newtypes when they’re used to compose nested data structures.

          2. 1

            For completeness, I worked out a fairly idiomatic-looking example of using data families as associated types to implement the functor: https://play.haskell.org/saved/VNsabwp0

            The above encoding introduces a non-transparent type (although, of course, for the caller, it won’t be opaque). It seems that you can use type families as well to specify the associated types transparently, but I needed to use UndecidableInstances to get the Show instance to work, so it would probably require a Haskell expert to fix up to be idiomatic: https://play.haskell.org/saved/pARhOWvL

    2. 10

      It blows my mind how old this language is. It got so many things right.

      But then again, I took over a non-trivial code base in OCaml and had to create a team around it. It was just impossible. We tried for a year, but in the end we ported to Rust. Sorry OCaml, we tried. :(

      1. 1

        Ouch. That must be frustrating. There are thousands of college students who want to work in ocaml instead of (anything), and you couldn’t find anyone to hire :(

        1. 1

          We live in different countries you and I :)

    3. 7

      The OCaml code in the wild example is spot on. And hilarious.

    4. 5

      Been using OCaml for programming language experiments and have to say I really enjoy it! It’s really nice for throwing together quick implementations, especially when using menhir for parsers.

      I disagree with a lot of this post to be honest, but me and the author obviously have very different tastes. The module system is really great… wish more languages had something similar, even if I do wish for modular implicits. I do agree that the syntax is very goofy and takes a bit to get used to. The postfix, uncurried type application is annoyingly inconsistent with function application, and nested match expressions are a pain (beginend can help a bit though).

    5. 3

      I really long for a language that checks all the boxes that the author gives at the end, but would :

      • have the tooling, ecosystem (instead of I don’t know how many standard libraries) and concurrency story of Go,
      • be even less galaxy brained than Ocaml: of course when compared to Haskell it looks reasonnable, but for “mere mortals” it’s still pretty weird (it originated in academy and it shows. That’s not inherently bad, but I’d like to see one targeted at engineers).
      1. 2

        The problem with continuously inventing new PLs to find the Goldilocks fit among them, is that supporting a PL and its ecosystem is a crapton of work. OCaml is about as old as Java, and look at where its tooling and ecosystem are. A language like Go doesn’t get funded and developed every day, and even if it did, it still takes time to bake and iron and polish it to the point where it’s widely usable in production. Meanwhile we have OCaml, a system which has been carefully engineered for decades and has a solid foundation with remarkable backwards compatibility. And really, it’s only as galaxy-brained as you make it. Here’s the most pedestrian thing done in OCaml–Unix system programming: https://ocaml.github.io/ocamlunix/ocamlunix.html#htocintro

        OCaml is a language targeted at engineers. Its developers are Unix hackers who also happen to be computer science academics.

    6. 2

      Particularly if you what you want is “garbage collected Rust that’s not Go”, OCaml is a good choice.

      Rust and Go fits different purposes. And why should OCaml be chosen over Go? The main point of using Go is to get amazingly short compilation and test cycles, even for large projects.

      1. 8

        Rust and Go fits different purposes.

        Rust and Go are similar in that they’re some of the only memory-safe ahead-of-time compiled languages at our disposal.

        And why should OCaml be chosen over Go?

        The specific criteria that the author was using were these points:

        1. Is statically typed.
        2. Has a solid type system, by which I mean algebraic data types with exhaustiveness checking.
        3. Is garbage collected.
        4. Compiles to native.
        5. Has good out of the box performance.
        6. Is not too galaxy brained.
        7. Lets you mutate and perform IO to your heart’s content.
        8. Has a decent enough ecosystem.

        OCaml can be picked over Go if point 2 is more important than point 8 for your software.

        The main pont of using Go is to get amazingly short compilation and test cycles, even for large projects.

        It’s not discussed in the article, but the OCaml compiler is also fairly fast, particularly compared to the Rust compiler.

        1. 3

          the OCaml compiler is also fairly fast, particularly compared to the Rust compiler.

          The bytecode compiler is much faster than the native code compiler though. To the point where in some cases I found myself using the bytecode compiler because compilation times were dominating my otherwise short test cycle.

      2. 6

        With OCaml you get more safety and expressivity

        1. 1

          I agree. But is there measurably more safety than in Go, though, from a deployment/security perspective?

          1. 9

            Yes.

            Golang’s type system doesn’t even attempt to prevent nulls, which are the most common type error by a wide margin. There is an enormous difference.

            1. 1

              nulls is not a common type error in Go. I understand that it may look that way from a syntax perspective, but in practice, and from a deployment/security perspective, I don’t believe that this is the case.

              I wish there was statistics available on this for Go.

              1. 6

                While not a direct answer to your question, some of the concerns raised in this paper make me want to believe that you are much more likely to shoot yourself in the foot in Go, security and deployment wise.

                https://arxiv.org/pdf/2204.00764.pdf

              2. 5

                nulls is not a common type error in Go

                I’m not sure that’s the case. They seems pretty common to me and I’ve certainly encountered them in Go projects I’ve used.

      3. 2

        OCaml compilation and test cycles are not appreciably different from Go’s during development. I’ve used both. OCaml gives me much better type safety. In Go what I’m always wary of is the default emptiness of values which aren’t initialized for whatever reason. E.g. if I have a struct type and add a new field, the compiler doesn’t help me refactor my code in all places which work with this struct to ensure that the new field is handled. In OCaml it does.

      4. 1

        Rust and Go fits different purposes.

        I think the author agrees here : Rust and garbage collected Rust fit different purposes.

        That’s very subjective so probably not exactly what the author has in mind, but what I imagine a garbage collected Rust could be is a language whose design would de-emphasize performance a bit in favor of ease of use. So no lifetimes, no multiple variants of string, but the type safety story. And I imagine the performance would be in the same ballpark as Go, hence the mention.

    7. 2

      If we’re complaining about syntax (been reading and trying some OCaml recently) my small but biggest issue are the a) lack of Unicode support (ocaml-m17n wanted to solve this to for identifiers but not operators) and b) the language support tabs, but none of the formatters support it and most code in the wild uses 2 spaces which I find very difficult to read. A lot of other things seem like a practical trade off and I disagree with the author on most things because I prefer the ML-family syntax & conventions.

    8. 1

      Few other languages have anything like this. [the module system]

      I see this often, and want to point out that OBJ-family languages have similar module systems, and in fact directly influenced the Ada and ML module systems. Maude is an OBJ descendant which is still being actively developed. 1

      1. 1

        Maybe we should qualify this explicitly, but I thought it was pretty obvious that we are talking about languages where you have a reasonable chance of finding an ecosystem and tooling (package manager, language server, editor support etc.).

        1. 1

          Given the extreme scarcity of ML-style module systems, despite their importance and elegance, I personally think it’s relevant to at least know that it has cousins/ancestors. That’s not to say Maude could in any way be considered a plausible replacement to consider for (for example) an OCaml project. It’s a language with completely different goals.