Here at Esper, we have chosen to use the OCaml language for the server-side backend that implements most of our application’s functionality. OCaml includes many features that are not available in the more mainstream programming languages listed above, and we believe this gives us a competitive advantage. But even among Silicon Valley startups, OCaml is an unusual choice. The purpose of this post is to explain the benefits of OCaml and compare it to other languages. We hope to convince readers, especially other developers, to consider adopting OCaml for their projects as well.
PROGRAMMING LANGUAGE FEATURES WE LIKE
Here are some of the most important features of the OCaml language that we consider useful, or even essential, for building high-quality, reliable software:
First-class functions and immutable values
It is not always clear what people mean when they say “functional programming”, so we avoid this term and state directly what we want. Functions should be full-fledged values that can be created within other functions, stored inside data structures, passed as arguments, and returned as results. A function should automatically capture the relevant parts of its environment at the time of creation into a closure.
Most recent programming languages support these features, but they may be little used in idiomatic programs. One reason is that a function’s behavior is more difficult to predict when the values in its environment can be modified after its definition. Such modification could cause the function to produce different results when called with the same arguments, contrary to the expected behavior of a function. Mutability can be a useful tool in some situations, but it is often unnecessary and potentially harmful.
As a member of the ML family of languages, OCaml has its theoretical origins in the lambda calculus, a model where functions are so fundamental that everything else, even natural numbers, is built from them. OCaml uses a more efficient implementation, but functions are still taken very seriously, and they’re the primary building block for structuring programs.
OCaml does support mutable values like arrays and hash tables, but immutability is the default. Most programs are constructed with primarily immutable values, and the OCaml compiler and garbage collector are well optimized for this use case. While programmers learning imperative languages must contend with mutable state as soon as they write a while loop, the introductory book “OCaml from the Very Beginning” does not even introduce mutability until chapter 13 of a total 16.
Strong static type checking
Here we are concerned with two important properties of type systems. First, a language that performs implicit conversions between incompatible types is said to be weakly typed, versus a strongly typed language where conversions are explicit. C/C++/Obj-C, PHP, and to some extent Java all perform implicit conversions. This behavior may save a few keypresses, but programs are typically read more often than they are written, so we think it is better to be explicit about our type conversions, rather than require all programmers to memorize complex rules for narrowing integer conversions or a special truth table for the == operator.
Second, there is the question of when types are checked. No matter what language you use, all values have types in the sense of operations that they support. If a piece of code makes no sense due to incorrect type (even after implicit conversions), like an integer addition between a floating-point number and a string, or a method call on an object with no such method, the program gets stuck and cannot proceed safely. In a language without type safety, like those in the C family, the implementation may attempt to “do it anyway”, without defining what that means, and often with disastrous consequences. We are interested only in type-safe languages.
Algebraic data types and pattern-matching
Some programmers are turned off by static type systems, probably because the most popular ones are cumbersome to use, not particularly expressive, or both. OCaml and other languages in the ML family provide a convenient way to define complex data types by combining simple structures using products (ANDs, like a struct) and sums (ORs, like a tagged union). For example, an HTML document can be defined as:
type tag = string type attributes = (string * string) list type document = | Element of tag * attributes * document list | Data of string
This is the real type used in the OCamlNet HTML parser, not a simplified version. OCaml’s pattern-matching makes it easy to manipulate document values, deconstructing them according to the type definition. For example, here is a function to extract the text inside every <div> with class “foo” or “bar”:
let rec extract_text inside_div = function | Element ("div", [("class", ("foo"|"bar"))], children) -> String.join "" (List.map (extract_text true) children) | Element (_, _, children) -> String.join "" (List.map (extract_text inside_div) children) | Data str -> if inside_div then str else ""
Pattern-matching makes these common test-and-extract operations simple, and it also provides additional compile-time checks, like warnings about missing or useless cases. Such checks help make code refactoring in OCaml quite pleasant. In contrast, the typical object-oriented implementation of this in Java would involve multiple subclasses of a common base class. The methods for each case of extract_text would be spread across different files and organized around dynamic dispatch. A separate visitor object might also be used, further obscuring the control flow of the program.
The function extract_text above does not include any type annotations for the variables it declares, including the parameter “inside_div” and the pattern variables “children” and “str”. No return type is declared for the function itself, either. Yet OCaml is able to infer the following type:
val extract_text : bool -> document -> string
Each call to extract_text takes a boolean (indicating whether this recursive call is inside a relevant <div>) along with the current node of the document to be processed, and it returns a string. As in a dynamically typed language, we didn’t have to write our types down just to satisfy the compiler, and yet type correctness is still enforced at compile-time. To do this, OCaml uses a modified version of the Hindley-Milner type inference algorithm, in which variable types are inferred from their usages in context, and any conflicts generate a type error.
MORE REASONS TO CHOOSE OCAML
None of the mainstream languages listed in the introduction provide more than two of the features described above. Some of them provide zero. OCaml has all of the above and many more advanced features, such as:
- Parametric polymorphism (like generics in Java)
- Polymorphic variant types, which require no type definitions and are still inferred automatically like magic
- A module system supporting separate compilation, functors (module-level functions), and general modular programming with abstract types
- Unique structural object system based on row types, with explicit subtyping
- Labeled and optional parameters
- Generalized algebraic data types (GADTs)
The OCaml implementation is mature; its codebase has been around for over 20 years and is maintained by the Gallium research team at INRIA. Its tools include both bytecode and native-code compilers, a replay-capable debugger, profiler, documentation generator, and lexer and parser generators.
OCaml’s community has seen some recent growth, with new books attracting more users. The OCaml Labs group at Cambridge is working on improvements to the core infrastructure while also developing a unikernel operating system in the language. Their efforts have produced lots of high-quality libraries, joining the many others already available in the OPAM package repository maintained by OCamlPro.