Intermediate

Functions in OCaml

Learn how to define and use functions in OCaml - currying, partial application, closures, higher-order functions, recursion, and labeled arguments with Docker-ready examples

In OCaml, functions are not just a way to organize code - they are the central building block of the entire language. As a functional language, OCaml treats functions as first-class values: you can store them in variables, pass them as arguments, return them from other functions, and build new functions by combining existing ones.

This changes how you think about functions. There is no return keyword and no statement/expression distinction - a function simply evaluates to the value of its body. Every function technically takes exactly one argument and returns one result, a design called currying that makes partial application natural and powerful.

In this tutorial you’ll learn how to define functions, write recursive functions, work with higher-order functions and closures, use the pipe operator to compose data transformations, and take advantage of OCaml’s labeled and optional arguments.

Defining Functions

Functions are defined with let. There is no return - the value of the last expression is the result. Recursive functions need the rec keyword, and mutually recursive functions are joined with and.

Create a file named functions.ml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
(* Functions in OCaml *)

(* A simple function with two parameters - no parentheses, no commas *)
let add x y = x + y

(* Type annotations are optional but allowed *)
let multiply (x : int) (y : int) : int = x * y

(* The last expression is the return value - no 'return' keyword *)
let square x = x * x

(* Local bindings with 'let ... in' create scope inside a function *)
let hypotenuse a b =
  let a2 = square a in
  let b2 = square b in
  sqrt (float_of_int (a2 + b2))

(* Recursion requires the 'rec' keyword *)
let rec factorial n =
  if n <= 1 then 1
  else n * factorial (n - 1)

(* Mutual recursion uses 'and' to join definitions *)
let rec is_even n =
  if n = 0 then true else is_odd (n - 1)
and is_odd n =
  if n = 0 then false else is_even (n - 1)

let () =
  Printf.printf "add 3 4 = %d\n" (add 3 4);
  Printf.printf "multiply 6 7 = %d\n" (multiply 6 7);
  Printf.printf "square 9 = %d\n" (square 9);
  Printf.printf "hypotenuse 3 4 = %.1f\n" (hypotenuse 3 4);
  Printf.printf "factorial 5 = %d\n" (factorial 5);
  Printf.printf "is_even 10 = %b\n" (is_even 10);
  Printf.printf "is_odd 7 = %b\n" (is_odd 7)

Note how arguments are separated by spaces, not commas, and the call site uses the same syntax: add 3 4, not add(3, 4). The let ... in form inside hypotenuse introduces names that are only visible within the function body - this is how local scope works in OCaml.

Higher-Order Functions, Closures, and Currying

Because functions are values, a function can take a function as an argument or return a brand new function. A function that captures variables from its surrounding scope is a closure. And since every OCaml function is curried, supplying only some arguments produces a new function - partial application.

Create a file named higher_order.ml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
(* Higher-order functions, closures, and partial application *)

(* Takes a function 'f' as an argument and applies it twice *)
let apply_twice f x = f (f x)

(* Returns a function - the returned 'fun' closes over 'n' (a closure) *)
let make_adder n = fun x -> x + n

(* Currying means this is equivalent: supply one arg, get a function back *)
let make_multiplier n x = n * x

let () =
  (* Pass an anonymous function (lambda) defined with 'fun' *)
  Printf.printf "apply_twice (+3) to 10 = %d\n" (apply_twice (fun x -> x + 3) 10);

  (* Partial application: supply some arguments now, the rest later *)
  let add5 = make_adder 5 in
  Printf.printf "add5 100 = %d\n" (add5 100);

  let double = make_multiplier 2 in
  Printf.printf "double 21 = %d\n" (double 21);

  (* Standard higher-order functions over lists *)
  let nums = [1; 2; 3; 4; 5] in
  let squared = List.map (fun x -> x * x) nums in
  let total = List.fold_left (+) 0 squared in
  Printf.printf "sum of squares = %d\n" total;

  (* The pipe operator |> threads a value through a chain of functions *)
  let result =
    nums
    |> List.filter (fun x -> x mod 2 = 1)
    |> List.map (fun x -> x * 10)
    |> List.fold_left (+) 0
  in
  Printf.printf "odd numbers x10 summed = %d\n" result

The pipe operator |> is idiomatic OCaml: x |> f is just f x, but chaining reads top-to-bottom like a pipeline. Here we filter the odd numbers, multiply each by ten, then sum them. Note also (+) - wrapping an operator in parentheses turns it into an ordinary function value you can pass to List.fold_left.

Labeled and Optional Arguments

OCaml lets you name arguments with ~label, so callers can pass them in any order, and declare optional arguments with ? plus a default value. This makes function signatures self-documenting and avoids the “which argument was which?” problem.

Create a file named labeled_args.ml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
(* Labeled and optional arguments *)

(* Labeled arguments use ~name and may be passed in any order *)
let divide ~numerator ~denominator =
  numerator / denominator

(* Optional arguments use ?(name = default) *)
let greet ?(greeting = "Hello") name =
  Printf.sprintf "%s, %s!" greeting name

let () =
  (* Labeled arguments - order does not matter *)
  Printf.printf "%d\n" (divide ~numerator:20 ~denominator:4);
  Printf.printf "%d\n" (divide ~denominator:5 ~numerator:100);

  (* Optional argument omitted - the default "Hello" is used *)
  print_endline (greet "World");

  (* Optional argument supplied explicitly *)
  print_endline (greet ~greeting:"Bonjour" "Monde")

Labeled arguments are especially valuable for functions like divide where swapping the two integers silently produces a wrong answer. The label makes the intent explicit at every call site.

Running with Docker

Run each example using the official OCaml image. The ocaml command interprets the file directly - no separate compile step needed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the official OCaml image
docker pull ocaml/opam:alpine

# Run the basic functions example
docker run --rm -v $(pwd):/home/opam/app -w /home/opam/app ocaml/opam:alpine ocaml functions.ml

# Run the higher-order functions example
docker run --rm -v $(pwd):/home/opam/app -w /home/opam/app ocaml/opam:alpine ocaml higher_order.ml

# Run the labeled arguments example
docker run --rm -v $(pwd):/home/opam/app -w /home/opam/app ocaml/opam:alpine ocaml labeled_args.ml

Expected Output

Running functions.ml:

add 3 4 = 7
multiply 6 7 = 42
square 9 = 81
hypotenuse 3 4 = 5.0
factorial 5 = 120
is_even 10 = true
is_odd 7 = true

Running higher_order.ml:

apply_twice (+3) to 10 = 16
add5 100 = 105
double 21 = 42
sum of squares = 55
odd numbers x10 summed = 90

Running labeled_args.ml:

5
20
Hello, World!
Bonjour, Monde!

Key Concepts

  • No return keyword - a function evaluates to the value of its last expression, since everything in OCaml is an expression.
  • Space-separated application - call functions with add 3 4, not add(3, 4); arguments are separated by spaces, not commas.
  • Recursion is explicit - use let rec for recursive functions and let rec ... and ... for mutually recursive ones.
  • Functions are first-class values - they can be stored, passed as arguments, and returned, enabling higher-order functions like List.map and List.fold_left.
  • Currying and partial application - every function takes one argument and returns a result; supplying fewer arguments yields a new specialized function.
  • Closures capture variables from their defining scope, as in make_adder returning a function that remembers n.
  • The pipe operator |> threads a value through a chain of transformations for readable, top-to-bottom data pipelines.
  • Labeled (~name) and optional (?(name = default)) arguments make call sites clear and let you pass arguments in any order.

Running Today

All examples can be run using Docker:

docker pull ocaml/opam:alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining