OCaml Module 2 – Expressions, values and functions
Labelled and optional arguments
Name OCaml arguments with ~labels, give ?optional ones defaults, see when a default is used, and pass labelled functions to List and ListLabels.
What you will learn
- Define and call functions with ~labelled and ?optional arguments, including defaults
- Explain when an optional argument is erased and why a final unit parameter is often needed
- Use label punning and pass labelled arguments in any order
- Use labelled library variants such as ListLabels with higher-order functions
Before you start
What you’ll learn · Before you start
What you will learn
- Define and call functions with ~labelled and ?optional arguments, including defaults
- Explain when an optional argument is erased and why a final unit parameter is often needed
- Use label punning and pass labelled arguments in any order
- Use labelled library variants such as ListLabels with higher-order functions
Before you start
On this page
A function with two int parameters accepts them in the wrong order without a word: the swapped call still
type-checks, and only the result is wrong. OCaml lets you name them instead, and then div ~num:10 ~den:2 and
div ~den:2 ~num:10 both give 5, because the labels say which value is which.
Here you define and call functions with ~labelled and ?optional arguments, learn exactly when a default is used and
why so many functions end in (), pass arguments by name in any order, and use labelled functions with List and
ListLabels.
A label names an argument, so a call may give it in any order
A labelled argument: An argument passed by name, written ~name:value at the call. Labelled arguments of a complete call may come in any order. is written with a tilde in the definition, let div ~num ~den = num / den, and its name becomes part of the type: num:int -> den:int -> int. At the call you write the label
again, ~num:10, and the label, not the position, decides which parameter receives the value. The session below calls
div in both orders, supplies one label early, and passes a variable whose name matches its label:
from session import toplevel
# Labelled arguments: each argument has a name at the call, so a complete call may give them in any order, a variable
# with the label's name can be passed as just ~name (punning), and a label can be supplied first for partial
# application.
toplevel("""
let div ~num ~den = num / den;;
div ~num:10 ~den:2;;
div ~den:2 ~num:10;;
let halve = div ~den:2;;
halve ~num:9;;
let num = 42;;
div ~num ~den:6;;
div 10 2;;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# let div ~num ~den = num / den;;
val div : num:int -> den:int -> int = <fun>
# div ~num:10 ~den:2;;
- : int = 5
# div ~den:2 ~num:10;;
- : int = 5
# let halve = div ~den:2;;
val halve : num:int -> int = <fun>
# halve ~num:9;;
- : int = 4
# let num = 42;;
val num : int = 42
# div ~num ~den:6;;
- : int = 7
# div 10 2;;
Line 1, characters 0-3:
1 | div 10 2;;
^^^
Warning 6 [labels-omitted]: labels num, den were omitted in the application
of this function.
- : int = 5
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 labels_session.py
Both orders gave 5. div ~den:2 supplied only the denominator and returned halve, of type num:int -> int: partial
application works through labels too, in any order, which positional arguments cannot do. halve ~num:9 gave 4,
integer division as always. When a variable already has the label’s name, you can write just the label:
let num = 42 and then div ~num ~den:6 passed 42 as num and gave 7. This shorthand is called label punning, and the
manual uses it in its first example of labels.
The four ways a labelled argument reached div in the session:
| Call | What it means | Result |
|---|---|---|
div ~num:10 ~den:2 |
each value with its label | 5 |
div ~den:2 ~num:10 |
the same, in the other order | 5 |
div ~den:2 |
one label now, the other later | num:int -> int |
div ~num ~den:6 |
~num takes the variable num, 42 |
7 |
The last phrase, div 10 2, left the labels out. A call that supplies every argument may do that, and it gave 5, but
OCaml warned with warning 6, labels-omitted: without labels the order is back to the definition’s, and the protection
the labels gave is gone.
Label the arguments a caller could swap
Give labels to arguments of the same type whose roles differ, such as ~num and ~den, ~src and ~dst, or
~pos and ~len. Leave the one main argument of a function unlabelled, so the function still fits List.map and
|>.
An optional argument has a default that a call may leave out
An optional argument: An argument written ?name that a call may leave out; the function then uses its default, or sees None when it has none. is written with a question mark, ?(punct = "!"), which gives
it a default, and its type shows ?punct:string. A call may give it with a tilde, ~punct:"?", or leave it out and get
the default. Inside the function an optional argument without a default is an option, None when it was left out:
from session import toplevel
# An optional argument ?punct with a default. The final () tells OCaml the call is complete, so the default can be
# used. ~punct passes a value, ?punct passes an option as it is, and a function can forward an option it received.
toplevel("""
let greet ~name ?(punct = "!") () = "Hello, " ^ name ^ punct;;
greet ~name:"Asha" ();;
greet ~punct:"?" ~name:"Meera" ();;
greet ~name:"Asha" ?punct:(Some "...") ();;
greet ~name:"Asha" ?punct:None ();;
let shout ?punct who = greet ~name:who ?punct ();;
shout "Kiran";;
shout ~punct:"!!!" "Kiran";;
let show_punct ?punct () = punct;;
show_punct ();;
show_punct ~punct:"." ();;
""") Output
$ ocaml OCaml version 5.5.1 Enter #help;; for help. # let greet ~name ?(punct = "!") () = "Hello, " ^ name ^ punct;; val greet : name:string -> ?punct:string -> unit -> string = <fun> # greet ~name:"Asha" ();; - : string = "Hello, Asha!" # greet ~punct:"?" ~name:"Meera" ();; - : string = "Hello, Meera?" # greet ~name:"Asha" ?punct:(Some "...") ();; - : string = "Hello, Asha..." # greet ~name:"Asha" ?punct:None ();; - : string = "Hello, Asha!" # let shout ?punct who = greet ~name:who ?punct ();; val shout : ?punct:string -> string -> string = <fun> # shout "Kiran";; - : string = "Hello, Kiran!" # shout ~punct:"!!!" "Kiran";; - : string = "Hello, Kiran!!!" # let show_punct ?punct () = punct;; val show_punct : ?punct:'a -> unit -> 'a option = <fun> # show_punct ();; - : 'a option = None # show_punct ~punct:"." ();; - : string option = Some "."
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 optional_session.py
greet ~name:"Asha" () used the default and gave "Hello, Asha!", and greet ~punct:"?" ~name:"Meera" () gave
"Hello, Meera?", with the labels in either order again. Two calls passed the option itself instead of a value, with a
question mark at the call: ?punct:(Some "...") gave "Hello, Asha...", and ?punct:None gave the default, exactly as
leaving the argument out does. That form exists for one job, passing on an optional argument that another function
received. shout takes ?punct and hands it to greet as ?punct, so shout "Kiran" used greet’s default and
shout ~punct:"!!!" "Kiran" used the three marks. show_punct, which has no default, showed the option as the function
body sees it: None without the argument and Some "." with it.
The ways to pass an optional argument, side by side:
| At the call | The function receives | When to use it |
|---|---|---|
| nothing | the default, or None |
the usual case |
~punct:"?" |
the value "?" |
to choose a value |
?punct:(Some "...") |
the option Some "..." |
an option you already hold |
?punct |
whatever option the caller had | forwarding, as shout did |
Key idea
~label passes a value and ?label passes an option, so a function that receives an optional argument can hand it on unchanged with ?label, and the callee’s default still applies.
Checkpoint
Check: Labelled arguments and punning · Optional arguments and defaults
An optional argument is erased only when a later positional argument arrives
OCaml cannot know that a call is finished just because you stopped typing arguments. The manual states the rule: an optional argument counts as left out once an argument without a label, which comes after it in the function’s type, is applied. Then the optional argument is erasure: Filling a left-out optional argument with its default, which happens once an unlabelled argument after it in the type is applied. and its default is used. A function whose optional argument has nothing positional after it can never reach that point, and the session shows what happens:
from session import toplevel
# An optional argument is erased, and its default used, once a positional argument after it is applied. With no
# positional argument after it, it can never be erased, OCaml warns, and the call returns a function instead of a
# string. A final unit parameter fixes it.
toplevel("""
let tag ?(level = "info") msg = "[" ^ level ^ "] " ^ msg;;
tag "started";;
tag ~level:"warn" "disk 91% full";;
let note ~msg ?(level = "info") = "[" ^ level ^ "] " ^ msg;;
note ~msg:"started";;
let note ~msg ?(level = "info") () = "[" ^ level ^ "] " ^ msg;;
note ~msg:"started" ();;
note ~msg:"low space" ~level:"warn" ();;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# let tag ?(level = "info") msg = "[" ^ level ^ "] " ^ msg;;
val tag : ?level:string -> string -> string = <fun>
# tag "started";;
- : string = "[info] started"
# tag ~level:"warn" "disk 91% full";;
- : string = "[warn] disk 91% full"
# let note ~msg ?(level = "info") = "[" ^ level ^ "] " ^ msg;;
Line 1, characters 16-21:
1 | let note ~msg ?(level = "info") = "[" ^ level ^ "] " ^ msg;;
^^^^^
Warning 16 [unerasable-optional-argument]: this optional argument cannot be erased.
val note : msg:string -> ?level:string -> string = <fun>
# note ~msg:"started";;
- : ?level:string -> string = <fun>
# let note ~msg ?(level = "info") () = "[" ^ level ^ "] " ^ msg;;
val note : msg:string -> ?level:string -> unit -> string = <fun>
# note ~msg:"started" ();;
- : string = "[info] started"
# note ~msg:"low space" ~level:"warn" ();;
- : string = "[warn] low space"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 erasure_session.py
tag has type ?level:string -> string -> string, so tag "started" applied the positional msg after ?level, the
default “info” was used, and the result was "[info] started". note ~msg ?(level = "info") has only a labelled
argument before ?level and nothing after it. OCaml warned at once, “Warning 16 [unerasable-optional-argument]: this
optional argument cannot be erased”, and note ~msg:"started" returned a function, ?level:string -> string, rather
than a string. Step through both calls:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- An optional, then a positional. tag has type ?level:string -> string -> string: the optional ?level first, then msg, which has no label.
- The positional is applied. tag "started" gives no ~level. The string fills msg, a positional argument that comes after ?level, so ?level is erased and its default, "info", is used.
- Nothing after the optional. note ~msg ?(level = "info") = … has no positional argument after ?level. OCaml warns, warning 16: this optional argument cannot be erased.
- Still a function. So note ~msg:"started" never uses the default. It returns a function of type ?level:string -> string that waits for a level.
- A final unit parameter. Adding () as the last parameter gives ?level a positional argument to come before. note ~msg:"started" () then gives "[info] started".
The fix is a last parameter of type unit. With note ~msg ?(level = "info") () = …, the () at the call is the
positional argument after ?level: note ~msg:"started" () gave "[info] started", and
note ~msg:"low space" ~level:"warn" () gave "[warn] low space". That is why so many OCaml functions with optional
arguments end in unit, and why greet took a final ().
Treating warning 16 as harmless
Wrong: ignoring the warning because the definition compiled. Every call of such a function returns a function
waiting for the optional argument, so the result is never the string or number you expected. Right: add a final
() parameter, or move a positional argument after the optional one, as tag has.
Labels meet higher-order functions through ListLabels
The standard List module has no labels: List.map takes the function first and the list second, by position. Its
twin ListLabels has the same functions with labels, so ListLabels.map names its function ~f. Passing a function
that has labels of its own to an unlabelled higher-order function needs care, and the session shows the three cases:
from session import toplevel
# The standard List functions have no labels; ListLabels has the same functions with labels. A function with a
# labelled argument must get that label before it is passed on, while one whose only labelled argument is optional is
# passed with every optional argument left out.
toplevel("""
List.map ~f:String.length ["ab"; "c"];;
ListLabels.map ~f:String.length ["ab"; "c"];;
ListLabels.map ["ab"; "c"] ~f:String.length;;
let scale ~by x = x * by;;
List.map (scale ~by:10) [1; 2; 3];;
List.map scale [1; 2; 3];;
let tag ?(level = "info") msg = "[" ^ level ^ "] " ^ msg;;
List.map tag ["start"; "stop"];;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# List.map ~f:String.length ["ab"; "c"];;
Line 1, characters 12-25:
1 | List.map ~f:String.length ["ab"; "c"];;
^^^^^^^^^^^^^
Error: The function applied to this argument has type 'a list -> 'b list
This argument cannot be applied with label ~f
# ListLabels.map ~f:String.length ["ab"; "c"];;
- : int list = [2; 1]
# ListLabels.map ["ab"; "c"] ~f:String.length;;
- : int list = [2; 1]
# let scale ~by x = x * by;;
val scale : by:int -> int -> int = <fun>
# List.map (scale ~by:10) [1; 2; 3];;
- : int list = [10; 20; 30]
# List.map scale [1; 2; 3];;
Line 1, characters 9-14:
1 | List.map scale [1; 2; 3];;
^^^^^
Error: The value scale has type by:int -> int -> int
but an expression was expected of type 'a -> 'b
The first argument is labeled by,
but an unlabeled argument was expected
# let tag ?(level = "info") msg = "[" ^ level ^ "] " ^ msg;;
val tag : ?level:string -> string -> string = <fun>
# List.map tag ["start"; "stop"];;
- : string list = ["[info] start"; "[info] stop"]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 labels_hof_session.py
List.map ~f:String.length failed with “This argument cannot be applied with label ~f”, since List.map has no
parameter called f. ListLabels.map ~f:String.length ["ab"; "c"] gave [2; 1], and so did the same call with the
list first, because labelled arguments may come in any order. scale ~by x = x * by has a required label, so
List.map scale failed: “The first argument is labeled by, but an unlabeled argument was expected”. Applying the label
first made an ordinary function, and List.map (scale ~by:10) [1; 2; 3] gave [10; 20; 30]. A function whose only
labelled argument is optional is different: List.map tag ["start"; "stop"] worked and gave
["[info] start"; "[info] stop"], because OCaml passes such a function with every optional argument left out.
Function passed to List.map |
Works? | Why |
|---|---|---|
String.length |
yes | no labels |
scale, with ~by |
no | a required label must be applied first |
scale ~by:10 |
yes | the label is already applied |
tag, with ?level |
yes | optional arguments are left out |
Choose one module per file
Use ListLabels (or open ListLabels in a small scope) when labels help a call read clearly, and plain List
elsewhere. Mixing the two in one expression makes a reader check, at each call, which one takes labels.
Checkpoint
Check: When a default is used · Labels with List and ListLabels
Practise: arguments by name
The questions use the functions of the sessions: div, greet, shout, tag, note and scale. Decide each type
or result first, then check it against what the toplevel printed:
Practice that adapts to you
Practise this lesson
8 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): what do ~ and ? mean in front of an OCaml argument?
~x marks a labelled argument: the caller must supply it, writing ~x:value, and may do so in any order with other
labelled arguments. ?x marks an optional argument: the caller may leave it out, and the function then uses its
default, ?(x = 0), or sees None when there is no default. At a call, ~x:v passes a value, while ?x:opt passes
an option, which is how one function forwards an optional argument it received to another. An optional argument is
only filled with its default once a positional argument after it is applied, which is why such functions usually end
with a unit parameter.
Two harder questions follow, about a logging call that never logs and about a library change that adds labels. Decide what you would check first, then open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A helper is defined as let log ~msg ?(level = "info") = print_endline (level ^ ": " ^ msg). Calls such as log ~msg:"saved"; compile with warnings in the toplevel, but nothing is ever printed. What is wrong, and how do you fix the helper and its callers?
-
Senior to staff
Your library exports resize : int -> int -> image -> image, taking width and height, and users keep swapping the two numbers. You want labels, resize ~width ~height img, without breaking every caller on one day. How would you plan the change?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What do labels give a call of div ~num ~den?
Arguments named by their labels, so div ~num:10 ~den:2 and div ~den:2 ~num:10 both give 5. ~num alone passes a variable called num.
-
Which arguments are worth labelling?
Arguments of the same type whose roles differ, such as ~num and ~den or ~src and ~dst. Leave the one main argument unlabelled so the function still fits List.map and |>.
-
What does ?(punct = "!") declare in a parameter list?
An optional argument whose default is used when the call leaves it out. Without a default, the function receives None instead.
-
What is the difference between ~punct:v and ?punct:opt in a call?
~punct:v passes a value, ?punct:opt passes an option. The second is how a function forwards an optional argument it received, so the callee's default still applies.
-
When does an optional argument fall back to its default, and why do such functions end in ()?
Only when a positional argument after it is applied. With the optional argument last, every call returns a function (warning 16), so a final unit parameter is added.
-
How do List and ListLabels differ for labelled functions?
List functions take no labels and ListLabels functions do; a function with a required label must have it applied before it is passed to List.map.
Key takeaways
- A labelled argument,
~num, is supplied by name, so a complete call may give labelled arguments in any order, and~numalone passes a variable of the same name. - An optional argument,
?(punct = "!"), falls back to its default when left out;~punct:vpasses a value and?punct:optpasses an option, which is how a function forwards one. - An optional argument is erased only when a positional argument after it is applied, so a function whose optional
argument comes last needs a final
unitparameter, or every call returns a function. Listfunctions take no labels andListLabelsfunctions do; a function with a required label must have it applied before it is passed toList.map.
Terms in this lesson
- erasure
- Filling a left-out optional argument with its default, which happens once an unlabelled argument after it in the type is applied.
- labelled argument
- An argument passed by name, written ~name:value at the call. Labelled arguments of a complete call may come in any order.
- optional argument
- An argument written ?name that a call may leave out; the function then uses its default, or sees None when it has none.
References
- The OCaml Manual: Labeled arguments (Inria and the OCaml developers)
- Labelled and Optional Arguments (OCaml.org)
- Module ListLabels (Inria and the OCaml developers)
Report a problem with this lesson
Kept only in this browser. Your Learn progress