Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 5 minutes
  • Examples run with ocaml 5.5.1, opam 2.6.0, dune 3.24.2 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 5 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

Labelled arguments, in any order and by name Python · labels_session.py
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:

Defaults, explicit options and passing an option on Python · optional_session.py
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

  1. Question 1 of 2 let rect ~w ~h = w * h. Which call gives 12?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: rect ~h:3 ~w:4

    div ~den:2 ~num:10 gave 5 in the session for the same reason: the label, not the position, matches the parameter.

  2. Question 2 of 2 A wrapper receives ?timeout and must pass it on to fetch ?timeout url, keeping fetch's default when no timeout was given. How does it call fetch?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: fetch ?timeout url

    shout passed ?punct to greet the same way: shout "Kiran" used greet's default, and shout ~punct:"!!!" "Kiran" used the three marks.

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:

Erased, never erased, and the unit fix Python · erasure_session.py
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:

Interactive · step by step When an optional argument is erased, and when it is not Step through a call of tag and a call of note, and watch whether the default of ?level is used or still waits.
When an optional argument is erased, and when it is not, step 5 of 5. Showing tag "started" (?level, then msg), note ~msg:"started" (warning 16), tag gives a string ("[info] started"), note gives a function (?level:string -> string), note … () ("[info] started"). In focus: note … ().msg appliednothing after ?leveladd a unit parametertag "started"?level, then msgnote ~msg:"started"warning 16tag gives a string"[info] started"note gives a function?level:string -> stringnote … ()"[info] started"

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. An optional, then a positional. tag has type ?level:string -> string -> string: the optional ?level first, then msg, which has no label.
  2. 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.
  3. Nothing after the optional. note ~msg ?(level = "info") = … has no positional argument after ?level. OCaml warns, warning 16: this optional argument cannot be erased.
  4. 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.
  5. 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:

List, ListLabels and labelled functions Python · labels_hof_session.py
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

  1. Question 1 of 2 let log ~msg ?(level = "info") = level ^ ": " ^ msg. What does log ~msg:"ok" give?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A function of type ?level:string -> string, after warning 16

    note ~msg:"started" returned - : ?level:string -> string = <fun> in the session. A final () parameter fixes it.

  2. Question 2 of 2 let pad ~width s = … has type width:int -> string -> string. Which call pads every name in names to width 8?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: List.map (pad ~width:8) names

    List.map (scale ~by:10) [1; 2; 3] gave [10; 20; 30], while List.map scale failed with "The first argument is labeled by, but an unlabeled argument was expected".

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.

  1. Question 1 of 8 What type did the toplevel print for let div ~num ~den = num / den?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: num:int -> den:int -> int

    Labels appear in types as name:type, as in ListLabels.map's f:('a -> 'b) -> 'a list -> 'b list.

  2. Question 2 of 8 After let den = 4, what does div ~num:20 ~den give?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: 5

    div ~num ~den:6 gave 7 in the session with num bound to 42 the same way.

  3. Question 3 of 8 greet ~name ?(punct = "!") () = "Hello, " ^ name ^ punct. What does greet ~name:"Asha" () give?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: "Hello, Asha!"

    The session printed - : string = "Hello, Asha!".

  4. Question 4 of 8 Inside let f ?limit () = …, with no default, what is the type of limit?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: An option, None when the caller left it out

    The manual says optional arguments are implemented as options; a default only hides the option from the body.

  5. Question 5 of 8 When does OCaml use an optional argument's default?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: When an argument without a label, which comes after it in the type, is applied

    tag "started" used the default because msg comes after ?level; note ~msg:"started" did not.

  6. Question 6 of 8 A library function connect ~host ?(port = 443) has warning 16, and callers complain that connect ~host:"a" does nothing. What is the best fix?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: Add a final unit parameter, connect ~host ?(port = 443) (), and update callers to pass ()

    With the final (), note ~msg:"started" () gave "[info] started", and ~level could still be given.

  7. Question 7 of 8 What does List.map ~f:String.length ["ab"; "c"] give?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: An error: This argument cannot be applied with label ~f

    ListLabels.map ~f:String.length ["ab"; "c"] gave [2; 1], with the list first or last.

  8. Question 8 of 8 tag has type ?level:string -> string -> string. What does List.map tag ["start"; "stop"] give?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: ["[info] start"; "[info] stop"]

    The session printed ["[info] start"; "[info] stop"]. scale, whose ~by is required, failed in the same position.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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 |>.

  3. 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.

  4. 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.

  5. 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.

  6. 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 ~num alone passes a variable of the same name.
  • An optional argument, ?(punct = "!"), falls back to its default when left out; ~punct:v passes a value and ?punct:opt passes 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 unit parameter, or every call returns a function.
  • 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.

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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.