OCaml Cheat Sheet
The purpose of this page is to provide a brief "cheat sheet" of the basics of OCaml that you will use in this class.
The REPL
You can use the REPL (Read/Eval/Print Loop) to interact with OCaml, including with files that you have written.
Entering Commands
OCaml can be used interactively with the utop
REPL. Each full command you enter into utop
must be ended with ;;
.
Contrast this with:
utop# 2 + 2
3;;
Error: This expression has type int (highlighting the second 2)
This is not a function; it cannot be applied.
This gives an error because it is parsed the same as 2 + 2 3
.
Loading Files
You can load a file into utop
by using #use
:
If your file uses modules in other files, you will need to compile them first.
This will generate test.cmo
, which you then will load by calling utop test.cmo
:
We use ocamlc -c
to just compile the file (but not generate an executable).
Note that OCaml always capitalizes the first letter of your library file: our file was test.ml
, but when we use it in another file, we need to say open Test
.
Running OCaml
If you just want to run OCaml outside of the REPL, you can call utop file.ml
, which will interpret it, but not give you a REPL.
Just like above, if your file uses another file, you will need to compile it first and then pass the .cmo
file to utop
.
You can also use ocamlc
to generate an executable:
Integers, Floats, Booleans, and Strings
Integers
Other than integer constants (e.g., 32
), integers can be created using the usual arithmetic operators: +
, *
, /
, and mod
.
The orinary comparison operators also work:
Each infix operator (including the comparison operators) are actually OCaml functions:
Floats
Floating point numbers must be written with decimals (e.g., 2.0
). You can write 2.
as shorthand for 2.0
. Arithmetic operators
for floating point numbers are written with a .
at the end: +.
, *.
, and /.
. Comparison operators are the same as for integers:
Booleans
The boolean constants are true
and false
. The boolean operators are given by &&
, ||
, and not
:
Other than pattern matching, you can also use if/then/else
expressions for analyzing a bool
:
Strings and Chars
Strings can be concatenated with ^
:
A string is logically a sequence of char
s, each of which stands for a single byte.
Function Types
The type t1 -> t2
is the type of OCaml functions from t1
to t2
.
Top-level functions can be defined using let
.
let foo (x : int) = print_int x
let bar (x : string) (y : string) = print_string (x ^ y) (* ^ is string concatenation *)
The type for bar
above will be string -> string -> unit
: you can either think of this as a two-argument function, or a function from string
to functions of type string -> unit
:
utop# let bar (x : string) (y : string) = print_string (x ^ y);;
val bar : string -> string -> unit = <fun>
utop# bar "hi" " there\n";;
hi there
- : unit = ()
utop# bar "hi";;
- : string -> unit = <fun>
Inline (anonymous) functions are created using fun
:
Recursive functions need to be defined via let rec
.
Unit and Printing
The type unit
, which only has the value ()
, is used for the return type of expressions that produce side effects such as printing a string:
You can sequence programs by using e1 ; e2
: this will evaluate e1
, throw away its return value, and evaluate e2
:
utop# print_string "Hello"; print_string " world!\n";;
Hello world!
- : unit = ()
utop# let _ = print_string "foo"; print_string "bar\n";;
foobar
- : unit = ()
utop# let x = (print_string "hi\n"; 42);;
hi
val x : int = 42
Lists
Creating lists is done with the notation [1; 2; 3]
. You can append lists using @
:
You can add to the end of a list by using ::
:
Pairs
Pattern Matching
For lists:
For pairs, we can pattern match on pairs of more than two elements:
(See pattern matching on other types below.)
Defining functions via function
A convenient syntax exists for defining functions by pattern matching:
It is preferable to use the more explicit match ... with
syntax at first, though, until you are more expert with OCaml.
Type aliases, ADTs and Records
Type aliases
ADTs
Algebraic data types can be defined as follows:
We then construct bool_tree
values by using the constructors:
utop# Leaf false;;
- : bool_tree = Leaf false
utop# Branch (Leaf false, Leaf true);;
- : bool_tree = Branch (Leaf false, Leaf true)
We inspect values of ADTs via pattern matching:
let rec sum_tree (t : bool_tree) =
match t with
| Leaf b => if b then 1 else 0
| Branch (t1, t2) -> sum_tree t1 + sum_tree t2
Note that constructors of ADTs are not quite the same as functions in OCaml; i.e., Leaf
on its own is not a function of type bool -> bool_tree
.
utop# Leaf;;
Error: The constructor Leaf expects 1 argument(s), |~
but is applied here to 0 argument(s)
However, you can create functions to wrap these constructors:
utop# let leaf x = Leaf x;;
- : bool -> bool_tree = <fun>
utop# let branch x y = Branch (x, y);;
- : bool_tree -> bool_tree -> bool_tree = <fun>
Records
Records, similar to structs in other languages, let you package together data with convenient ways to access it:
type my_rec = { x : int; y : bool; z : bool }
let tst : my_rec = { x = 3; y = false; z = true }
let tst_x : int = tst.x
Given a record, you can create another one by replacing some of its fields:
State
The only state we will use in this class is ref
. You can create a reference by using ref
. If x
has type int ref
, then !x
(dereference) has type int
, and x := 3
(assignment) has type unit
.
utop# let x = ref 3;;
val x : int ref = {contents = 3} (* Don't worry about what contents means for now. *)
utop# x := 2;;
- : unit = ()
utop# !x;;
- : unit = 2
utop# x := 2; x := 1; !x;;
- : int = 1
Anything can be stored in a ref
, including functions and other ref
s.
utop# let x = ref (ref 2);;
val x : int ref ref = ...
utop# x := ref 3;;
- : unit = ()
utop# ! (! x);;
- : int = 3
utop# let y = ref (fun (x : int) -> x + 1);;
val y : (int -> int) ref = ...
utop# y := (fun (x : int) -> x * 2);;
- : unit = ()
utop# !y 4;;
- : int = 8
Polymorphism
Polymorphic Functions
If you write a function that is valid for multiple different types, a polymorphic type will be assigned to it.
Here, 'a/'b/'c
are type variables, which stand in for any type. When you apply arguments to f
, you constrain those type variables by equalities:
The other two type variables stay type variables (albeit weak ones, which we won't need to discuss), because f 32
doesn't constrain the type of the other two arguments. However, note that the return type of f 32
is int
, because the signature of f
states that the return type ('a
) is the same as the type of the first argument.
You can also write f
using type annotations (which I often do for documentation purposes):
Note here that we have used type variables to constrain the third argument to have the same type as the second one:
utop# g 32 "a" "b";;
- : string = "a"
utop# g 32 "a" false;;
Error: This expression (false) has type bool but an expression was expected of type string
Why did we expect a string
here? Because "a"
caused the constraint 'b = string
.
Polymorphic Types
Datatypes and type aliases can have type variables:
utop# type 'a t = 'a list;;
type 'a t = 'a list
utop# type 'a tree = | Leaf of 'a | Branch of 'a tree * 'a tree;;
type 'a tree = | Leaf of 'a | Branch of 'a tree * 'a tree
Indeed, you can think of list
as being defined like below:
utop# type 'a my_list = | Nil | Cons of 'a * 'a my_list;;
type 'a my_list = Nil | Cons of 'a * 'a my_list
You can have multiple type variables, but they need to be expressed as a tuple:
utop# type ('a, 'b) either = | Left of 'a | Right of 'b;;
type ('a, 'b) either = | Left of 'a | Right of 'b
In this way, either
is similar to a binary function on types. However, unlike normal functions, we cannot partially apply a single type to either
.
Modules and Module Types
Given a file foo.ml
, the collection of definitions in this file will be associated to a module Foo
.
If Foo
contains a value (say, let x : int = 42
), another module can access this value either via Foo.x
, or by opening Foo
:
Other files, like client.ml
above, can only use Foo
if we compile foo.ml
into foo.cmo
(by calling ocamlc foo.ml -c
), and then interpreting/compiling
client.ml
with foo.cmo
: utop foo.cmo foo.ml
.
Modules in OCaml can also be manipulated within files:
Like how you can do type s = t
, you can also give new names to old modules:
And, of course, you can embed modules in other modules:
module Bar = struct
module M = struct
let z = 4
end
let w = M.z
end
let a = Bar.w (* OK *)
let b = Bar.M.z (* Also OK *)
Module Types
The power of modules comes from module types, which hides information behind a module type.
module type S = sig
type t
val z : t
end
module M : S = struct
type t = int
let z = 4
end
let x : M.t = M.z (* OK *)
let z : M.t = 3 (* Not OK *)
let y = x + 2 (* Not OK *)
Above, z
and y
fail to type check because the world outside of M
knows that t
is a type, and that z
has type t
, but nothing else. The outside world does not know that t = int
. However, if we added this information to S
, it would work:
module type S = sig
type t = int
val z : t
end
module M : S = struct (* ... *) end
let y = x + 2 (* OK *)
Importantly, M
has to contain at least the definitions in S
, but more are OK too:
Functors
A functor in OCaml is a function from modules to modules. We specify functors via a signature that takes other modules, along with their module types, as parameters.
module type Eq = sig
type t
val eq : t -> t -> bool
end
module Foo (M : Eq) = struct
let f (x : M.t) (y : M.t) = not (M.eq x y)
end
Sub-modules
A powerful feature of modules, when combined with functors, is that smaller module types can serve as specifications of larger modules:
module type Eq = sig
type t
val eq : t -> t -> bool
end
module MkFoo (M : Eq) = struct
let f (x : M.t) (y : M.t) = not (M.eq x y)
end
module MyType = struct
type t = | TInt of int | TString of string
let eq (x : t) (y : t) =
match (x, y) with
| (TInt a, TInt b) -> a = b
| (TString a, TString b) -> a = b
| _ -> false
let print (x : t) : unit =
match x with
| TInt i -> print_int i
| TString i -> print_string i
(* many more ops.. *)
end
module Foo = MkFoo(MyType) (* OK, because MyType adheres to Eq *)