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 12 – Effects, concurrency and parallelism (OCaml 5)

Effect handlers

Declare OCaml 5 effects, perform them and answer them with match … with effect, then meet Effect.Unhandled, one-shot continuations and Effect.Deep.match_with.

  • Advanced
  • 15 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: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 3 interview questions.

What you will learn

  • Declare an effect by extending Effect.t, and perform it from code that does not know who answers
  • Handle an effect with the match … with effect syntax and resume the suspended code with continue
  • Predict where Effect.Unhandled is raised when no handler covers a performed effect
  • Explain why a continuation can be resumed only once, and end one with discontinue so its cleanup runs
  • Compare the OCaml 5.3 syntax with the Effect.Deep.match_with record API, and write a handler in each

Before you start

What you’ll learn · Before you start

What you will learn

  • Declare an effect by extending Effect.t, and perform it from code that does not know who answers
  • Handle an effect with the match … with effect syntax and resume the suspended code with continue
  • Predict where Effect.Unhandled is raised when no handler covers a performed effect
  • Explain why a continuation can be resumed only once, and end one with discontinue so its cleanup runs
  • Compare the OCaml 5.3 syntax with the Effect.Deep.match_with record API, and write a handler in each

Before you start

On this page

A report that draws a 32-column rule needs two numbers, a page width and a margin, yet only the function four calls down reads them. Passing them as parameters changes the three functions in between, and a global variable gives the whole program one width at a time. An effect: An operation declared by adding a constructor to OCaml's extensible type 'a Effect.t and started with Effect.perform: the code that performs it stops until a handler supplies an answer of type 'a. removes both costs: the deep function performs a question, OCaml suspends it, and an effect handler: Code that runs a computation and receives each effect it performs together with the continuation, then decides what to do: resume it with continue, end it with discontinue, or neither. OCaml 5.3 writes one as effect cases in a match. written around the call supplies the answer and lets the function carry on from where it stopped.

You will declare an effect, perform it and handle it with the match … with effect syntax of OCaml 5.3, then see what happens when nobody answers, when an answer is given twice and when the waiting code is never resumed, and write the same handler with the older Effect.Deep.match_with record. Each program was compiled and run with OCaml 5.5.1, and the page shows what it printed, errors included.

An effect lets deep code ask, and its caller answer

report.ml builds the header of a sales report in four layers: report calls header, header calls rule, and rule calls usable_width, the only function that needs the page settings. It asks for each one with perform (Ask "width") and perform (Ask "margin") and never says where the answers come from. The last function of the file, with_settings, runs a computation and answers every Ask from a table, and the program runs the same report under two tables:

One report under two settings handlers Python · inject_session.py
from session import run, write

# One report, four functions deep, needs two page settings. Only the innermost function asks for them, with the
# effect Ask; the handler around the call answers. The same report then runs under two handlers, with no parameter
# passed down and no global variable.
write("report.ml", """open Effect
open Effect.Deep

type _ Effect.t += Ask : string -> int Effect.t

(* The only function that needs the settings. *)
let usable_width () =
  let width = perform (Ask "width") in
  let margin = perform (Ask "margin") in
  width - 2 * margin

(* Three callers in between, none of them mentions the settings. *)
let rule () = String.make (usable_width ()) '-'

let header title =
  let line = rule () in
  Printf.sprintf "%s (%d columns)\\n%s" title (String.length line) line

let report () = print_endline (header "Monthly sales")

(* Runs f and answers every Ask it performs from a table. *)
let with_settings (settings : (string * int) list) f =
  match f () with
  | v -> v
  | effect (Ask name), k -> continue k (List.assoc name settings)

let () =
  with_settings [ ("width", 40); ("margin", 4) ] report;
  with_settings [ ("width", 24); ("margin", 2) ] report
""")
run("""
ocamlopt report.ml -o report
./report
""")

Output

$ ocamlopt report.ml -o report
$ ./report
Monthly sales (32 columns)
--------------------------------
Monthly sales (20 columns)
--------------------

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 inject_session.py

The first run answered 40 and 4 and drew a rule of 40 − 2 × 4 = 32 columns; the second answered 24 and 2 and drew 20. Neither rule, header nor report mentions a width, and no variable outside the handlers changed. That is the job an effect does: the code that needs a value asks for it, and the code that starts the work decides the answer, much as a parameter would, without passing through every function in between.

The mechanism will look familiar from exceptions. raise stops the current function and the nearest enclosing handler with a matching case takes over. perform stops the current function too, with one difference that changes what a handler can do. An exception handler: A try-with or match-with-exception clause that intercepts an exception and decides what to do with it instead of letting it propagate. More in “Exceptions and resource cleanup” can only take over from code that has been abandoned, while an effect handler also receives the rest of the stopped computation and can send it back to work with an answer. The manual calls effect handlers a generalisation of exception handlers, and the table sets the two side by side:

An exception An effect
Declared as exception Missing of string type _ Effect.t += Ask : string -> int Effect.t
Started with raise (Missing "width") perform (Ask "width")
Caught by try … with Missing n -> … match … with effect (Ask n), k -> …
The handler receives the exception’s value the effect’s value and the continuation k
Can the stopped code go on? No, it has been left for good Yes: continue k 40 resumes it with 40

The last row is the whole difference, and the rest of the lesson is about it: what k holds, what continue gives back, and what happens when k is used twice or never.

Key idea

An effect is a question that deep code asks and its caller answers: the function that needs a value performs it, and the handler around the call supplies the value and lets the function continue.

Checkpoint

Check: Effects let a caller answer deep code

  1. One question An exception handler and an effect handler both receive something sent from deep inside a call. What can only the effect handler do?

    Choose one answer.

    Show the answer: One question

    Answer: Send a value back to the point where the effect was performed, so that the stopped code carries on

    Both handlers are found the same way, the nearest enclosing one with a matching case. Only the effect handler receives the continuation, the suspended rest of the computation, and can resume it with an answer, which is how usable_width received 40 and 4 without a parameter.

An effect is declared as a constructor, then performed

An effect is a value of the type 'a Effect.t, where 'a is the type of its answer. Effect.t is an A variant type declared with = .. whose constructors can be added later, anywhere in a program, with +=. The type of exceptions, exn, is one, and so is Effect.t., like the type of exceptions, exn, and it is a GADT (generalised algebraic data type): A variant whose constructors can each give a different return type on the right of a colon, which pattern matching then refines; the index tells OCaml which arms are reachable for which value. More in “Generalised algebraic data types (GADTs)”: each constructor names its own answer type after its last arrow. Load the declarations and functions of a small file into the toplevel and read the types it prints:

Two effects declared, their types, and two mistakes the compiler stops Python · declare_session.py
from session import toplevel, write

# Declaring an effect adds a constructor to the extensible GADT Effect.t; the type after the last arrow is the answer
# the performer gets back. A function that performs an effect has an ordinary type: nothing in it names the effect.
# The last two phrases are mistakes the compiler catches: an effect used as the wrong type, and a declaration without
# the type parameter of Effect.t.
write("widths.ml", """type _ Effect.t += Ask : string -> int Effect.t
type _ Effect.t += Log : string -> unit Effect.t

let usable_width () =
  let width = Effect.perform (Ask "width") in
  let margin = Effect.perform (Ask "margin") in
  width - 2 * margin

let logged_width () =
  Effect.perform (Log "usable_width asked");
  usable_width ()
""")
toplevel("""
#use "widths.ml";;
Ask "width";;
Log "starting";;
Effect.perform;;
let label () = Effect.perform (Ask "width") ^ " columns";;
type Effect.t += Ping : unit Effect.t;;
""")

Output

$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# #use "widths.ml";;
type _ Stdlib.Effect.t += Ask : string -> int Effect.t
type _ Stdlib.Effect.t += Log : string -> unit Effect.t
val usable_width : unit -> int = <fun>
val logged_width : unit -> int = <fun>
# Ask "width";;
- : int Effect.t = Ask "width"
# Log "starting";;
- : unit Effect.t = Log "starting"
# Effect.perform;;
- : 'a Effect.t -> 'a = <fun>
# let label () = Effect.perform (Ask "width") ^ " columns";;
Line 1, characters 15-43:
1 | let label () = Effect.perform (Ask "width") ^ " columns";;
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type int but an expression was expected of type
         string
# type Effect.t += Ping : unit Effect.t;;
Line 1, characters 0-37:
1 | type Effect.t += Ping : unit Effect.t;;
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This extension does not match the definition of type Stdlib.Effect.t
       They have different arities.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 declare_session.py

Ask : string -> int Effect.t reads as “Ask carries a string, and performing it gives back an int”, and Log : string -> unit Effect.t gives back (). Ask "width" on its own is only a value of type int Effect.t: nothing happens until it is handed to Effect.perform, whose type, 'a Effect.t -> 'a, turns an int Effect.t into an int. That is why usable_width type-checks as unit -> int, like any arithmetic, and why label, which joins the answer to a string with ^, was refused: the answer is an int, not a string.

Two facts in those types matter for the rest of the lesson. The type of logged_width, unit -> int, says nothing about Log or Ask: OCaml’s types do not list the effects a function may perform, so the compiler never checks that a handler is waiting. And the declaration must keep its _ parameter. Without it OCaml refused the line with “They have different arities”, because Effect.t takes the answer type as its parameter. That index is what lets one Effect.perform return an int for Ask and () for Log, as the GADTs lesson showed for typed expressions.

Checkpoint

Check: Declaring and performing an effect

  1. Question 1 of 2 After type _ Effect.t += Log : string -> unit Effect.t, what does Effect.perform (Log "start") give back to the code that performs it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: () of type unit, once a handler resumes it

    The toplevel printed - : unit Effect.t = Log "starting" for the effect value and 'a Effect.t -> 'a for Effect.perform: performing a unit Effect.t gives back (), the way performing Ask "width" gives back an int.

  2. Question 2 of 2 Which declaration did the OCaml 5.5.1 toplevel refuse with "They have different arities"?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: type Effect.t += Ping : unit Effect.t

    Effect.t is 'a Effect.t: every declaration that extends it writes type _ Effect.t += …, and the type after the constructor's last arrow fills in the parameter.

A handler answers with continue, and the code carries on

with_settings is the handler in full: match f () with | v -> v | effect (Ask name), k -> continue k (List.assoc name settings). The first case is the value case, which runs when f () returns normally. The second is an effect case, marked by the keyword effect. It matches when the computation performs Ask name, and it binds k to the continuation: In OCaml's effect handlers, the suspended rest of a computation, from the perform up to the handler, of type ('a, 'b) continuation: resuming it with an 'a runs that rest and gives back its 'b result.: the suspended rest of f (), from the perform up to this match. continue k 40 resumes that rest, and 40 becomes the value the perform returns. continue and discontinue are functions of Effect.Deep rather than keywords, so a file that writes effect cases still opens that module, as every program on this page does. Step through one call, each arrow a jump of control between the handler and the code it runs:

Interactive · message by message Who runs when, in one call to with_settings Step through the traced run. Each arrow is a jump of control between the handler and the code it runs, and the sub-labels say what each side is doing.
Who runs when, in one call to with_settings, step 7 of 7. Messages so far: with_settings to usable_width: f (); usable_width to with_settings: Ask "width", with k; with_settings to usable_width: continue k 40; usable_width to with_settings: Ask "margin", with k; with_settings to usable_width: continue k 4; usable_width to with_settings: returns 32.with_settingsgives back 32usable_widthreturned 32f ()Ask "width", with kcontinue k 40Ask "margin", with kcontinue k 4returns 32

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

Every step as text
  1. The handler starts the work. with_settings runs f () inside its match, so every effect that usable_width performs goes to this handler first. usable_width prints "asks for width".
  2. perform suspends the code. usable_width performs Ask "width" and stops in the middle of its let. The rest of its work becomes the continuation k, and control jumps to the effect case with name bound to "width".
  3. continue k 40. The handler looks up the width, prints "width is 40, continue" and calls continue k 40. usable_width carries on from the perform, which returns 40, and prints "asks for margin".
  4. The same handler again. Ask "margin" comes to the same handler, because a deep handler goes back around the rest of the computation each time it is resumed. The first continue call has not returned yet.
  5. continue k 4. The handler prints "margin is 4, continue" and resumes the new k with 4. usable_width prints "returns" and gives back 40 - 2 * 4 = 32.
  6. The value case runs. usable_width has returned normally, so the value case of the match runs and prints "value case: 32". Its result, 32, is what the innermost continue call returns.
  7. The continue calls return. continue k 4 returns 32, and the handler prints "continue for margin gave 32". That ends the margin case, so continue k 40 returns 32 too, and with_settings gives 32 to its caller.

The same handler, with a line printed at each step, gives the run that the figure follows:

The settings handler traced step by step, a missing annotation, and the handler in a try Python · handle_session.py
from session import run, write

# The handler of report.ml with a print at every step: who runs when, what continue returns, and that the same
# handler answers the second Ask (a deep handler). The second file is the same handler without the annotation on
# settings, which OCaml refuses.
write("trace.ml", """open Effect
open Effect.Deep

type _ Effect.t += Ask : string -> int Effect.t

let usable_width () =
  print_endline "usable_width: asks for width";
  let width = perform (Ask "width") in
  print_endline "usable_width: asks for margin";
  let margin = perform (Ask "margin") in
  print_endline "usable_width: returns";
  width - 2 * margin

let with_settings (settings : (string * int) list) f =
  match f () with
  | v ->
      Printf.printf "value case: %d\\n" v;
      v
  | effect (Ask name), k ->
      let answer = List.assoc name settings in
      Printf.printf "handler: %s is %d, continue\\n" name answer;
      let rest = continue k answer in
      Printf.printf "handler: continue for %s gave %d\\n" name rest;
      rest

let () = Printf.printf "result: %d\\n" (with_settings [ ("width", 40); ("margin", 4) ] usable_width)
""")
write("no_annotation.ml", """open Effect
open Effect.Deep

type _ Effect.t += Ask : string -> int Effect.t

let with_settings settings f =
  match f () with
  | v -> v
  | effect (Ask name), k -> continue k (List.assoc name settings)
""")
write("with_try.ml", """open Effect
open Effect.Deep

type _ Effect.t += Ask : string -> int Effect.t

let usable_width () =
  let width = perform (Ask "width") in
  let margin = perform (Ask "margin") in
  width - 2 * margin

(* The same handler in a try: effect cases, no value case. *)
let with_settings (settings : (string * int) list) f =
  try f () with effect (Ask name), k -> continue k (List.assoc name settings)

let () = Printf.printf "try: %d\\n" (with_settings [ ("width", 40); ("margin", 4) ] usable_width)
""")
run("""
ocamlopt trace.ml -o trace
./trace
!ocamlopt -c no_annotation.ml
ocamlopt with_try.ml -o with_try
./with_try
""")

Output

$ ocamlopt trace.ml -o trace
$ ./trace
usable_width: asks for width
handler: width is 40, continue
usable_width: asks for margin
handler: margin is 4, continue
usable_width: returns
value case: 32
handler: continue for margin gave 32
handler: continue for width gave 32
result: 32
$ ocamlopt -c no_annotation.ml
File "no_annotation.ml", line 9, characters 39-65:
9 |   | effect (Ask name), k -> continue k (List.assoc name settings)
                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type 'a but an expression was expected of type int
       This instance of int is ambiguous:
       it would escape the scope of its equation
[exit status 2]
$ ocamlopt with_try.ml -o with_try
$ ./with_try
try: 32

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 handle_session.py

Three things in the trace deserve a slow read. usable_width printed “asks for margin” only after the handler had printed “width is 40”: the performer really waits. One handler answered both questions, although they were performed one after the other, because it is a deep handler: An effect handler that is installed again around the computation each time it is resumed, so it receives every effect the computation performs until it returns; the match … with effect syntax writes deep handlers.: it goes back around the computation each time continue resumes it, until the computation returns. And continue itself returns. It gives back what the rest of the computation produced, here 32 from the value case, so “continue for margin gave 32” came out before “continue for width gave 32”: the inner call finished first. The last file, with_try.ml, answered the same questions from a try … with effect, which has no value case, and printed 32 as well.

A handler whose answer has no known type

no_annotation.ml is with_settings without the type on settings, and OCaml refused it with “This instance of int is ambiguous: it would escape the scope of its equation”. Inside the effect case the answer is an int only because the Ask constructor says so, and the type of List.assoc name settings comes from outside the case. Write the type where the value comes from, (settings : (string * int) list), as report.ml does, and the case compiles.

Whatever supplies the answer, a lookup in a table or a constant such as the 0 of the nested handlers below, the value given to continue must have exactly the type the effect declares: an int for Ask.

Key idea

Inside an effect case, k is the rest of the computation: continue k v makes the perform return v, runs the rest under the same handler, and returns what that rest produced.

Checkpoint

Check: Handling an effect and resuming with continue

  1. Question 1 of 2 In the traced run of with_settings, which line came out last, just before result: 32?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: handler: continue for width gave 32

    Each continue returns what the rest of the computation produced. The rest after the width included the margin question, so continue k 4 returned inside continue k 40, and the run printed margin before width.

  2. Question 2 of 2 Why did OCaml refuse let with_settings settings f = match f () with v -> v | effect (Ask name), k -> continue k (List.assoc name settings)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Inside the effect case the answer is an int only by the Ask constructor, and the type of settings comes from outside the case

    The run printed the error at List.assoc name settings. With (settings : (string * int) list) written on the parameter, as in report.ml, the answer is a known int and the case compiles.

An effect with no handler stops the program at the perform

Since no function’s type lists its effects, nothing stops a program from performing one that no handler covers. widths.ml adds a Log line at the top of usable_width and runs it under with_settings, which handles only Ask. It compiled without a word, and then stopped:

An effect nobody handles, then three handlers nested around one function Python · unhandled_session.py
from session import run, write

# The compiler does not check that some handler covers every effect a function performs. widths.ml performs Log as
# well as Ask under a handler that knows only Ask: it compiles without a word and stops when it runs. nested.ml adds
# a handler for Log inside the one for Ask, and runs a handler that answers only "margin" on its own.
write("widths.ml", """open Effect
open Effect.Deep

type _ Effect.t += Ask : string -> int Effect.t
type _ Effect.t += Log : string -> unit Effect.t

let usable_width () =
  perform (Log "computing the usable width");
  let width = perform (Ask "width") in
  let margin = perform (Ask "margin") in
  width - 2 * margin

let with_settings (settings : (string * int) list) f =
  match f () with
  | v -> v
  | effect (Ask name), k -> continue k (List.assoc name settings)

let () = Printf.printf "%d\\n" (with_settings [ ("width", 40); ("margin", 4) ] usable_width)
""")
write("nested.ml", """open Effect
open Effect.Deep

type _ Effect.t += Ask : string -> int Effect.t
type _ Effect.t += Log : string -> unit Effect.t

let usable_width () =
  perform (Log "computing the usable width");
  let width = perform (Ask "width") in
  let margin = perform (Ask "margin") in
  width - 2 * margin

let with_settings (settings : (string * int) list) f =
  match f () with
  | v -> v
  | effect (Ask name), k -> continue k (List.assoc name settings)

(* Prints every Log line; any other effect goes on to the next handler out. *)
let printing_log f =
  match f () with
  | v -> v
  | effect (Log line), k ->
      print_endline ("log: " ^ line);
      continue k ()

(* Answers Ask "margin" only. *)
let no_margin f =
  match f () with
  | v -> v
  | effect (Ask "margin"), k -> continue k 0

let settings = [ ("width", 40); ("margin", 4) ]

let () =
  Printf.printf "nested: %d\\n" (with_settings settings (fun () -> printing_log usable_width));
  Printf.printf "inner answers margin: %d\\n"
    (with_settings settings (fun () -> printing_log (fun () -> no_margin usable_width)));
  let alone =
    try printing_log (fun () -> no_margin usable_width)
    with Effect.Unhandled (Ask name) -> Printf.printf "nobody answered %S\\n" name; -1
  in
  Printf.printf "alone: %d\\n" alone
""")
run("""
ocamlopt widths.ml -o widths
!./widths
ocamlopt nested.ml -o nested
./nested
""")

Output

$ ocamlopt widths.ml -o widths
$ ./widths
Fatal error: exception Stdlib.Effect.Unhandled(Widths.Log("computing the usable width"))
[exit status 2]
$ ocamlopt nested.ml -o nested
$ ./nested
log: computing the usable width
nested: 32
log: computing the usable width
inner answers margin: 40
log: computing the usable width
nobody answered "width"
alone: -1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 unhandled_session.py

OCaml raised Effect.Unhandled with the effect inside it, Widths.Log("computing the usable width"), and the program ended with exit status 2, as with any uncaught exception. The manual says where the exception starts: at the point of the perform. An effect that no enclosing handler covers is an unhandled effect: An effect performed where no enclosing handler has a case for it. OCaml does not detect this at compile time; it raises Effect.Unhandled, with the effect inside, at the point of the perform., and because OCaml’s types do not track effects, the first sign of one is this exception at run time.

nested.ml shows how handlers share the work. A handler that has no case for an effect hands it on to the next handler out, as a try that does not match an exception lets it through. Follow its second and third runs:

Interactive · stage by stage How an effect finds its handler, in the nested runs Step through the second and third runs of nested.ml. An effect goes to the innermost handler first and moves outward until a handler has a case for it.
How an effect finds its handler, in the nested runs, step 6 of 6. Showing usable_width, no_margin (answers the margin), printing_log (answers Log), with_settings (not installed), Effect.Unhandled. In focus: Effect.Unhandled, usable_width.raised hereusable_widthperforms three effectsno_marginanswers the marginprinting_loganswers Logwith_settingsnot installedEffect.Unhandledno handler is leftEffect.Unhandled

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

Every step as text
  1. Three handlers, one call. In the second run, with_settings runs printing_log, which runs no_margin, which runs usable_width. Every effect goes first to the innermost handler, no_margin, and moves outward from there.
  2. Log moves past no_margin. usable_width performs Log. no_margin has a case only for Ask "margin", so the effect moves on to printing_log, which prints the log line and resumes the performer.
  3. The width goes outward. Ask "width" matches neither the margin case of no_margin nor anything in printing_log, so it reaches with_settings, which answers 40 from its table.
  4. The margin stays inside. Ask "margin" is caught by the innermost handler, no_margin, which answers 0, and with_settings never sees it. The run printed inner answers margin: 40, from 40 - 2 * 0.
  5. No outer handler. The third run calls printing_log and no_margin with no with_settings around them. Ask "width" passes both, and no handler is left to answer it.
  6. Raised at the perform. OCaml raises Effect.Unhandled (Ask "width") where usable_width performed the effect. It travels up through both handlers like an exception, to the try around the call, which printed nobody answered "width".

In the second run printing_log printed the log line, no_margin answered 0 for the margin and with_settings answered the width, so the innermost answer won: 40 − 2 × 0 = 40. In the third run nothing answered Ask "width". Effect.Unhandled came out of the perform, passed through both handlers like an exception, and reached the try around the call, which matched Effect.Unhandled (Ask name) and printed nobody answered "width".

Key idea

OCaml never checks effects at compile time: an effect that no enclosing handler covers raises Effect.Unhandled at its perform, and a handler without a case for an effect hands it to the next handler out.

Checkpoint

Check: Effect.Unhandled and handlers that hand effects on

  1. One question widths.ml performs Log under a handler that has a case only for Ask. What did OCaml 5.5.1 do?

    Choose one answer.

    Show the answer: One question

    Answer: It compiled without a word and stopped at run time with Effect.Unhandled and exit status 2

    The search for a handler happens when the effect is performed. No enclosing handler had a case for Log, so Effect.Unhandled was raised at that perform and ended the program like any uncaught exception.

A continuation can be resumed only once

The handlers so far called continue once for each effect. A handler may also call it twice, keep k for later, or never call it, and the runtime treats each of these differently. twice.ml answers one Ask with 40 and then again with 24. cleanup.ml suspends a job that keeps its closing step in Fun.protect, where a job with a real file would close it, and ends it in two ways:

A continuation resumed twice, and a suspended job dropped or ended by discontinue Python · oneshot_session.py
from session import run, write

# A continuation can be resumed once. twice.ml resumes one two times. cleanup.ml ends a suspended job that holds an
# open resource in two ways: the first handler never resumes it, the second resumes it with an exception through
# discontinue, so the job's Fun.protect cleanup runs.
write("twice.ml", """open Effect
open Effect.Deep

type _ Effect.t += Ask : string -> int Effect.t

let () =
  let total =
    match perform (Ask "width") with
    | v -> v
    | effect (Ask _), k ->
        let first = continue k 40 in
        Printf.printf "first continue gave %d\\n" first;
        first + continue k 24
  in
  Printf.printf "total %d\\n" total
""")
write("cleanup.ml", """open Effect
open Effect.Deep

type _ Effect.t += Ask : string -> int Effect.t

exception Cancelled

let job () =
  print_endline "job: opens the output file";
  Fun.protect
    ~finally:(fun () -> print_endline "job: closes the output file")
    (fun () ->
      let width = perform (Ask "width") in
      Printf.printf "job: writes %d columns\\n" width;
      width)

(* Never resumes k: the rest of job, its cleanup included, never runs. *)
let dropping () =
  match job () with
  | v -> v
  | effect (Ask _), _k ->
      print_endline "handler: no answer, returns -1";
      -1

(* Resumes k by raising Cancelled where job performed Ask. *)
let cancelling () =
  match job () with
  | v -> v
  | exception Cancelled ->
      print_endline "handler: Cancelled came out of job";
      -1
  | effect (Ask _), k ->
      print_endline "handler: discontinue k Cancelled";
      discontinue k Cancelled

let () =
  Printf.printf "dropping gave %d\\n\\n" (dropping ());
  Printf.printf "cancelling gave %d\\n" (cancelling ())
""")
run("""
ocamlopt twice.ml -o twice
!./twice
ocamlopt cleanup.ml -o cleanup
./cleanup
""")

Output

$ ocamlopt twice.ml -o twice
$ ./twice
first continue gave 40
Fatal error: exception Stdlib.Effect.Continuation_already_resumed
[exit status 2]
$ ocamlopt cleanup.ml -o cleanup
$ ./cleanup
job: opens the output file
handler: no answer, returns -1
dropping gave -1

job: opens the output file
handler: discontinue k Cancelled
job: closes the output file
handler: Cancelled came out of job
cancelling gave -1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 oneshot_session.py

The first continue ran the rest of the computation and gave 40, and the second raised Continuation_already_resumed. An OCaml continuation is a one-shot continuation: A continuation that can be resumed only once: OCaml never copies the suspended stack, and continue or discontinue on a continuation already resumed raises Continuation_already_resumed.: it points at the suspended part of the stack itself, which is never copied, so once that part has run there is nothing left to run again. The manual’s reasons for the design are cost, since capturing and resuming copy no stack frames, and safety for resources such as files and sockets, which a second run would use twice.

The two job runs show the other half of the rule. dropping returned −1 without touching k, and the job never printed its closing line: a continuation that is never resumed leaves its code suspended with whatever it opened, and the manual adds that the memory of the suspended stack leaks too. cancelling ended the job with discontinue k Cancelled, which raises Cancelled at the perform. The job’s Fun.protect ran its finally, the exception left the job, and the exception Cancelled case of the same match caught it, so one match can hold a value case, exception cases and effect cases together. The manual’s discipline is that every continuation is eventually continued or discontinued, exactly once.

Once does not have to mean at once. A handler may store k and resume it after its match has returned:

A continuation kept in a reference and resumed after its handler returned Python · keep_session.py
from session import run, write

# A handler may keep the continuation and resume it after the match has returned. Kept in a reference whose type
# the compiler must infer, it is refused; with the type written on the reference, it compiles and runs.
write("keep_bad.ml", """open Effect
open Effect.Deep

type _ Effect.t += Pause : unit Effect.t

let saved = ref None

let () =
  match (print_endline "step 1"; perform Pause; print_endline "step 2") with
  | () -> print_endline "finished"
  | effect Pause, k -> saved := Some k
""")
write("keep.ml", """open Effect
open Effect.Deep

type _ Effect.t += Pause : unit Effect.t

let saved : (unit, unit) continuation option ref = ref None

let () =
  (match (print_endline "step 1"; perform Pause; print_endline "step 2") with
   | () -> print_endline "finished"
   | effect Pause, k ->
       print_endline "paused, k kept";
       saved := Some k);
  print_endline "the match has returned";
  match !saved with
  | Some k ->
      saved := None;
      continue k ()
  | None -> ()
""")
run("""
!ocamlopt -c keep_bad.ml
ocamlopt keep.ml -o keep
./keep
""")

Output

$ ocamlopt -c keep_bad.ml
File "keep_bad.ml", line 11, characters 37-38:
11 |   | effect Pause, k -> saved := Some k
                                          ^
Error: The value k has type (unit, unit) continuation/2
       but an expression was expected of type 'a
       This instance of unit is ambiguous:
       it would escape the scope of its equation
       File "_none_", line 1:
         Definition of type continuation/2
[exit status 2]
$ ocamlopt keep.ml -o keep
$ ./keep
step 1
paused, k kept
the match has returned
step 2
finished

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 keep_session.py

keep.ml printed “the match has returned” before “step 2”: the computation waited in the reference until the program chose to resume it, which is how a scheduler built on effects keeps a task waiting until its turn comes. The first attempt, keep_bad.ml, failed with “This instance of unit is ambiguous: it would escape the scope of its equation”, because the reference had no type of its own and the continuation’s types are fixed only inside the effect case. Writing the type on the reference, (unit, unit) continuation option ref, was enough.

Key idea

Resume every continuation exactly once: a second resume raises Continuation_already_resumed, no resume leaves the code and its resources suspended, and discontinue ends it with an exception so that its cleanup runs.

Checkpoint

Check: One-shot continuations and discontinue

  1. Question 1 of 3 A handler calls continue k 40 and then continue k 24 on the same k. What does the second call do?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: It raises Continuation_already_resumed

    A continuation points at the suspended part of the stack itself. Once continue k 40 has run that part, there is nothing left to resume, so the second continue raises Continuation_already_resumed.

  2. Question 2 of 3 A suspended job keeps its closing step in Fun.protect, and its handler decides not to give it an answer. Which handler lets that closing step run?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: One that calls discontinue k Cancelled, raising Cancelled where the job performed its effect

    discontinue resumes the continuation by raising an exception at the perform, so the job runs on from there as if the perform had raised: Fun.protect runs its finally, and the exception leaves the job.

  3. Question 3 of 3 keep_bad.ml stored k in let saved = ref None and was refused. What made keep.ml compile?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: Writing the type of the reference, (unit, unit) continuation option ref

    With the type written, keep.ml printed step 1, paused, k kept, the match has returned, step 2 and finished: the continuation waited in the reference and was resumed after the handler had returned.

Before OCaml 5.3, a handler was a record

Effects arrived in OCaml 5.0 without any syntax of their own: a handler was a record of three functions given to Effect.Deep.match_with. The match … with effect cases came in 5.3, and the record API is still in the standard library, so you will meet it in code written for 5.0 to 5.2 and in libraries that still build there. Here is with_settings written twice in the record API:

The settings handler as a match_with record and a try_with record, and the annotation effc needs Python · record_session.py
from session import run, write

# The handler of report.ml written as before OCaml 5.3: a record given to Effect.Deep.match_with (value, exception
# and effect cases) or to Effect.Deep.try_with (the effect case only). Both still work on 5.5.1. The effc field must
# accept every effect type, so it needs a locally abstract type; record_bad.ml leaves it out.
write("record.ml", """open Effect
open Effect.Deep

type _ Effect.t += Ask : string -> int Effect.t

let usable_width () =
  let width = perform (Ask "width") in
  let margin = perform (Ask "margin") in
  width - 2 * margin

let with_settings (settings : (string * int) list) f =
  match_with f ()
    { retc = (fun v -> v);
      exnc = raise;
      effc =
        (fun (type c) (eff : c Effect.t) ->
          match eff with
          | Ask name ->
              Some (fun (k : (c, _) continuation) -> continue k (List.assoc name settings))
          | _ -> None) }

let with_settings_try (settings : (string * int) list) f =
  try_with f ()
    { effc =
        (fun (type c) (eff : c Effect.t) ->
          match eff with
          | Ask name ->
              Some (fun (k : (c, _) continuation) -> continue k (List.assoc name settings))
          | _ -> None) }

let settings = [ ("width", 40); ("margin", 4) ]

let () =
  Printf.printf "match_with: %d\\n" (with_settings settings usable_width);
  Printf.printf "try_with: %d\\n" (with_settings_try settings usable_width)
""")
write("record_bad.ml", """open Effect
open Effect.Deep

type _ Effect.t += Ask : string -> int Effect.t

let with_settings (settings : (string * int) list) f =
  match_with f ()
    { retc = (fun v -> v);
      exnc = raise;
      effc =
        (fun eff ->
          match eff with
          | Ask name -> Some (fun k -> continue k (List.assoc name settings))
          | _ -> None) }
""")
run("""
ocamlopt record.ml -o record
./record
!ocamlopt -c record_bad.ml
""")

Output

$ ocamlopt record.ml -o record
$ ./record
match_with: 32
try_with: 32
$ ocamlopt -c record_bad.ml
File "record_bad.ml", lines 11-14, characters 8-22:
11 | ........(fun eff ->
12 |           match eff with
13 |           | Ask name -> Some (fun k -> continue k (List.assoc name settings))
14 |           | _ -> None)..
Error: This field value has type
         int Effect.t -> ((int, 'a) Effect.Deep.continuation -> 'a) option
       which is less general than
         'c. 'c Effect.t -> (('c, 'b) Effect.Deep.continuation -> 'b) option
       The type int is not a type variable.
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 record_session.py

The record’s fields are the cases of the match: retc handles the value, exnc an exception (here raise, so exceptions go on unchanged) and effc the effects. effc receives the effect alone and returns an option: Some of a function that takes the continuation, for an effect it handles, or None to hand the effect on. try_with takes only effc, keeps the value as it is and raises every exception again. Both records printed 32, as the syntax did.

The difference is in the types. effc must accept an effect of any answer type, so it starts with fun (type c) (eff : c Effect.t) ->, a locally abstract type: A type a function introduces with 'type a.', kept abstract inside the body and refined per arm, which lets pattern matching on a GADT type-check each arm against its own case's type. More in “Generalised algebraic data types (GADTs)” like the type a. of the GADTs lesson, written here for one anonymous function, and each case annotates its continuation as (k : (c, _) continuation). Without them OCaml inferred int Effect.t from the Ask case and refused the field as “less general than” the record’s 'c. 'c Effect.t -> … type. The syntax needs none of this, and it needs no last | _ -> None case either: an effect without a case simply goes on outward. Which form to write depends on the compilers your code must build on and on what the handler does with values and exceptions:

Interactive · answer and compare The 5.3 syntax or an Effect.Deep record? Answer two questions about your code. The table marks the handler forms that fit and says why the others do not.
Which OCaml releases must the code build on?
Must the handler change the result, or catch exceptions from the computation?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Three ways to write the settings handler, against what decides between them, with what the lesson's runs printed
Handler form Compilers it builds on Annotations it needs Value and exception cases Effects it does not handle
match … with effect Written as: effect cases beside the value case Mixed 5.3 and later only: older compilers have no effect patterns Good only the type of the answer, such as the settings table Good v -> … and exception e -> … sit in the same match Good an effect with no case goes on to the next handler by itself
Effect.Deep.match_with Written as: a record of retc, exnc and effc Good every release from 5.0; the run printed 32 on 5.5.1 Poor fun (type c) (eff : c Effect.t), and k annotated in each case Good retc for the value, exnc for exceptions Mixed needs a last case, | _ -> None, to hand them on
Effect.Deep.try_with Written as: a record with effc only Good every release from 5.0; the run printed 32 on 5.5.1 Poor the same locally abstract type and annotations as match_with Poor keeps the value as it is and raises every exception again Mixed needs | _ -> None, as match_with does
How the answers decide
  • “Which OCaml releases must the code build on?” Also 5.0, 5.1 or 5.2: rules out match … with effect. Effect patterns arrived in 5.3, so a handler for 5.0 to 5.2 is a record given to match_with or try_with.
  • “Which OCaml releases must the code build on?” 5.3 or later only: adds a caveat to Effect.Deep.match_with and Effect.Deep.try_with. Both records still work on 5.5.1, where each printed 32, but they need a locally abstract type and annotations that the syntax does without.
  • “Must the handler change the result, or catch exceptions from the computation?” Yes, it changes the value or catches: rules out Effect.Deep.try_with. try_with returns the value unchanged and raises every exception again; the syntax has value and exception cases, and match_with has retc and exnc.
  • “Must the handler change the result, or catch exceptions from the computation?” No, it only answers effects: Any of the three can answer the effects; the two records also need a last | _ -> None case for the effects they hand on.

In short: on 5.3 and later, write effect cases in a match; for code that must also build on 5.0 to 5.2, use match_with, or try_with when the handler only answers effects. Beside deep handlers, Effect.Shallow provides shallow ones, which handle a single effect and must be given a handler again for the next one, with no syntax of their own; the next lesson runs one program under both kinds. The releases since 5.0 changed how handlers are written far more than what they do:

Timeline · 6 events in order Effect handlers in OCaml, release by release Walk through the releases in order. Handlers have worked the same way since 5.0; what changed is how they are written and what the manual says about their limits.
  1. 5.0

    Effects arrive, without syntax new module

    The release that made OCaml multicore added the Effect module, with perform, continue and discontinue, and both Effect.Deep and Effect.Shallow. A handler was a record given to match_with or try_with, and the manual gained a tutorial on effect handlers.

  2. 5.1

    More back ends, and no marshalled continuations

    Multicore support, fibers and effects included, came back to the RISC-V and s390x back ends, and marshalling a continuation with the Marshal module was made to raise Invalid_argument.

  3. 5.2

    Where effects cannot go, documented

    The manual now says that an effect performed from a signal handler, a finaliser or a memprof callback cannot be caught by the main code, and that no effect crosses a call from C into OCaml through caml_callback: each case ends in Effect.Unhandled.

  4. 5.3

    The match … with effect syntax new syntax

    Deep handlers gained syntax: effect cases in a match or a try, with the continuation bound beside the effect, and the manual tutorial was rewritten with it. effect became a keyword, and the new -keywords option, as in -keywords 5.2, builds older code that uses effect as a name.

  5. 5.4

    effect as an attribute name

    effect, a keyword since 5.3, may be used as the name of an attribute, as in [@effect].

  6. 5.5

    An effect declared for one expression new in 5.5

    let type _ Effect.t += Ping : unit Effect.t in … declares an effect for the expression that follows, as let exception does for an exception; the lesson runs one. A bug with unhandled effects in bytecode programs was fixed.

The timeline has 6 events, from 5.0 to 5.5.

One last session shows the 5.3 keyword, the module’s alert and the 5.5 form of a declaration:

effect as a keyword, -keywords 5.2, the unstable alert and a 5.5 local effect Python · versions_session.py
from session import run, write

# Three version facts, run on OCaml 5.5.1. Since 5.3 effect is a keyword, so old code that names a value effect
# stops compiling unless it is built with -keywords 5.2. The Effect module carries an unstable alert, which the
# compiler shows only when asked. Since 5.5 an effect can be declared for one expression with let type ... in, and
# outside.ml shows that the constructor does not exist after that expression.
write("old_names.ml", """let effect = "side effect"
let () = print_endline effect
""")
write("uses_effect.ml", """open Effect

type _ t += Ask : string -> int t

let width () = perform (Ask "width")
""")
write("local.ml", """let () =
  let type _ Effect.t += Ping : unit Effect.t in
  match Effect.perform Ping; "pong" with
  | answer -> print_endline answer
  | effect Ping, k ->
      print_endline "ping";
      Effect.Deep.continue k ()
""")
write("outside.ml", """let pinged =
  let type _ Effect.t += Ping : unit Effect.t in
  match Effect.perform Ping with
  | () -> true
  | effect Ping, k -> Effect.Deep.continue k ()

let again = Effect.perform Ping
""")
run("""
!ocamlopt -c old_names.ml
ocamlopt -keywords 5.2 old_names.ml -o old_names
./old_names
ocamlopt -c uses_effect.ml
ocamlopt -c -alert +unstable uses_effect.ml
ocamlopt local.ml -o local
./local
!ocamlopt -c outside.ml
""")

Output

$ ocamlopt -c old_names.ml
File "old_names.ml", line 1, characters 4-10:
1 | let effect = "side effect"
        ^^^^^^
Error: Syntax error
[exit status 2]
$ ocamlopt -keywords 5.2 old_names.ml -o old_names
$ ./old_names
side effect
$ ocamlopt -c uses_effect.ml
$ ocamlopt -c -alert +unstable uses_effect.ml
File "uses_effect.ml", line 1, characters 5-11:
1 | open Effect
         ^^^^^^
Alert unstable: module Stdlib.Effect
The Effect interface may change in incompatible ways in the future.
$ ocamlopt local.ml -o local
$ ./local
ping
pong
$ ocamlopt -c outside.ml
File "outside.ml", line 7, characters 27-31:
7 | let again = Effect.perform Ping
                               ^^^^
Error: This variant expression is expected to have type 'a Effect.t
       There is no constructor Ping within type Effect.t
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 versions_session.py

local.ml declared Ping for a single expression with let type _ Effect.t += Ping : unit Effect.t in, and its handler printed “ping” before the expression’s own “pong”. Outside that expression Ping does not exist, as with a let exception: outside.ml performed it after the let and was refused with “There is no constructor Ping within type Effect.t”.

effect is a keyword from OCaml 5.3

Code that names a value effect no longer parses: old_names.ml failed with “Syntax error” on 5.5.1 and built unchanged with -keywords 5.2. The Effect module also carries an unstable alert, which says its interface may change in incompatible ways; the compiler showed it only when the build asked with -alert +unstable.

Checkpoint

Check: The match_with and try_with record API

  1. Question 1 of 2 In the match_with record, why does effc start with fun (type c) (eff : c Effect.t) ->?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: effc must accept effects of every answer type; without the locally abstract type OCaml inferred int Effect.t and refused it

    The record's effc field has the type 'c. 'c Effect.t -> (('c, 'b) continuation -> 'b) option, so it must work for every 'c. The locally abstract type c names that type inside the function, and each case of the inner match refines it.

  2. Question 2 of 2 A library that uses effects must still build on OCaml 5.1. Which handler form can it use?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Effect.Deep.match_with or try_with with an effc record

    Code that must build on 5.0, 5.1 or 5.2 writes its handlers as records for Effect.Deep.match_with or try_with; the same code still builds and runs on 5.5.1.

Practise: effects, handlers and continuations

These questions reuse the lesson’s own programs: the report and its two tables, the nested handlers, the job that holds a file open and the record handler. Work each answer out from what the sessions printed before you check it:

Practice that adapts to you

Practise this lesson

16 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 16 What does the Ask effect give the lesson's report?

    Choose one answer.

    Show the answer: Question 1 of 16

    Answer: usable_width reads its settings without a parameter through rule, header and report, and without a global

    The function that needs the values performs Ask, and the code that starts the report answers it, so neither a parameter threaded through every layer nor a global variable is needed.

  2. Question 2 of 16 A program prints two reports with different page settings, and usable_width must stay unchanged with no global variable. Which approach fits?

    Choose one answer.

    Show the answer: Question 2 of 16

    Answer: Run each report under its own settings handler

    A handler decides the answers for the computation it runs. Two handlers around two calls give two sets of answers to the same unchanged code.

  3. Question 3 of 16 In type _ Effect.t += Ask : string -> int Effect.t, what does the int say?

    Choose one answer.

    Show the answer: Question 3 of 16

    Answer: perform (Ask …) returns an int to the code that performs it

    An effect declaration reads like a constructor of a GADT: what it carries before the arrow, and the index after it, which is the type of the answer that perform returns.

  4. Question 4 of 16 The toplevel refused let label () = Effect.perform (Ask "width") ^ " columns". Why?

    Choose one answer.

    Show the answer: Question 4 of 16

    Answer: perform (Ask "width") has type int, and ^ joins two strings

    perform turns an int Effect.t into an int, and the type checker treats that int like any other: string_of_int would be needed before ^.

  5. Question 5 of 16 usable_width performs Ask twice. What type did the toplevel print for it?

    Choose one answer.

    Show the answer: Question 5 of 16

    Answer: unit -> int

    Nothing in a function's type records the effects it performs, so logged_width, which performs Log and Ask, also printed as unit -> int.

  6. Question 6 of 16 In effect (Ask name), k -> continue k 40, what is k?

    Choose one answer.

    Show the answer: Question 6 of 16

    Answer: The suspended rest of the computation, from the perform up to the handler, waiting for an int

    The manual calls k a delimited continuation: it stands for the computation between the perform and the handler, and its type, (int, 'b) continuation, says it expects an int.

  7. Question 7 of 16 What does with_settings [ ("width", 24); ("margin", 2) ] usable_width return?

    Type a number.

    A hint

    usable_width computes width - 2 * margin from the two answers.

    Show the answer: Question 7 of 16

    Answer: 20

    The handler answers 24 and then 2, so usable_width returns 24 - 2 * 2 = 20. The report run under this table printed Monthly sales (20 columns) above a rule of 20 dashes.

  8. Question 8 of 16 Why did one with_settings handler answer both Ask "width" and Ask "margin", performed one after the other?

    Choose one answer.

    Show the answer: Question 8 of 16

    Answer: It is a deep handler: continue resumes the rest of the computation with the handler around it again

    The manual describes a deep handler as one that handles every effect the computation performs until it terminates; the match … with effect syntax writes deep handlers.

  9. Question 9 of 16 When is Effect.Unhandled raised?

    Choose one answer.

    Show the answer: Question 9 of 16

    Answer: When an effect is performed and no enclosing handler has a case for it; it is raised at that perform

    The search for a handler happens at run time, when the effect is performed: if no handler around that point has a matching case, the perform raises Effect.Unhandled with the effect inside it.

  10. Question 10 of 16 In the run where no_margin sat inside printing_log, inside with_settings, which handler answered Ask "width"?

    Choose one answer.

    Show the answer: Question 10 of 16

    Answer: with_settings, because no_margin answers only "margin" and printing_log only Log

    An effect goes to the innermost handler first and moves outward until a case matches. The innermost answer wins where there is one: no_margin answered the margin with 0, so the run printed 40.

  11. Question 11 of 16 What did twice.ml print before it stopped?

    Choose one answer.

    Show the answer: Question 11 of 16

    Answer: first continue gave 40, then a Fatal error with Continuation_already_resumed

    OCaml continuations are one-shot. The run ended with exit status 2 after Fatal error: exception Stdlib.Effect.Continuation_already_resumed.

  12. Question 12 of 16 In cleanup.ml, which line appeared only in the run that used discontinue?

    Choose one answer.

    Show the answer: Question 12 of 16

    Answer: job: closes the output file

    A dropped continuation never runs again, so the cleanup inside it never runs; discontinue runs the rest of the job with an exception, and the finally of Fun.protect runs as it would for any exception.

  13. Question 13 of 16 keep.ml printed "the match has returned" before "step 2". What does that show?

    Choose one answer.

    Show the answer: Question 13 of 16

    Answer: A continuation can be resumed after its handler has returned, by any code that holds it

    Resuming once does not mean resuming at once. A stored continuation waits until the program resumes it, which is how a scheduler keeps many suspended computations and runs them in turn.

  14. Question 14 of 16 What are the three fields of an Effect.Deep handler record for match_with?

    Choose one answer.

    Show the answer: Question 14 of 16

    Answer: retc for the value, exnc for exceptions and effc for effects

    ('a, 'b) Effect.Deep.handler has retc : 'a -> 'b, exnc : exn -> 'b and effc, which takes an effect and returns an optional function of its continuation.

  15. Question 15 of 16 In effc, what does returning None for an effect mean?

    Choose one answer.

    Show the answer: Question 15 of 16

    Answer: This handler does not handle it, and the effect goes on to the next handler out

    effc returns Some of a function that takes the continuation for each effect it handles, and None for the rest, which then travel outward as an effect without a case does in the syntax.

  16. Question 16 of 16 Old code with let effect = "side effect" failed on 5.5.1 with Syntax error. Which option built it unchanged?

    Choose one answer.

    Show the answer: Question 16 of 16

    Answer: -keywords 5.2

    effect became a keyword with the 5.3 syntax. The -keywords option, added in 5.3, picks the keyword set of an earlier release for code written before then.

Interview questions

A warm-up for freshers and 2–5 years. Both kinds of handler catch something thrown from deep inside a call: what can an effect handler do that an exception handler cannot?

Both are found the same way: the nearest enclosing handler with a matching case runs, and one without a case lets the exception or the effect go on outward. The difference is what the handler receives. An exception handler gets the exception’s value, and the code that raised it has already been abandoned, so the handler can only take over. An effect handler also gets the continuation, the suspended rest of the computation from the perform onwards, and can resume it with continue k v, which makes the perform return v. That makes an effect a question the caller answers, such as a setting or a log line. It also brings duties an exception lacks: a continuation can be resumed only once, or Continuation_already_resumed is raised, and one that will not be continued should be ended with discontinue so its cleanup runs.

Three questions for experienced engineers follow: a settings handler that worked until request work moved into domains, a test handler for code that logs and reads settings, and an importer whose callers each want bad rows treated differently. Sketch how you would approach each one; its page has a first step to measure your sketch against.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A service installs one settings handler around its main function, and its code reads limits with perform (Ask "max_rows"). After request work moves into Domain.spawn, every request fails with Effect.Unhandled (Ask "max_rows"), although the handler is still in place. Why, and how do you fix it?

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

  2. Mid to senior

    A pricing function reads settings through an Ask effect and writes progress lines through a Log effect. You want unit tests that run it with no files, no globals and no printing, and that check both its result and what it logged. How do you write the test handler, and what should each test assert?

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    A CSV importer meets rows with no price. One caller wants such rows skipped and counted, another wants a default price, a third wants the import stopped at the first bad row, and the importer must not know which. Design this with an effect, and say how stopping should work.

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

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

  1. What is an effect in OCaml 5?

    A question that deep code asks with perform and the code around the call answers in a handler, so a setting reaches a function four calls down without a parameter or a global.

  2. How is an effect declared, and why does perform (Ask "width") have type int?

    As a constructor of the extensible GADT Effect.t, type _ Effect.t += Ask : string -> int Effect.t, whose index is the answer's type.

  3. In match … with effect (Ask name), k -> continue k v, what are k and continue?

    k is the suspended rest of the computation; continue k v makes the perform return v, runs the rest under the same deep handler and returns what the rest produced.

  4. What happens when no handler covers a performed effect?

    Effect.Unhandled is raised at the perform, at run time only, because OCaml does not check effects at compile time. A handler with no case for an effect passes it to the next handler out.

  5. How many times may a continuation be resumed, and what does discontinue do?

    Exactly once: a second continue raises Continuation_already_resumed, and a dropped one never runs its cleanup. discontinue ends it with an exception so that Fun.protect can tidy up.

  6. Why was a handler refused with "This instance of int is ambiguous"?

    The answer's type inside the effect case came from outside it. Annotate where the value comes from, as in (settings : (string * int) list).

  7. What changed for handlers in OCaml 5.3, and how did code look before?

    effect became a keyword, so match … with effect works; before, a handler was a record of retc, exnc and effc passed to Effect.Deep.match_with, which still works. Code that names a value effect needs -keywords 5.2.

  8. How does an effect handler differ from an exception handler?

    Both are the nearest enclosing handler with a matching case, but an effect handler also gets the continuation and can resume the code from the perform, while the code that raised an exception is already abandoned.

Key takeaways

  • An effect lets the function that needs a value ask for it with perform while the code that starts the work answers in a handler, so the report’s settings reached usable_width without a parameter or a global.
  • type _ Effect.t += Ask : string -> int Effect.t adds a constructor to an extensible GADT whose index is the answer’s type, so perform (Ask "width") has type int, and no function’s type records the effects it performs.
  • In match … with effect (Ask name), k -> continue k v, k is the suspended rest of the computation, and continue makes the perform return v, keeps the deep handler around the rest and returns what the rest produced.
  • An effect that no handler covers raises Effect.Unhandled at its perform, at run time only, and a handler with no case for an effect hands it to the next handler out.
  • A continuation is resumed exactly once: a second continue raises Continuation_already_resumed, a dropped one never runs its cleanup, and discontinue ends it with an exception so that Fun.protect can tidy up.
  • Before 5.3 a handler was a record of retc, exnc and effc given to Effect.Deep.match_with or try_with, which still works, at the cost of a locally abstract type and annotations that the syntax does not need.

Terms in this lesson

continuation
In OCaml's effect handlers, the suspended rest of a computation, from the perform up to the handler, of type ('a, 'b) continuation: resuming it with an 'a runs that rest and gives back its 'b result.
deep handler
An effect handler that is installed again around the computation each time it is resumed, so it receives every effect the computation performs until it returns; the match … with effect syntax writes deep handlers.
effect
An operation declared by adding a constructor to OCaml's extensible type 'a Effect.t and started with Effect.perform: the code that performs it stops until a handler supplies an answer of type 'a.
effect handler
Code that runs a computation and receives each effect it performs together with the continuation, then decides what to do: resume it with continue, end it with discontinue, or neither. OCaml 5.3 writes one as effect cases in a match.
one-shot continuation
A continuation that can be resumed only once: OCaml never copies the suspended stack, and continue or discontinue on a continuation already resumed raises Continuation_already_resumed.
unhandled effect
An effect performed where no enclosing handler has a case for it. OCaml does not detect this at compile time; it raises Effect.Unhandled, with the effect inside, at the point of the perform.

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.