Introduction to OCaml
This post series started as a response to requests from some friends curious about OCaml. There are quite a few nice books already, but I realized that if I just recommend them any one of those books, it still will leave me with quite a few things to explain in depth, or force me to recommend another just to learn about that part. So I thought I may as well write something that hopefully will allow a person who already knows how to program in some other language get started with writing OCaml programs and continue learning on their own and find their own sources.
I decided to follow these principles:
Do not make it REPL-centric. Real life programs are not developed in the REPL. Include examples of complete programs. Do not introduce any concept before enough background is given to explain it without unjustified simplifications. Incomplete truth is better than a lie. Provide motivating examples and explain pathological cases. Do not mention foxes or chunky bacon.
Is OCaml hard to learn? Yes and no. It's easy to learn because it's not a “puzzle language” — its rules are generally hard and fast, and its syntax and semantics are predictable. However, it's also harder to learn than most popular languages because it uses ideas that are only starting to gain wide acceptance. It is not unlike the difference between the assembly languages and first structured programming languages.
Why would one want to use it? Here are some points:
- Very fast compiler. The reference implementation of OCaml can bootstrap itself within minutes.
- Static typing without a need to write any type annotations by hand.
- The type system is expressive enough to find some classes of logic errors, not just misplaced variables.
Its traditional domain is compiler writing and automated theorem proving systems. For example, Coq proof assistant, that was used to create the first C compiler where all optimizations are mathematically proven correct, is written in it. The Rust compiler was written in OCaml until it was able to compile itself.
A bit of history
OCaml itself is not new. It was first released as a distinct language in 1996, and even has one direct descendant — F#, that even includes syntax compatibility mode. However, the history of the ML language family is even longer and goes back to the 70's, and the theory that made those languages possible is even older yet, dating back to the 30's — it is called the lambda calculus.
Here is a brief and simplified story.
In the 30's, mathematicians took intense interest in the concept of computability. They could be called computer scientists even though general purpose computers didn't exist yet, and they were creating the foundations for their development. The main questions of the computability theory are what problems can be solved by algorithms, and how to reason about algorithms, for example, how and when can we find out if an algorithm always terminates. It required computation models, and multiple models were developed independently.
Alan Turing developed the well known abstract machine that is now called Turing machine — an infinite length tape and a head that can read and write symbols to the tape cells. Independently, Alonzo Church and Haskell Curry found that it is possible to model arbitrary computations using nothing but functions — that was the lambda calculus. Then it was discovered that the two models are equally powerful.
In the 40's, the lambda calculus was extended with a concept of types. Types were invented even before the lambda calculus, and long before computers, as an alternative to the set theory that would be free of its paradoxes (Bertrand Russel, who discovered a famous paradox, worked on type theory extensively). However, it took a while longer for the typed lambda calculi to take root in computers languages, and the first languages based on it were untyped.
The Turing machine is roughly the model behind imperative languages such as C and Fortran. The first language closely related to the lambda calculus was Lisp developed by John McCarthy in 1958. For a while, all functional languages were untyped (or dynamically typed).
However, in the 1970's, J. Roger Hindley and Robin Milner independently discovered a type system that allowed to unambiguously infer types of all expressions in a language without any type annotations. Then Milner et al. developed an algorithm for doing it efficiently, and created a programming language called ML (Meta Language) that was statically typed but made type annotations entirely optional, since it could infer all types and detect type errors on its own.
Initially, ML was an embedded language of a theorem proving system, but later took a life of its own, and people (I need to research who did it first) discovered a way to extend it with mutable references and exception handling in a type safe manner at cost of a small restriction. Now it was suitable for general purpose programming.
ML had multiple descendants, most of them research languages not intended for production use. Its type system was also incorporated into the family of programming languages that led to creation of Haskell, though whether Haskell belongs to the ML family or not is debatable.
The original ML went through a process of standardization in the 1990's and its entire specification was mathematically proven to be consistent by Robert Harper et al., which is a truly outstanding result. However, it’s specification was not extended since 1997, and it's rarely used now, even though it remains fairly popular for teaching and a few theorem proving systems still use it. One notable modern project that uses it is Ur/Web, a specialized programming language for web development.
Another ML descendant called CAML remained under active development, and eventually evolved into OCaml we know today. Along with F#, it remains the most common ML in production use now.
The implementation is licensed under GNU LGPL and is available from ocaml.org. On UNIX-like systems, however, the fastest and most convenient way to install it is to use OPAM, the OCaml package manager. Unlike most similar tools such as Python's pip or Haskell's cabal, OPAM allows installing the compiler itself, keeping multiple compiler versions on the same machine, and switching between them, in addition to installing OCaml libraries.
OPAM for Windows, however, is still under development, so I will not cover it yet.
When you have OPAM installed, use the
opam switch command to install the compiler. At the time of writing, the latest version is 4.07, so the command will be:
opam switch 4.07. When it completes, you may want to run this command to setup the environment varibles without restarting your shell:
eval $(opam config env).
Once you are set, you can verify the installation by executing the three main programs:
ocamlc (the bytecode compiler),
ocamlopt (the native code compiler), and
ocaml (the interactive interpreter). The first two should exit without any output, the third one starts the interactive top level where you can enter expressions and have them evaluated — more on that later.
Using the REPL
The interactive interpreter allows you to enter expressions and have them evaluated. One thing you should note is that it uses double semicolon as an end of input mark, so you should terminate all expression with ;;.
The default REPL is quite minimalist and doesn't even support command history. You can either alleviate the issue with rlwrap, or, better, install utop from OPAM (
opam install utop). It is an alternative REPL that supports history, completion, and more.
While the examples here tend to be compiler-centric, you should not neglect the REPL. First, it's the quickest way to find out the type of any function without even opening the documentation: just type something like print_endline ;; and you'll see the type. Second, any valid OCaml program can pasted into the REPL or its file can be loaded into it with
#use "somefile.ml";; directive. It is even possible to run OCaml programs in the same fashion as Python or Ruby scripts with
ocaml file.ml. It is also possible to load compiled libraries into the REPL, but we'll not discuss that part now.
Starting out: bindings, variables, and expressions
The structure of OCaml programs
An OCaml program, loosely speaking, is a sequence of expressions evaluated from top to bottom. There is no designated main function. There are also no statements in OCaml, everything is an expression, all expressions have values, and all values have types.
This is an absolutely useless but valid OCaml program:
You can save it to file, for example one.ml and compile it with
ocamlopt -o one ./one.ml. The executable it produces will exit immediately. What happens there? It's a program with a single expression, 1, which is a constant of type int. Constants evaluate to themselves, so this program evaluates 1, which produces no effects, and exits, since it has nothing else to do.
A constant is an example of a pure expression. It produces no side effects such as input/output, and it stays the same no matter how many times it's evaluated. An expression can also be impure, if they produce side effects, or evaluate to a different value if you evaluate it more than once (like a function for getting current system time, for example).
Constants and types
So, integer literal 1 is a constant of type int. Let's look at some other kinds of constants we can use:
4.0, 3.5, 1. — floating point literals have type float.
'c', '\n'— character literals have type char.
"foo", "bar\n"— string literals have type string
true, false— boolean literals have type boolThis list is not complete, but it's enough for the start.
Function application and Hello World
Now let's write a slightly less useless program, the traditional “hello world”. For this we'll need to use a function. The standard library function that prints a string with a newline at the end is printendline, there's also printstring function that doesn't add a line break.
The syntax for function application is very simple: function name followed by its arguments. You need no parentheses or any other special syntax.
This is the Hello World program:
print_endline "hello world" Now compile it and try it out:
$ ocamlopt -o hello ./hello.ml $ ./hello hello world
For the sake of experiment, you can try applying the print_endline function to a non-string constant and get your first type error:
$ cat hello.ml print_endline 1 $ ocamlopt -o hello ./hello.ml File "./test.ml", line 1, characters 14-15: Error: This expression has type int but an expression was expected of type string
This is type inference in action: the compiler inferred the type of 1 as int, checked the type of the
print_endline function, and found that it expects a string. How it knows that int is a wrong type to use with
print_endline though? By checking it against the type of that function.
The type of functions and the unit type
In OCaml, like in any functional language, functions themselves are values. If functions are values, they must also have types. By typing
print_endline;; in the REPL you can see that its type is
string -> unit. The arrow signifies that it's a function from a type named
string to another type named unit. When the compiler encountered the
print_endline 1 expression, it knew that the type of 1 is int rather than
string, and from the type of
print_endline it knew that its argument must be string, so it was able to detect the type error.
Now let's examine the “return type” of that function on the right hand side of the arrow. We are already familiar with string, but the unit is new. What is it and why it's needed?
As you remember, all expressions have types, so when
print_endline "hello world" is evaluated, the result of evaluation must have some type. A function in OCaml cannot “return nothing”. Since many functions are used just for their side effects and cannot produce any useful values, some type must have been invented just to have them comply with the “all values have types” rules.
unit type is a type that has only one value, and it was invented specially for this purpose. Its only possible value is a constant written (). Whether it was made to look this way to mimic calling functions without arguments in other languages is debatable, you should just remember that the constant () has type
The unit type is also used for functions that take no useful arguments, but have to take something because in OCaml a function cannot have no arguments either. The “arrow type” must always have both left and right hand sides.
An example of a function with
unit -> unit type is
print_newline that just prints a line break. A program that prints a line break thus can be written as:
Programs with multiple expressions and variables
So far we have only written programs that consist of a single expression. Let's see how to introduce variables and how to use multiple expressions — in OCaml these concepts are related.
To create a variable, you bind a value to a name. They are variables in mathematical sense, their value generally cannot change, even though the same name can be bound to a different value in a different scope. Variables are also called bindings.
Bindings are created with the let keyword. There are two ways to use let-bindings: one allows you to make a binding accessible only to one expression that follows it (
let <name> = <value> in <expr>), while the other (
let <name> = <value>) makes a binding accessible to all expressions below it. This is not a standard OCaml terminology, but for convenience let's call them local and global bindings respectively.
Let's rewrite the Hello World program with a local binding:
$ cat ./test.ml let hello = "hello world" in print_endline hello $ ocamlopt -o test ./test.ml $ ./test hello world
We could also use two bindings instead of one to demonstrate that
let ... in constructs can be nested:
let hello = "hello " in let world = "world" in print_endline (hello ^ world)
The ^ operator here is string concatenation. What happens here? Earlier I said that in the
let … in form, the binding will only be available to the expression that follows the in keyword, but remember that
let-bindings are themselves expressions, and they can be chained. Every let-binding opens a new scope. Here we first create a scope where the name hello is bound to a string constant "
hello ", then inside it we create a scope where the name world is bound to a string constant "
world", and in that scope, evaluate the
print_endline (hello ^ world) expression.
Now let's try global bindings. Before we can try them, we need to learn how to use multiple expressions in our programs. You might have already noticed that we have not used a semicolon or another statement terminator. Simply writing:
print_endline hello print_endline world
will not work because it will be parsed by the compiler as an attempt to apply the
print_endline function to three arguments, of which the first is a string, the second if a function, and the third is string again; and this will fail because the type of
print_endline is string -> unit. In the example above we avoided the issue by applying
print_endline to another expression in parentheses, but this isn't always feasible.
How do we make a program with multiple independent expressions parse correctly then? It's time to learn a secret of
let: its left hand side is not just a name, but a
pattern. Patterns have multiple uses and forms, which we will explore later. For now, you need to know that a name is a pattern. Another possible pattern is the
wildcard pattern written _, which comes in handy when you need to have an expression evaluated, but don't want to bind its value to any name.
To create independent expressions you can make “fake” bindings with wildcard patterns:
let hello = "hello " let world = "world" let _ = print_string hello let _ = print_endline world
A constant is also a valid pattern. As you remember, the type of
print_endline is string -> unit, so it always evaluates to the () constant. Thus you can also write:
let () = print_string hello let () = print_endline world In this case you need to watch that the constant pattern on the left hand side and the expression on the right hand side have the same type, but when you start using more complex expressions, this can also serve as a useful safeguard against accidentally using an expression of a non-unit type on the right hand side. While the wildcard pattern accepts values of any types in the let-binding context, a constant pattern, such as (), will force type checking. The choice whether to use this or not is up to the programmer.
Finally, if you have multiple expressions of the
unit type, you can chain them using semicolons. In OCaml, the semicolon is an expression separator rather than a statement terminator, so you will need at least one wildcard binding to use it though:
let helloworld = "hello world" let _ = print_string helloworld; print_newline ()
If you try this with expressions of types other than unit, the compiler will produce a warning. To suppress the warning, you can apply the
ignore function to your expression, as in:
let _ = ignore 1; print_endline "hello world"
Finally, you can also use ;; like in the REPL, but it's a very bad style and should be avoided whenever possible.
As you remember, every new binding opens a new scope. We can illustrate it like this:
Scope 0 *) let hello = "hello " (* Opens scope 1 *) (* Scope 1 *) let world = "world" (* Opens scope 2 *) (* Scope 2, (hello = "hello ", world = "world") *) let () = print_endline (hello ^ world)
Now let's stop and think what happens if we make two bindings with the same name.
(* Scope 0 *) let hello = "hello" (* Scope 1 *) let hello = "hi" (* Scope 2 *) let () = print_endline hello
If you compile this program and run it, you'll see that it prints hi. This is because the second binding redefined the value of
hello in the scope 2. This is called shadowing. It is distinct from variable assignment. The original value of hello did not change, it just becomes inaccessible from the new scope where it was redefined. Is the original value of hello lost forever? In the example above, yes, it will be completely inaccessible. In general case, the question is more interesting, but we will learn about it later when we get to functions and closures.
The case when difference from variable assignment is especially visible is the let ... in bindings. It is perfectly safe to redefine a binding locally and it will have no effect on the rest of the program.
Consider this program:
(* Scope 0 *) let hello = "hello " let hello = hello ^ "world" in (* Local scope 1 *) print_endline hello (* Back to scope 0 *) let () = print_endline hello
It will print hello world, and then print hello, because our let ... in binding only redefined the hello variable for the
print_endline hello expression.
$$$Arithmetics Now that we know how to use expressions and bindings and have basic idea of how function types work, we can look at the arithmetics.
As we've seen earlier, integer and floating point numbers are distinct types in OCaml. The character type is not a numeric type and cannot be used in arithmetic expressions.
An unusual feature of OCaml is that it also uses different sets of arithmetic functions for integers and floating point numbers. The reason for it is that otherwise the language would require either support for ad hoc polymorphism, which would ruin the decidable type inference without any type annotations; or magical overloading specially for arithmetics. The language designers sacrificed some convenience for consistency.
The integer operators look as usual: +, -, , /. The floating point operators have a dot at the end: +., -., , /..
let a = 4 + 2 (* good *) let b = 4.0 *. 3.5 (* good *) let c = (float 4) +. 2. (* good, integer is converted to float *) let d = 4.0 + 2.0 (* bad, using an integer addition with floats *) let e = 4 +. 2 (* bad using a floating point addition with integers *) let f = 4.0 + 2 (* bad, mixing floats with integers *)
Now let's write a program that takes temperature in Celsius from the standard input and converts it to Kelvin.
let celsius = read_float () let kelvin = celsius +. 273.15 let _ = print_float kelvin; print_newline ()
Let's verify that it works:
$ ocamlopt -o kelvin ./kelvin.ml $ ./kelvin 20 293.15
Write a program that takes an integer from the standard input and prints its square. Use
read_int function for reading and
print_int for writing.
Write a program that takes a floating point number representing temperature in Celsius from the standard input and converts it to Fahrenheit.
Originally published on blog.baturin.org
Sign up now and apply for roles at companies that interest you.
Engineers who find a new job through Functional Works average a 15% increase in salary.Get Started with