If you're new to OCaml, you may find the ecosystem confusing. There are a few reasons for this:
- OCaml was born in academia and has historically focused on type theory and functional programming
- The community has a reputation for being small
- It was created a long time ago and carries some old-fashioned methods from the past
- It's a functional programming language, which may be a different paradigm from the one you're used to
- It's well suited to creating programming languages but has expanded into general-purpose programming
Why should you listen to me?
I'm not a teacher. I don't have 10+ years of experience in OCaml or a PhD, and, more terribly, I didn't finish my Computer Science degree.
But it's often useful to learn from someone who is one step above you rather than someone who's 10 steps above you.
I've worked professionally in OCaml and ReasonML (a dialect of OCaml) for almost 3 years and counting. Meanwhile, I've created and maintained some useful projects:
- styled-ppx: Typed styled components for ReScript
- query-json: Faster, simpler and more portable implementation of
jqin Reason - server-reason-react: Server rendering Reason React components natively
- jsoo-react: js_of_ocaml bindings for ReactJS. Based on ReasonReact
- jsoo-css: CSS Typed functional interface in jsoo, bindings to inline styles and emotion
- reason: Simple, fast & type safe code that leverages the JavaScript & OCaml ecosystems
How to look at the OCaml ecosystem
The OCaml ecosystem seems overwhelming because it's always explained in abstract terms, with minimal explanation and probably in the wrong order.
- Basics of the language
- Standard lib replacements
- opam: package manager
- dune: build system
- Configure editor
- deriving/ppx
- Compilation modes
A few topics are often described as "more advanced." The topics below are interesting, but they're difficult to understand, far less popular than the ones above and aren't required for most apps. They can still become immensely helpful when a specific problem appears.
Learning the basics of the language
Like the first-hour guide in the OCaml docs, this is a quick overview of the core language to get you up and running.
Types
OCaml is a statically typed functional programming language with type inference, so you don't need to specify every variable, argument, or return type.
let fn x = x + 1
(* fn: int -> int *)Note: fn is a function that takes an int and returns an int. Any other type will make the compiler angry. The compiler infers the type as int because the + operator works only with ints. To sum floats, use the +. operator.
The most important built-in types are records, tuples and variants.
recordsdefine a data structure with a fixed set of fields. They hold values together and define a domain model.
type person = { first_name : string; surname : string; age : int; }tupleshave a fixed set of values without names. They're often used to associate values without creating a more formal data structure such as a record.
let t = (1, "one", '1') in
(* int * string * char *)variantsdefine a sum type, also known as ADTs: Algebraic Data Types. They can represent a value that may be one of a few different kinds.
type color =
| Red
| Green
| Blue
| Yellow
| RGB of int * int * intPattern match
Pattern matching is OCaml's greatest feature. It lets you match a value against a pattern and execute code for each branch. It works amazingly well with variants and is also great with records, tuples and lists.
match color with
| Red -> "red"
| Green -> "green"
| Blue -> "blue"
| RGB (r, g, b) -> "rgb(" ^ string_of_int r ^ "," ^ string_of_int g ^ "," ^ string_of_int b ^ ")"The compiler enforces that your pattern match defines every possible case. If I forget a case, such as | Yellow, the compiler will say:
Warning number 8
You forgot to handle a possible case here, for example:
**Yellow**Pattern matching will become one of your most (ab)used OCaml features. You can combine it with almost any other feature of the language, and it can become a very advanced topic. For a more extensive example, see Mathematical expressions.
Balance of styles
OCaml has the right balance between functional and imperative styles. It lets you write both, though it's often preferable to stick to functional code and occasionally opt out into imperative code.
By default, all values are immutable. List.map returns a new list instead of mutating the original one.
let data = [1, 2, 3] in
let data_plus_one = List.map (fun x => x + 1) data in
(* `data_plus_one` is a new list and `data` is still available in scope *)But you can mutate values with ref and the mutable keyword inside records. There's an extensive explanation here.
Along the same lines, you can debug values by printing to stdout with print_endline or Printf. The Printf module has many useful functions for formatting output. Its printing notation is similar to C: %s for strings, %d for integers, %f for floats, etc.
print_endline "Hello world";
(* Hello world *)Modules
Modules are how you organize code in OCaml. Each file is an implicit module that uses the file's name. You can create modules inside it with module Whatever = { ... }.
Note: All modules need to start with an uppercase letter.
You'll often find that modules have a type t representing the module's "main" type. For example, the List module has a type t that is 'a list, a list of any type.
Modules can import other modules with include, which makes every function and type from the included module available in the current module.
You can also open modules, removing the need to prefix every value, function and type with the module name.
Learning opam
opam is OCaml's package manager. Read this post to become familiar with the basics. It can create a "switch," a set of packages for each project attached to a compiler version, as well as download and install packages.
Learning dune
dune.readthedocs.io/en/stable/overview.html
Dune is the most common build system for OCaml projects. Its power comes from letting users define a build system declaratively while dune handles most of the low-level details of OCaml compilation and the creation of libraries and executables.
I recommend creating a dummy project by following Building a Hello World Program From Scratch. Try to understand the dune file and how the modules are organized.
Configure Editor
Merlin and ocaml-lsp-server (OCaml's Language Server Protocol) enhance editors such as Visual Studio Code, Vim and Emacs. They provide useful features such as "jump to definition," "type on hover," "refactor symbol," "autocomplete," "expand switch statement" and "create an interface file from an implementation."
Set up your preferred IDE by following this guide.
Learning about standard library replacements
In addition to the OCaml standard library, several popular third-party libraries are widely used in the OCaml community. Some provide alternative implementations of certain features, while others offer functionality that isn't available in the standard library.
- Base: janestreet/base
- Core: janestreet/core
- Containers: c-cube/ocaml-containers
- devkit: ahrefs/devkit
Even though most of these might be handy, I recommend starting with the standard library, exploring what's missing and reaching for one of these when the time comes. Knowing that they exist and appear in some online materials is enough to move forward.
Learning about ppx
ocaml.org/docs/metaprogramming
What are the [@deriving yojson], [@react.component], [%...], [@@ ...] and let%lwt annotations? They're called ppx, or preprocessing extensions, and they do a lot for you. They generate code by either extending the language or producing boilerplate.
This is often called meta-programming. It's a powerful tool that's also very easy to abuse. The official documentation has an extensive explanation of Preprocessors in OCaml.
Learning about Compilation modes: native, byte, js
One of OCaml's strengths is that it can be compiled to run on a wide variety of platforms: native code, bytecode and JavaScript.
OCaml itself comprises two compilers.
One generates bytecode, which a C program then interprets. This compiler runs quickly, generates compact code with moderate memory requirements and is portable to essentially any 32- or 64-bit Unix platform. The generated programs perform quite well for a bytecode implementation. You can use this compiler either as a standalone, batch-oriented compiler that produces standalone programs or as an interactive, toplevel-based system.
The other compiler generates high-performance native code for several processors. Compilation takes longer and generates larger code, but the programs deliver excellent performance while retaining the bytecode compiler's moderate memory requirements.
There are also two compilers that target JavaScript: js_of_ocaml and Melange.
js_of_ocaml tries to work closely with the OCaml library ecosystem, allowing any OCaml library to compile to JavaScript.
Melange integrates more closely with the JavaScript/npm ecosystems.
Learning Functors
dev.realworldocaml.org/functors.html
Functors are a way to parameterize modules over other modules. Modules from the Standard Library, such as Map and Set, use them.
module Map_with_strings = Map.Make(String)
(* ^^^^^^ `String` is a module
`Map.Make` can have access to all the interface from String and
any module with the same interfaced can be passed:
type t = 'a
val compare : t -> t -> int
*)
module Custom_map = Map.Make(struct type t = int let compare a b = a - b end)
(* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Here's the module without a name *)Here's a simple implementation of the Map functor:
module Map = struct
(* The module type this functor recieves *)
module type OrderedType = sig
type t
val compare : t -> t -> int
end
(* The interface from the module created *)
module type S = sig
type key
type 'a t
val empty : 'a t
val add : key -> 'a -> 'a t -> 'a t
val find : key -> 'a t -> 'a
end
(* The implementation of the functor itself *)
module Make (Ord : OrderedType) : S with type key = Ord.t = struct
type key = Ord.t
type 'a t = (key * 'a) list
let empty = []
let add key value map = (key, value) :: map
let rec find key map =
match map with
| [] -> raise Not_found
| (current_key, value) :: rest -> if Ord.compare key current_key = 0 then value else find k rest
end
endA specific example of a Functor
Learning about memory management
OCaml provides a garbage collector, so you don't need to allocate and free memory explicitly as you do in C/C++. The OCaml garbage collector is a modern hybrid generational/incremental collector that outperforms hand allocation in most cases.
Learning GADTs
ocaml.org/manual/gadts-tutorial.html
GADTs (Generalized Algebraic Datatypes) are a different kind of variant (ADT). They enable typechecker use cases that normal variants can't express, including polymorphism in the sense that a function can return different types depending on the input.
They're very sophisticated and aren't something you'll need to use often, but they're good to recognize in code such as the Printf implementation.
The best resource I've read for understanding the basics is blog.mads-hartmann.com/ocaml/2015/01/05/gadt-ocaml.html.
References
- Cornell University CS3110 Playlist and Book
- Real-world OCaml Book
- MOOC: Introduction to Functional Programming in OCaml
- What I wish I knew when learning OCaml
Based on https://github.com/petehunt/react-howto and https://github.com/petehunt/webpack-howto