{"slug": "two-years-of-ocaml", "title": "Two Years of OCaml", "summary": "After two years of using OCaml to rewrite the Austral compiler, the author shares a critical assessment of the language, highlighting its syntactic flaws and pragmatic shortcomings while acknowledging its strengths over alternatives like Haskell.", "body_md": "The other day I saw [this post on OCaml](https://osa1.net/posts/2023-04-24-ocaml-thoughts.html) discussed in [Hacker News](https://news.ycombinator.com/item?id=35699697)\nand [Lobsters](https://lobste.rs/s/jvxb8s/my_thoughts_on_ocaml).\n\nAlmost two years ago I rewrote the [Austral compiler](https://github.com/austral/austral) from [Standard\nML](https://en.wikipedia.org/wiki/Standard_ML) to [OCaml](https://ocaml.org/), so I thought I’d share my thoughts on OCaml after\nusing it in writing a complex software project, explaining what is good and what\nis bad and how it compares mainly to [Haskell](https://www.haskell.org/).\n\nIf this seems overwhelmingly negative, it’s because the things OCaml does right\nare really just uncontroversial. They’re *obviously* right and hardly worth\npointing out. It’s actually a weirdly optimistic thing: that a language with so\nmany glaring deficiencies stands far above everything else.\n\n# Contents\n\n[Syntax](#syntax)[Modules: Better is Worse](#modules)[Semantics](#semantics)[Pragmatics](#pragmatics)[At Least It’s Not Haskell](#haskell)[My OCaml Style](#my-style)[Should You Use OCaml?](#should)\n\n# Syntax\n\nYeah, yeah, *de gustibus*, and people spend [ way too much time whining about\nsyntax](https://en.wikipedia.org/wiki/Law_of_triviality) and other superficial issues, rather than focusing on language\nsemantics and pragmatics.\n\nBut I’m not a partisan about syntax. I genuinely think code written in C, Java, Lisp, Pascal, and ML can be beautiful in different ways. Some of these complaints will be personal, others will be more objective.\n\n## Aesthetics\n\n[ML](https://en.wikipedia.org/wiki/ML_(programming_language)) was born as the implementation language of a [theorem prover](https://en.wikipedia.org/wiki/Logic_for_Computable_Functions), so\nnaturally the syntax is meant to look like whiteboard math.\n\nAnd it does look good for math. If you’re writing something like a symbolic differentiation engine:\n\n```\ntype expr =\n  | Const of float\n  | Add of expr * expr\n  | Sub of expr * expr\n  | Mul of expr * expr\n  | Div of expr * expr\n\nlet rec diff (e: expr): expr =\n  match e with\n  (* c' = 0 *)\n  | Const _ ->\n     Const 0.0\n  (* (f + g)' = f' + g' *)\n  | Add (f, g) ->\n     Add (diff f, diff g)\n  (* (f - g)' = f' - g' *)\n  | Sub (f, g) ->\n     Sub (diff f, diff g)\n  (* (fg)' = f'g + fg' *)\n  | Mul (f, g) ->\n     Add (Mul (diff f, g), Mul (f, diff g))\n  (* (f/g)' = (f'g - g'f)/gg *)\n  | Div (f, g) ->\n     Div (Sub (Mul (diff f, g), Mul (f, diff g)), Mul (g, g))\n```\n\nThen it’s simply delightful. It does tend to fall apart for everything else however.\n\nOCaml, like Haskell, is [expression-oriented](https://en.wikipedia.org/wiki/Expression-oriented_programming_language), meaning that there is no\nseparation of statements (control flow, variable assignment) and expressions\n(evaluate to values) and instead everything is an expression. Most expressions\nin OCaml tend not to have terminating delimiters.\n\nThis is very vague, but ML-family (meaning Standard ML, OCaml, Haskell and\nderivatives) code often feels like the expressions are “hanging in the air”, so\nto speak. Terminating delimiters (like semicolons in C or `end`\n\nin\n[Wirth-family](https://wiki.c2.com/?WirthLanguages) languages) make the code feel more “solid” in a way.\n\nAnd expression orientation (which most modern languages advertise as a feature)\ncuts both ways. The benefit is simplicity and symmetry: you don’t need both an\n`if`\n\nstatement and a ternary if expression. You can have a big expression that\ncomputes a value and then assigns it to a containing `let`\n\n, like so:\n\n``` php\nlet a: ty =\n  match foo with\n  | Foo a ->\n    (* ... *)\n    let bar =\n      (* ... *)\n      (* imagine deeply nested expressions *)\nin\n(* etc *)\n```\n\nWithout having to use an uninitialized variable or refactor your code into too-small functions. However, this generality comes at a cost: you can write arbitrarily deep and complex expressions, where a statement-oriented language would force you to keep your code flatter and break it down into small functions.\n\nIt takes discipline to write good code in an expression-oriented language. I\noften see e.g. [Common Lisp](https://lisp-lang.org/) code with functions hundreds of lines\nlong. It’s almost impossible to track the flow of data in that context. This, by\nthe way, is why [Austral](https://austral-lang.org/) is statement-oriented, despite every modern\nlanguage moving towards expression-oriented syntax.\n\n## Declaration Order\n\nIn OCaml, like in C, declaration must appear in dependency order. That is, you can’t write this:\n\n``` js\nlet foo _ =\n  bar ()\n\nlet bar _ =\n  baz ()\n\nlet baz _ =\n  print_endline \"muh one-pass compilation\"\n```\n\nInstead you must write:\n\n``` js\nlet baz _ =\n  print_endline \"muh one-pass compilation\"\n\nlet bar _ =\n  baz ()\n\nlet foo _ =\n  bar ()\n```\n\nAlternatively, you can use `and`\n\nto chain your declarations:\n\n``` js\nlet rec foo _ =\n  bar ()\n\nand bar _ =\n  baz ()\n\nand baz _ =\n  print_endline \"muh one-pass compilation\"\n```\n\nAnd the same thing is true of types:\n\n```\ntype foo = Foo of bar\n\nand bar = Bar of baz\n\nand baz = Baz of unit\n```\n\n*But*, you can’t interleave an `and`\n\n-chain of functions with one of types. So\nyou have a choice:\n\n-\nYou can write all of your code backwards, with the utility functions and the leaf-nodes of the call graph up front, and the important code at the bottom.\n\n-\nOr, you can write a big\n\n`and`\n\n-chain of types at the start of the file, followed by a big`and`\n\n-chain of functions for the remainder of the file.\n\nOption one makes the code harder to read, and option two feels incredibly brittle.\n\nHaskell gets this right: declaration order is irrelevant. Austral also allows declarations to appear in any order, partly because of my frustration with this aspect of OCaml.\n\nNote that having a module interface doesn’t save you here, because interfaces\nand modules are compiled separately. So if you have a `Foo.mli`\n\nfile like this:\n\n``` php\nval foo : unit -> unit\nval bar : unit -> unit\nval baz : unit -> unit\n```\n\nThe corresponding `.ml`\n\nfile *still* has to have the declarations appear in\ndependency order.\n\n## Comments\n\nOCaml has no single-line comment syntax. Instead, you have block comment syntax, like so:\n\n```\n(* I'm a comment. *)\n```\n\nThe double parenthesis-asterisk pair is torture to write on my fingers. Again, Haskell does this right: single-line comments are a double hyphen. Quick and easy.\n\nUnlike C and other languages, comments can be nested, like in Common Lisp:\n\n```\n(* I'm a (* nested *) comment. *)\n```\n\nThis is useful for commenting-out large chunks of code.\n\n## Type Specifiers\n\nThe syntax for type specifiers and type annotation is a bit of a pain.\n\nFirst, there’s inconsistency: `a * b`\n\nis the type specifier for a tuple\n(asterisk as in [product](https://en.wikipedia.org/wiki/Product_type)), but the syntax for constructing a tuple is\n`(a, b)`\n\n:\n\n``` js\nlet derp: int * string = (0, \"\")\n```\n\nAgain, Haskell gets this right:\n\n```\nderp :: (Int, String)\nderp = (0, \"\")\n```\n\nSimilarly, the unit type is `unit`\n\nbut its value is `()`\n\n. And, again, Haskell\ngets this right: the unit type is the empty tuple, denoted `()`\n\n, and its sole\nvalue is `()`\n\n.\n\n## Generic Types\n\nGenerics are weird. Most modern languages are moving towards ```\nName[Arg, ...,\nArg]\n```\n\nas the syntax for a generic type specifier. So in Swift you’d write\n`List[Int]`\n\n, but in OCaml you write `int list`\n\n. The order is inverted, but I\nthink the argument is that you can read it like it’s English?\n\nHaskell is not much better: `List Int`\n\n. This obsession with terseness is a big\nproblem: please give me punctuation.\n\n## Type Annotations\n\nType annotations go in the same line as functions:\n\n``` js\nlet derp (a: foo) (b: bar option) (c: baz * quux): herp =\n  (* ... *)\n```\n\nWhich isn’t *bad*, but it’s a functional language, so you end up passing more\nstuff in. Haskell makes this a bit more comfortable:\n\n``` php\nderp :: Foo -> Maybe Bar -> (Baz, Quux) -> Herp\nderp a b c =\n  -- ...\n```\n\n## Semicolons Work Sometimes\n\nSemicolons let you sequence statements. They work inconsistently. This works:\n\n``` js\nlet foo _ =\n  print_endline \"Hello, world!\";\n  true\n```\n\nThis doesn’t:\n\n``` js\nlet foo _ =\n  if true then\n    print_endline \"Hello, world!\";\n    true\n  else\n    false\n```\n\nWhich makes it hard to insert debugging `print`\n\nstatements. You have to\ntransform the above into the more tiresome:\n\n``` js\nlet foo _ =\n  if true then\n    let _ = print_endline \"Hello, world!\" in\n    true\n  else\n    false\n```\n\nThere’s an easy way to solve this: add an `end if`\n\ndelimiter. Again: terseness\nbites.\n\n## Inconsistencies\n\nAs above: the syntax for tuple and unit types and values is inconsistent.\n\nThe syntax for a list literal is `[1; 2; 3]`\n\n. This is because the comma is an\ninfix operator, so if you typo this as `[1, 2, 3]`\n\nyou don’t get a syntax error,\nthat’s a singleton list with a tuple as its element type.\n\nTypes are defined with `type`\n\n, both in module interfaces and module bodies:\n\n```\nmodule type FOO = sig\n  type t\nend\n\nmodule Foo: FOO = struct\n  type t\nend\n```\n\nBut values are *defined* with `let`\n\nand *declared* with `val`\n\n:\n\n``` js\nmodule type FOO = sig\n  val a: int\nend\n\nmodule Foo: FOO = struct\n  let a: int = 10\nend\n```\n\nAnd, as you can see above, the syntax for modules is inconsistent. In Standard\nML module interfaces are called *signatures*, and module bodies are called\n*structures*. In OCaml, these are called *module types* and *modules*\nrespectively—but it’s like they forgot to fully update the syntax, so `sig`\n\ndefines a `module type`\n\nand `struct`\n\ndefines a `module`\n\n.\n\nIn Standard ML you’d write:\n\n```\nsignature FOO = sig\n  (* ... *)\nend\n\nstructure Foo: FOO = struct\n  (* ... *)\nend\n```\n\nWhich is at least consistent.\n\n## Nested Match Expressions\n\nAgain, because `match`\n\nstatements have no terminating delimiter, you can’t nest\nthem in the obvious way:\n\n``` php\nlet rec safe_eval (e: expr): float option =\n  match e with\n  | Const f -> Some f\n  | Add (a, b) ->\n    match safe_eval a, safe_eval b with\n     | Some a, Some b -> Some (a +. b)\n     | _, _ -> None\n```\n\nThis will yield a confusing type (not syntax!) error. Instead, you have to parenthesize:\n\n``` php\nlet rec safe_eval (e: expr): float option =\n  match e with\n  | Const f -> Some f\n  | Add (e1, e2) ->\n    (match safe_eval e1, safe_eval e2 with\n     | Some f1, Some f2 -> Some (f1 +. f2)\n     | _, _ -> None)\n  (* ... *)\n```\n\nSo everything gets *slightly* out of alignment, and when you have a few nested\n`match`\n\nstatements, the code starts to look like Lisp, with a trailing train of\nclose parentheses on the last line.\n\nYou can avoid this by refactoring each match into a separate function, but that has other costs.\n\n## Do Notation\n\nOCaml would benefit from having this. Putting IO aside, `do`\n\nnotation is\nfantastic for writing succint error handling in functional, exception-free code,\nand also for doing the “mutation-free mutation” pattern. A lot of the Austral compiler looks like this:\n\n``` js\nlet rec monomorphize_stmt (env: env) (stmt: tstmt): (mstmt * env) =\n  match stmt with\n  | TSkip _ ->\n     (MSkip, env)\n  | TLet (_, name, ty, value, body) ->\n     let (ty, env) = strip_and_mono env ty in\n     let (value, env) = monomorphize_expr env value in\n     let (body, env) = monomorphize_stmt env body in\n     (MLet (name, ty, value, body), env)\n  | TAssign (_, lvalue, value) ->\n     let (lvalue, env) = monomorphize_lvalue env lvalue in\n     let (value, env) = monomorphize_expr env value in\n     (MAssign (lvalue, value), env)\n  (* ... *)\n```\n\nWhich in `do`\n\nnotation could be written more succinctly.\n\n# Modules: Better is Worse\n\n**Fig 1.** Society if OCaml had type classes instead of modules.\n\nThe [module system](https://v2.ocaml.org/manual/moduleexamples.html) is the central feature that sets OCaml and Standard\nML apart. This is how OCaml does ad-hoc polymorphism with early binding.\n\nThe module system consists of:\n\n**Module types**, which define the interface of a module. A module type is a collection of types and functions.** Modules**, which conform to an interface and define its types and functions.** Functors**, which are functions from modules to modules. They take modules as arguments and combine them into new modules.\n\nFew other languages have anything like this. [Modula-2](https://en.wikipedia.org/wiki/Modula-2) and [Ada](https://en.wikipedia.org/wiki/Ada_(programming_language)) work\nkind of like this, but they are much lower-level languages than OCaml.\n\n## Modules Are Better\n\nModules are similar to [type classes](https://serokell.io/blog/haskell-typeclasses) in Haskell, but they are more general:\n\n- A module can have multiple types, not just one.\n- Multiple modules can implement the same interface, while in Haskell, a type can only implement a type class in one way.\n\n## Modules Are Worse\n\nThe drawback is you lose implicit instantiation. You have to manually\ninstantiate modules, and manually refer to them. You can’t write `show x`\n\n, you\nhave to write `FooShow.show x`\n\n. This adds a baseline level of line noise to all\ncode that uses modules.\n\nIt makes composing code harder. In Haskell, you can define a type class and say\nthat the type parameters can only accept types that implement other type\nclasses. This lets you naturally compose implementations: for example, you can\nmake it so that if a type `A`\n\nimplements the equality type class `Eq`\n\n, and a\ntype `B`\n\nalso implements `Eq`\n\n, then the tuple `(A, B)`\n\nimplements `Eq`\n\nin the\nobvious way.\n\nIn OCaml the only way to do this is with functors, which, again, have to be manually instantiated, and the resulting module referred to by name.\n\nAnd this is also anti-modular, since you can have multiple modules created by instantiating the same functor over the same structure, and this duplication may not be trivial to erase. In Haskell, the type class database is global and there is no duplication.\n\nSo modules are more general, more flexible, and more powerful. They are also vastly more inconvenient to use, and their added power is more than undone by how cumbersome they are to use. Type classes, on the other hand, give you 80% of the features, let you implement the remaining 20% without much trouble, and are easier to use and to compose.\n\nIt’s not even fair to say type classes are [worse is better](https://en.wikipedia.org/wiki/Worse_is_better): type\nclasses are better is better.\n\n## Equality\n\nYou’d think equality in OCaml would work like this:\n\n``` php\nmodule type EQUALITY = sig\n  type t\n  val eq : t -> t -> bool\nend\n\nmodule IntEquality: EQUALITY = struct\n  type t = int\n\n  let eq (a: int) (b: int): bool =\n    a = b\nend\n```\n\nRather, equality in OCaml is special-cased. You have a [magical function](https://v2.ocaml.org/api/Stdlib.html#VAL(=))\nwith signature `'a -> 'a -> bool`\n\nthat the compiler implements for every\ntype. Standard ML does the same. Compare this to Haskell, where equality is\nimplemented entirely in userspace via a type class.\n\nThis should be a sign that modules are not good enough. You should either have\nthe courage of your convictions—and make equality into a module type—or you\nshould implement some bridging solution like [modular implicits](https://arxiv.org/pdf/1512.01895.pdf) to\nmake modules have the convenience of type classes.\n\nModular implicits for OCaml were first proposed in 2015. There’s an [open pull\nrequest](https://github.com/ocaml/ocaml/pull/9187) from 2019 implementing a prototype. I don’t think this is going to\nbe merged any time soon.\n\n## Multiple Implementations Are Unnecessary\n\nIn Haskell, typically each type can implement each type class in one obvious way. It’s rare you need multiple distinct instances.\n\nWhen you do, you can just use `newtype`\n\nwrappers:\n\n```\nnewtype IntAsc = IntAsc Int\n  deriving (Eq, Show)\n\nnewtype IntDesc = IntDesc Int\n  deriving (Eq, Show)\n\ninstance Ord IntAsc where\n  compare (IntAsc a) (IntAsc b) = compare a b\n\ninstance Ord IntDesc where\n  compare (IntDesc a) (IntDesc b) = compare b a\n```\n\n# Semantics\n\nHaskellers don’t read this.\n\n## Currying is Bad\n\nCurrying is bad. Punctuation is good. Adjacency is not punctuation.\n\nIt’s “cute”, I guess, if you like terse math notation, but it comes at huge\ncosts. In a normal language where you write `f(x,y,z)`\n\n, if you forget an\nargument, or add another one, you get an error saying the arity doesn’t\nmatch. If you swap the order of two arguments of distinct types, you get a type\nerror.\n\nIn OCaml, if you make any of these mistakes, you don’t get an error to that\neffect. You get a type error *downstream* of your typo. Consider:\n\n``` js\nlet foo (a: int) (b: float) (c: string): unit =\n  let _ = (a, b, c) in ()\n```\n\nHere’s the error message for each kind of mistake:\n\n| Error | Code | Message |\n|---|---|---|\nMissing |\n`foo 0 1.0` |\nThis expression has type `string -> unit` but an expression was expected of type `unit` . |\nExtra |\n`foo 0 1.0 \"\" 1` |\nThis function has type `int -> float -> string -> unit` . It is applied to too many arguments; maybe you forgot a `;` . |\nSwap |\n`foo 1 \"\" 0.0` |\nThis expression has type `string` but an expression was expected of type `float` . |\n\nOnly in the case where you swap two arguments do you get a reasonable error message.\n\nGradually you learn, through trial an error, to pattern-match on the error\nmessages. When I see something like “this has type `a -> b`\n\n” I know I forgot an\nargument. When I see “applied to too many arguments” I know I added an extra\none.\n\nPartly this is a consequence of type inference, which is why I put this under semantics rather than syntax. Also because I’ve complained enough about syntax.\n\nYou can avoid currying with tuples, but it makes function type annotations harder to write. And it doesn’t play well with much of the standard library.\n\n## Type Inference is Bad\n\nIt’s bad because the type system doesn’t know which type constraints are correct and which are the result of errors the programmer made. So when you make a mistake, you’re still giving constraints to the inference engine. Erroneous type inferences propagate in every direction. Error messages appear hundreds of lines of code away from their origin.\n\nIn the worst cases, you end up adding type annotations, one `let`\n\nbinding at a\ntime, until the error messages start zeroing in on the problem. And so you’ve\nwrapped back around to writing down the types.\n\nIt’s bad because types are (part of the) documentation, and so you end up annotating the types of function arguments and return types anyways.\n\nIt’s 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.\n\nIt’s bad because there are many circumstances (reading a patch file, a GitHub diff, a book, a blog post) where you doin’t have access to a language server, so you have to mentally reconstruct the types.\n\nIt’s bad because for any non-trivial type system it’s undecidable. It is not robust to changes to the type system, and seemingly minor changes will tip you over to undecidability.\n\nFinally: type inference is not fundamental to functional programming, contrary\nto popular belief. You can just annotate types. If the type of an expression is\nhard to predict, that’s probably a signal that the type system is too complex,\nbut you can always force a type error (or use [typed holes](https://wiki.haskell.org/GHC/Typed_holes) in languages\nthat support them) to see the actual type.\n\n## Mutation\n\nOCaml is impure: you can mutate memory and perform IO anywhere. Unlike Java or Python, references are not implicit any time you have an object. Rather, you have a (garbage-collected) reference type that you can dereference and store things into.\n\nThis is good. In theory purely-functional languages have a higher performance ceiling, because computation can be scheduled in parallel. In practice this is rarely realized because the “sufficiently smart compiler” doesn’t exist.\n\nPeople have been saying, for some 20 years now, that single-core scaling has stalled and the von Neumann architecture has no future and we’d better port everything so parallel-by-default functional languages. What will actually happen is we’ll get better at designing semantics for fundamentally-imperative languages with controlled aliasing and side effects, more Rust than Haskell. The borrow checker might be hard, but I’ll choose that over some trans-dimensional monad optics stack that takes six GiB of RAM to print “Hello, world!”.\n\n# Pragmatics\n\nThe stuff outside the spec: the tooling, community etc.\n\n## PPX\n\nOCaml doesn’t have macros built into the language. Instead you use [PPX](https://ocaml.org/docs/metaprogramming),\nwhich lets you write programs that manipulate OCaml source code at the AST\nlevel.\n\nMostly this is used for the equivalent of Haskell’s `derive`\n\n. So you can write:\n\n```\ntype expr =\n  | Const of float\n  | Add of expr * expr\n  | Sub of expr * expr\n  | Mul of expr * expr\n  | Div of expr * expr\n[@@deriving show, eq]\n```\n\nAnd the `@@deriving`\n\nannotation is replaced with the functions:\n\n``` php\nshow_expr : expr -> string\nequal_expr : expr -> expr -> bool\n```\n\nThere’s not much to say about this. It’s convenient. But it doesn’t play well\nwith [functors](https://stackoverflow.com/questions/70816473/how-to-apply-deriving-show-to-a-type-from-module-parameter-of-my-functor).\n\n## Tooling\n\nMy standard for language tooling:\n\n- I should be able to run the commands from the docs, in the order in which they appear in the docs, and things should work.\n-\nThe standard Swiss army knife tool should have a project skeleton generator that gives me a project with:\n\n- Library code.\n- A basic “Hello, world!” CLI entrypoint.\n- Unit tests.\n- Stub documentation.\n\nAnd all relevant commands (\n\n`build`\n\n,`test`\n\n,`generate-docs`\n\n) should work from the project skeleton immediately. entrypoint, and unit tests.\n\nSurprisingly few languages clear this short bar.\n\nWhat is OCaml tooling like? There’s [ dune](https://dune.build/), the build system, and\n\n[, the package manager.](https://opam.ocaml.org/)\n\n`opam`\n\nThey’re alright. I managed to get them working, somewhat, for Austral. But I haven’t touched the configuration since and every time I have to I sigh.\n\n[Merlin](https://ocaml.github.io/merlin/) works with Emacs except when it doesn’t. I recently switched to\nNixOS so I might be able to get things working more reliably.\n\nThis is infinitely better than the state of the art for e.g. Python (and the\ndifference is even more stark when you consider how much money and time has been\ninvested into the Python ecosystem compared to OCaml). The worst I experience\nwith OCaml tooling is, oh, `dune`\n\nis so tiresome to use, I don’t know how to do\nX, I’m lazy; whereas with Python any time you see an error message from `pip`\n\nyou might as well take a blowtorch to your laptop because that’s going to be\neasier to repair.\n\n## How Do I Profile?\n\nSeriously. I looked this up. All the [documentation](https://v2.ocaml.org/manual/profil.html) seems to assume\nyou’re running `ocamlc`\n\nmanually, like it’s the 1990’s and you’re building a\none-file script, not a big application with tens of transitive dependencies. I\nneed to know how to do this at the `dune`\n\nlevel.\n\nI managed to cobble together something using `prof`\n\nand successfully profiled\nthe Austral compiler, but then I forgot what I did to get that to work.\n\n## Testing\n\nDifferent tasks have different activation energy—the amount of effort to accomplish them. And in the end, the code that gets written is the code that is easy to write.\n\nThat’s why OCaml programs will have 10x more types than Java programs: because in OCaml you can define a type in three lines of code, while in Java defining the humblest class requires opening a new file and writing the entire declaration out in triplicate.\n\nLanguages, tooling, and the community best practices affect the shape of the activation energy landscape, and channel you into a particular way of writing code.\n\nIn particular: the experience of writing unit tests varies markedly by\nlanguage. In Python I tend to write more tests, because Python is very dynamic\nand test frameworks take advantage of that. I just add a class and write a few\nmethods with names starting with `test_`\n\nand I have my unit tests.\n\nTest autodiscovery is a huge boon. Test frameworks where you have to manually register tests make it more tiresome and time-consuming to write tests, and I end up writing fewer.\n\nMaybe there’s a way to do quick, succinct tests in OCaml with autodiscovery. If\nthere is, I haven’t found it, because setting up even the most basic unit tests\nwith `dune`\n\nwas already a pain.\n\nThe *existence* of tooling is worthless. The Right Way to do things should be\nincluded in the project skeleton generator, so it’s not just experts who know\nhow to do it.\n\n## Minor Complaints\n\n-\nreturns in an`compare`\n\n`int`\n\n: like in C,`compare a b`\n\nreturns 0 to indicate`a = b`\n\n, a negative integer to indicate`a < b`\n\n, and a positive integer to indicate`a > b`\n\n. This is so you can implement integer comparison by doing`b - a`\n\n.And needless to say this is an archaism. It’s too late to change, but, for the _n_th time, Haskell does this right: comparison returns a type\n\nwith constructors`Ordering`\n\n`LT`\n\n,`EQ`\n\n,`GT`\n\n. -\n`compare`\n\nis a special case: like equality, it’s special-cased into the language. The type is`compare: 'a -> 'a -> int`\n\nwhich doesn’t make sense.This should be implemented by a module analogous to Haskell’s\n\ntype class.`Ord`\n\n-\nThe zoo of conversion functions:\n\n`string_of_int`\n\n,`bool_of_string`\n\n, etc. Again, have the courage of your convictions and make this a module type.\n\n# At Least It’s Not Haskell\n\nHaskell is the main competitor to OCaml. The areas where Haskell is superior to OCaml are:\n\n- Consistent syntax.\n- Declarations can appear in any order.\n- Better import system.\n- Tooling might be better (low-confidence, haven’t used Haskell in anger much).\n- Type classes are better than modules.\n`do`\n\nnotation is great for error handling.\n\nWhere Haskell is worse:\n\n- Infix operators are bad. Custom infix operators are worse.\n- Haskell is very indentation-sensitive, more so than Python. Slight, harmless-looking cosmetic changes can break the parser.\n- Lazy evaluation is bad.\n- Lazy data structures are bad.\n- Is purity worth it? Not really.\n- Every file starts with declaring thirty language extensions.\n\n# My OCaml Style\n\nI have a very conservative OCaml style. Types, functions, `let`\n\n, `match`\n\n,\n`if`\n\n. That’s it. What else do you need?\n\nI never use polymorphic variants, or named arguments (just define a new record type lol). I factor things out and try to keep expression nesting to a minimum. I don’t like functions that span multiple pages, but sometimes that’s hard to avoid, especially when you have large sum types.\n\nMuch of the OCaml I see in the wild is a mess of un-annotated, deeply-nested, functorized code that’s been PPX’d to death.\n\n# Should You Use OCaml?\n\nWhile there’s a lot that makes me shake my head, there’s really nothing in OCaml that makes me scream in terror. The language:\n\n- Is statically typed.\n- Has a solid type system, by which I mean algebraic data types with exhaustiveness checking.\n- Is garbage collected.\n- Compiles to native.\n- Has good out of the box performance.\n- Is not too galaxy brained.\n- Lets you mutate and perform IO to your heart’s content.\n- Has a decent enough ecosystem.\n\nAnd surprisingly few languages check these boxes that don’t also have significant drawbacks.\n\nIf you want a statically and strongly typed garbage-collected language that compiles to native and doesn’t require you to change the way you work too much, you should use OCaml.\n\nParticularly if you what you want is “garbage collected Rust that’s not Go”, OCaml is a good choice.", "url": "https://wpnews.pro/news/two-years-of-ocaml", "canonical_source": "https://borretti.me/article/two-years-ocaml", "published_at": "2026-06-17 07:25:58+00:00", "updated_at": "2026-06-17 07:53:36.966081+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["OCaml", "Austral", "Standard ML", "Haskell", "Hacker News", "Lobsters"], "alternates": {"html": "https://wpnews.pro/news/two-years-of-ocaml", "markdown": "https://wpnews.pro/news/two-years-of-ocaml.md", "text": "https://wpnews.pro/news/two-years-of-ocaml.txt", "jsonld": "https://wpnews.pro/news/two-years-of-ocaml.jsonld"}}