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 11 – Advanced types

Generalised algebraic data types (GADTs)

Index your variants with a type parameter so the compiler refuses impossible terms, write evaluators with a locally abstract type and pack existentials.

  • Advanced
  • 10 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: 10 minutes
Free: the whole lesson. With a pass: the model answer to its interview question.

What you will learn

  • Define a GADT whose constructors refine the type index
  • Write an evaluator with a locally abstract type annotation (type a. a expr -> a)
  • Build an existential constructor that packages a value with its operations
  • Choose a GADT or a plain variant for a given expression language

Before you start

What you’ll learn · Before you start

What you will learn

  • Define a GADT whose constructors refine the type index
  • Write an evaluator with a locally abstract type annotation (type a. a expr -> a)
  • Build an existential constructor that packages a value with its operations
  • Choose a GADT or a plain variant for a given expression language

Before you start

On this page

Writing an evaluator for a tiny language with integers, booleans and additions, in an ordinary variant, usually ends in a run-time guard: Add has to assert that both sub-terms are integers, and If has to assert that its condition is a boolean. A bad input finds the hole as Failure "int in Add", not as a type error. 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. gives each constructor its own return type, so the compiler already knows that Add makes an int expr and Bool true is a bool expr, and the two cannot be mixed.

Every output on this page was printed by OCaml 5.5.1. The four sessions build a small typed language, a list of mixed values that carry their own printers, a heterogeneous list in the style the new 5.5 Printf.lprintf reads, and a type-safe key-value store.

A constructor’s return type refines the index

In a plain variant, every constructor builds the same type: type t = A of int | B of string makes A and B both values of t. In a GADT, each constructor declares its own return type, so the A type parameter whose value, inside a GADT, picks which cases of a match are reachable. after the arrow is different per case:

A typed expression language, with the two compile-time refusals the GADT buys you Python · typed_expr_session.py
"""A typed expression language built with a GADT: typed-expr.ml runs, and two broken siblings fail at compile time."""

from session import files, run, toplevel, write

files("typed-expr.ml")

run("""
ocaml typed-expr.ml
""")

toplevel("""
#use "typed-expr.ml";;
eval (Add (Int 2, Int 3));;
eval (Eq (Int 1, Int 1));;
eval (If (Bool false, Int 10, Int 20));;
""")

# A match without the locally abstract type annotation, and an ill-typed term: both are refused.
write("no_annot.ml", """type _ expr =
  | Int : int -> int expr
  | Bool : bool -> bool expr
  | Add : int expr * int expr -> int expr

let rec eval = function
  | Int n -> n
  | Bool b -> b
  | Add (a, b) -> eval a + eval b
""")
write("ill_typed.ml", """type _ expr = Int : int -> int expr | Bool : bool -> bool expr | Add : int expr * int expr -> int expr
let _ = Add (Int 1, Bool true)
""")
run("""
!ocamlc -c no_annot.ml
!ocamlc -c ill_typed.ml
""")

Output

$ ocaml typed-expr.ml
If (Eq (Int 1, Add (Int 0, Int 1)), Int 10, Int 20) => 10
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# #use "typed-expr.ml";;
type _ expr =
    Int : int -> int expr
  | Bool : bool -> bool expr
  | Add : int expr * int expr -> int expr
  | Eq : int expr * int expr -> bool expr
  | If : bool expr * 'a expr * 'a expr -> 'a expr
val eval : 'a expr -> 'a = <fun>
If (Eq (Int 1, Add (Int 0, Int 1)), Int 10, Int 20) => 10
# eval (Add (Int 2, Int 3));;
- : int = 5
# eval (Eq (Int 1, Int 1));;
- : bool = true
# eval (If (Bool false, Int 10, Int 20));;
- : int = 20
$ ocamlc -c no_annot.ml
File "no_annot.ml", line 8, characters 4-10:
8 |   | Bool b -> b
        ^^^^^^
Error: This pattern matches values of type bool expr
       but a pattern was expected which matches values of type int expr
       Type bool is not compatible with type int
[exit status 2]
$ ocamlc -c ill_typed.ml
File "ill_typed.ml", line 2, characters 20-29:
2 | let _ = Add (Int 1, Bool true)
                        ^^^^^^^^^
Error: This constructor has type bool expr
       but an expression was expected of type int expr
       Type bool is not compatible with type int
[exit status 2]

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

Int : int -> int expr says the constructor takes an int and gives an int expr, while Bool : bool -> bool expr gives a bool expr. Add says both sub-terms must be int expr and the result is one too. If is parametric on the branches: bool expr * 'a expr * 'a expr -> 'a expr, so an If is an int expr when its branches are, and a bool expr when they are, but the condition is always a bool expr. The compiler used the indices to reject Add (Int 1, Bool true) with “This constructor has type bool expr but an expression was expected of type int expr”. No run-time check ever runs for that term, because no run time happens: the file does not compile. Step through what each sub-term’s type is:

Interactive · step by step Eval of If (Eq, Int 10, Int 20) Step through the typed evaluator. Each box is a sub-term; its sub-label is the type the GADT gives it, and the right column shows what eval returned.
Eval of If (Eq, Int 10, Int 20), step 5 of 5. Showing If (c, t, e), Eq (Int 1, Add _), Add (Int 0, Int 1), true, 1, Int 10, 10. In focus: 10.conditioninside Eqevaluates toevaluates tothen brancheval returnsIf (c, t, e)bool expr * 'a expr * 'a exprEq (Int 1, Add _)bool exprAdd (Int 0, Int 1)int exprtrueeval c1eval (Add …)Int 10int expr10eval (If …) : int

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

Every step as text
  1. Index set by the top. If (c, t, e) has type 'a expr: the index is a type variable because t and e share it. The branches that follow pin 'a to int, so this If is an int expr and its result is an int.
  2. Condition is a bool expr. Eq takes two int expr and gives a bool expr. The pattern | If (c, t, e) refines c to bool expr inside the match: the compiler now knows which arm of eval can run.
  3. Add is an int expr. Add : int expr * int expr -> int expr. Writing Add (Int 1, Bool true) is a compile-time error, and the type does not need a run-time check inside eval: it already holds.
  4. Each sub-eval returns. eval c : bool returned true, eval (Add (Int 0, Int 1)) : int returned 1. The | Eq (a, b) arm returned true because 1 = 1, so If takes its then branch.
  5. Result is an int. The then branch Int 10 is an int expr, so eval returned 10 as an int. The whole expression is typed int by the GADT, not by a cast at run time.

Key idea

A GADT puts a type on the result of each constructor, so pattern matching on a value whose index is int expr needs only the arms that return an int expr.

Pattern matching needs a locally abstract type

Matching a GADT asks the type-checker to refine the match’s own type at each arm. OCaml refuses to guess that refinement, so a plain let rec eval = function | Int n -> n | Bool b -> b | ... fails to compile with “This pattern matches values of type bool expr but a pattern was expected which matches values of type int expr. Type bool is not compatible with type int”. The compiler saw the first arm Int n -> n as pinning the input to int expr, and then rejected the Bool b -> b arm. The first session recorded this message when the file no_annot.ml was built.

The fix is 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. written on the function: let rec eval : type a. a expr -> a = function .... The a is a fresh type inside the function body, kept abstract so each arm can pin it: in | Int n -> n the compiler reads a = int, and in | Bool b -> b it reads a = bool. Each arm is type-checked against its own a, and the function has the general type 'a expr -> 'a outside. OCaml 5.5.1 printed that signature when the full typed-expr.ml was loaded with #use, and eval (If (Bool false, Int 10, Int 20)) returned the integer 20. The same shape works for a typed size, a printer or a serialiser over the GADT: the function is the only place that needs the annotation.

Checkpoint

Check: A constructor's return type refines the index · Locally abstract types in a match

  1. Question 1 of 2 What does ocamlc -c say about let _ = Add (Int 1, Bool true) when expr is declared with Int : int -> int expr | Bool : bool -> bool expr | Add : int expr * int expr -> int expr?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Error: This constructor has type bool expr but an expression was expected of type int expr

    The session built ill_typed.ml and ocamlc -c printed exactly that message, with Type bool is not compatible with type int on the next line.

  2. Question 2 of 2 In the typed evaluator, why does the compiler refuse let rec eval = function | Int n -> n | Bool b -> b | Add (a, b) -> eval a + eval b?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The first arm pins the input to int expr, so the Bool b arm cannot type-check

    The session recorded that message when it ran ocamlc -c no_annot.ml. Writing let rec eval : type a. a expr -> a = function … makes a a fresh type per arm, so Bool b -> b can pin a to bool while Int n -> n pins it to int.

An existential type packs a value with its printer

A GADT can also hide a type inside a constructor. The constructor introduces a type variable that no caller can name, called an existential type: A type variable a constructor introduces for its own fields that no caller can name, so values of different inner types share one visible type.; the value and anything that operates on it travel together. The classic shape is one visible type that holds mixed inner types:

One list, four inner types, four printers, all behind type showable Python · existential_session.py
"""One list of mixed values that each know how to print themselves: the existential type showable keeps the printer with the value."""

from session import files, run, toplevel, write

files("existential.ml")

run("""
ocaml existential.ml
""")

# At the toplevel: what the type looks like, and what cannot be done with an unpacked value.
toplevel("""
#use "existential.ml";;
List.length items;;
""")

# Trying to look at the inner value: the existential type variable 'a leaks out of its scope.
write("escape.ml", """type showable = Show : 'a * ('a -> string) -> showable
let get_value (Show (v, _)) = v
""")
run("""
!ocamlc -c escape.ml
""")

Output

$ ocaml existential.ml
42
"Thiruvananthapuram"
3.140
[1; 2; 3]
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# #use "existential.ml";;
type showable = Show : 'a * ('a -> string) -> showable
val items : showable list =
  [Show (<poly>, <fun>); Show (<poly>, <fun>); Show (<poly>, <fun>);
   Show (<poly>, <fun>)]
42
"Thiruvananthapuram"
3.140
[1; 2; 3]
# List.length items;;
- : int = 4
$ ocamlc -c escape.ml
File "escape.ml", line 2, characters 30-31:
2 | let get_value (Show (v, _)) = v
                                  ^
Error: The value v has type $a but an expression was expected of type 'a
       The type constructor $a would escape its scope
       Hint: $a is an existential type bound by the constructor Show.
[exit status 2]

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

type showable = Show : 'a * ('a -> string) -> showable reads as “a Show carries a value and a function that prints that value, agreeing on their type, and gives back a showable”. The 'a is bound by the constructor, so outside the box there is no 'a to name. The first list in the session held an int with string_of_int, a string with a quoting printer, a float with %.3f and an int list with a joiner, and List.iter ran the right printer on each entry. The toplevel showed each inner value as <poly> because its own printer does not know what to do with a hidden type.

Interactive · step by step Show hides the inner type Step through what Show (42, string_of_int) and Show ("…", quote) carry. The hidden type is marked 'a and never leaves the box.
Show hides the inner type, step 4 of 4. Showing Show : 'a * printer, Show (42, …), Show ("city", …), pp v : string, take v out?. In focus: take v out?.buildsbuildspp 42refusedShow : 'a * printerthe constructorShow (42, …)hidden 'a = intShow ("city", …)hidden 'a = stringpp v : stringthe one safe movetake v out?refused: $a escapes

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

Every step as text
  1. Two type variables inside. The declaration says Show takes a value and a printer that agree on their type, and gives back a showable. The 'a is bound by the constructor; outside, two Show values share the type showable even when their inner types differ.
  2. Two values of one type. Show (42, string_of_int) and Show ("Thiruvananthapuram", quote) each hide their 'a, int in the first and string in the second. The list items of showable list holds both; the inner types are gone from the type system's view.
  3. Printer always fits. The constructor was checked when the Show was built, so pp v is well typed on every element. The lesson's list printed 42, "Thiruvananthapuram", 3.140 and [1; 2; 3] with the four printers it carries.
  4. Taking the value out. let get_value (Show (v, _)) = v was refused with "The type constructor $a would escape its scope. Hint: $a is an existential type bound by the constructor Show." The value cannot leave the box with its type gone.

Trying to take the value back out, let get_value (Show (v, _)) = v, was refused with “The type constructor $a would escape its scope. Hint: $a is an existential type bound by the constructor Show.” The only safe move is to use the companion function inside the box: here pp v, which returns a string for every element whatever its hidden type was. That is often enough: a logger, a diff viewer, a form renderer or a (string * Yojson.t) list builder each wants a type back out, not the value itself.

Checkpoint

Check: Existential types and their one safe move

  1. Question 1 of 2 What is the type of [ Show (42, string_of_int); Show ("hi", fun s -> s); Show (3.14, Printf.sprintf "%.3f") ] after type showable = Show : 'a * ('a -> string) -> showable?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: showable list

    The hidden ''a lets a list of showables hold values of different inner types; the three printers run through List.iter (fun (Show (v, pp)) -> print_endline (pp v)) items.

  2. Question 2 of 2 What does ocamlc -c report for let get_value (Show (v, _)) = v?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The type constructor $a would escape its scope

    The session recorded that message. The only safe move is to use the companion inside the box, pp v, which has the fixed type string.

Where GADTs earn their keep, and where they do not

GADTs power two features of the 5.5 standard library that this track meets elsewhere, and they let you build a type-safe store where each key names its value’s type. Printf.lprintf reads its arguments as a heterogeneous list driven by its format string, so the type of the list is pinned by the format: the third session’s Printf.lprintf "%s %d %.02f %c\n" [ "ocaml"; 42; 3.14; 'c' ] printed ocaml 42 3.14 c, and a list whose types do not match the format would be a compile-time error. Effect declarations extend an open GADT: type _ Effect.t += Ask : string -> int Effect.t adds Ask with the result type int. A type-safe key-value store, like the one the fourth session builds, lets get Age return an int option and get Emails a string list option, one get for every type.

A heterogeneous list, Printf.lprintf and a mismatched use rejected Python · hlist_session.py
"""A heterogeneous list and the 5.5 Printf.Args list: a GADT lets one type describe lists whose cells have different types."""

from session import files, run, toplevel, write

files("hlist.ml")

run("""
ocaml hlist.ml
""")

toplevel("""
#use "hlist.ml";;
HCons (42, HNil);;
let h2 = HCons ("hi", HCons (true, HNil));;
length h2;;
""")

# A mismatched use rejected: a list whose type says (int * unit) hlist cannot be used where unit hlist is expected.
write("mismatch.ml", """type _ hlist = HNil : unit hlist | HCons : 'a * 'b hlist -> ('a * 'b) hlist
let h = HCons (42, HNil)
let bad : unit hlist = h
""")
run("""
!ocamlc -c mismatch.ml
""")

Output

$ ocaml hlist.ml
length = 3
Printf.lprintf gives: ocaml 42 3.14 c
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# #use "hlist.ml";;
type _ hlist = HNil : unit hlist | HCons : 'a * 'b hlist -> ('a * 'b) hlist
val length : 'a hlist -> int = <fun>
val h : (int * (string * (float * unit))) hlist =
  HCons (42, HCons ("ocaml", HCons (3.14, HNil)))
length = 3
Printf.lprintf gives: ocaml 42 3.14 c
# HCons (42, HNil);;
- : (int * unit) hlist = HCons (42, HNil)
# let h2 = HCons ("hi", HCons (true, HNil));;
val h2 : (string * (bool * unit)) hlist = HCons ("hi", HCons (true, HNil))
# length h2;;
- : int = 2
$ ocamlc -c mismatch.ml
File "mismatch.ml", line 3, characters 23-24:
3 | let bad : unit hlist = h
                           ^
Error: The value h has type (int * unit) hlist
       but an expression was expected of type unit hlist
       Type int * unit is not compatible with type unit
[exit status 2]

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

A key-value store whose key names the type of its value Python · kv_store_session.py
"""A GADT key-value store: each key names the type of its value, and the compiler types every get by the key passed."""

from session import files, run, toplevel

files("kv_store.ml")

run("""
ocaml kv_store.ml
""")

toplevel("""
#use "kv_store.ml";;
get Name store;;
get Age store;;
get Emails store;;
let n = (match get Age store with Some n -> n | None -> 0) + 1;;
""")

Output

$ ocaml kv_store.ml
name: Asha
age: 32
emails: asha@example.in, a2@example.in
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# #use "kv_store.ml";;
type _ key = Name : string key | Age : int key | Emails : string list key
type entry = Entry : 'a key * 'a -> entry
type (_, _) eq = Eq : ('a, 'a) eq
val key_eq : 'a key -> 'b key -> ('a, 'b) eq option = <fun>
val get : 'a key -> entry list -> 'a option = <fun>
val store : entry list =
  [Entry (Name, <poly>); Entry (Age, <poly>); Entry (Emails, <poly>)]
name: Asha
age: 32
emails: asha@example.in, a2@example.in
# get Name store;;
- : string option = Some "Asha"
# get Age store;;
- : int option = Some 32
# get Emails store;;
- : string list option = Some ["asha@example.in"; "a2@example.in"]
# let n = (match get Age store with Some n -> n | None -> 0) + 1;;
val n : int = 33

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

The hlist’s three elements lived under the type (int * (string * (float * unit))) hlist, and the mismatched use let bad : unit hlist = HCons (42, HNil) was refused with “Type int * unit is not compatible with type unit”. The lesson’s kv_store.ml kept three entries, one per key, and the toplevel typed get Age store as int option and get Emails store as string list option; the helper key_eq handles each key’s type by pattern matching. The hlist was loaded with #use on OCaml 5.5.1 and compiled under ocamlc -c to probe the mismatch.

Interactive · stage by stage Two kinds of GADT win Step through a plain variant with a run-time guard, and the two problems a GADT replaces: an impossible value and a mixed payload.
Two kinds of GADT win, step 6 of 6. Showing plain variant + assert, Failure at run time, GADT: type _ expr, existential Show, two wins. In focus: plain variant + assert.plain fits if uniformplain variant + assertmatch + run-time checkFailure at run timeassert fails for Bool trueGADT: type _ exprthe index refines the matchexistential Showone type, mixed payloadstwo winsrefusal + heterogeneous listuniform payloads

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

Every step as text
  1. The plain variant hole. A variant such as type expr = Int of int | Bool of bool | Add of expr * expr forces every Add arm to assert that its sub-terms carry integers. OCaml accepts assert on an int it cannot know is one; a bad sub-term raises at run time.
  2. The run-time refusal. eval (Add (Int 1, Bool true)) raises Failure "int in Add", because the assert runs when the match reaches it. A test or an unlucky input is what finds the bug, not the compiler.
  3. GADT moves the check up. type _ expr with Add : int expr * int expr -> int expr refuses Add (Int 1, Bool true) when the file compiles, with "This constructor has type bool expr but an expression was expected of type int expr". The arm Bool b -> b in a function without "type a." fails the same way.
  4. One type, many payloads. type showable = Show : 'a * ('a -> string) -> showable hides each element's own type behind one visible type. A list of mixed values can carry its own printers; no plain variant can list one Int and one String and one float together without naming them each.
  5. Two wins side by side. A GADT buys compile-time refusal of ill-typed terms, and existential constructors buy one type that lists values of different payload types. When neither question is in play, a plain variant stays simpler to read and faster to type.
  6. Plain is often enough. For an expression language whose sub-terms all carry the same payload, or a status variant such as Ok, Warn, Fail, no GADT is needed. Reach for the extra apparatus when the type checker could rule out a case or when payloads differ.

GADTs come with a cost. A type a. annotation is extra text, the error messages involve existential names such as $a, and small changes in the type index can push a function over the point where the compiler stops guessing. For a status variant such as Ok | Warn | Fail, or a tree whose nodes all carry the same payload, a plain variant stays simpler and faster to read. Reach for a GADT when the compiler could rule out a case for you, when payloads differ by type, or when the standard library already asks for one.

Checkpoint

Check: Where GADTs earn their keep

  1. One question Which problem is a GADT a good first answer to?

    Choose one answer.

    Show the answer: One question

    Answer: An expression language whose evaluator should refuse Add (Int 1, Bool true) at compile time

    Reach for a GADT when the compiler could rule out a case by its type, when payloads differ by type (an existential), or when the standard library already uses one, as Printf.Args and Effect.t do.

Practise: types that each constructor pins

The questions come from the three sessions: the typed evaluator and its two refusals, the showable existential, and the heterogeneous list with Printf.lprintf.

Practice that adapts to you

Practise this lesson

12 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 12 What does the colon in Int : int -> int expr announce?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: The constructor's own type: it takes an int and gives back an int expr

    Reading each constructor as a function of its arguments gives you the signatures the toplevel printed when typed-expr.ml was loaded with #use.

  2. Question 2 of 12 What type does the GADT give If (Bool true, Int 10, Int 20)?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: int expr

    The toplevel printed - : int = 20 for eval (If (Bool false, Int 10, Int 20)): the whole expression is an int, and eval returned 20 as one.

  3. Question 3 of 12 What does eval (Add (Int 2, Int 3)) return on OCaml 5.5.1?

    Type a number.

    Show the answer: Question 3 of 12

    Answer: 5 integer

    The toplevel printed - : int = 5. The GADT index of Add is int expr * int expr -> int expr, so eval adds two ints and returns an int.

  4. Question 4 of 12 What does the type a. in let rec eval : type a. a expr -> a = function … do?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: It introduces a as a locally abstract type: each arm can pin a to its own case's type

    The toplevel printed val eval : ''a expr -> ''a = <fun> after #use "typed-expr.ml". Removing the type a. part makes the compiler refuse the match on the Bool arm.

  5. Question 5 of 12 What is the first line of the error ocamlc -c no_annot.ml printed in the session?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: This pattern matches values of type bool expr but a pattern was expected which matches values of type int expr

    The session wrote no_annot.ml and ocamlc -c printed exactly that message. Adding the type a. annotation lets every arm pick its own a.

  6. Question 6 of 12 Why does the compiler refuse let get_value (Show (v, _)) = v?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: v has the existential type the constructor Show introduces, and that type cannot leave the match arm

    Inside the arm, the type system knows v agrees with its printer''s input; outside, there is no type to describe v on its own. Only functions that take v and return a type that does not mention it (string, int, Buffer.t, …) can leave the arm.

  7. Question 7 of 12 In the session's items, Show (3.14, Printf.sprintf "%.3f") printed which line when List.iter ran?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: 3.140

    The order of lines was 42, "Thiruvananthapuram", 3.140, [1; 2; 3]. Each inner value was printed by the function it carries, with no cast outside the box.

  8. Question 8 of 12 What can a function that takes a showable do with the hidden value?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: Call its companion function and return a type that does not mention the hidden one, such as string

    Design around what the box exposes: a to_string, a to_json or an append-to-buffer that each turns the hidden value into a fixed-type result.

  9. Question 9 of 12 In the lesson's hlist, what is the type of HCons (42, HCons ("ocaml", HCons (3.14, HNil)))?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: (int * (string * (float * unit))) hlist

    The toplevel printed val h : (int * (string * (float * unit))) hlist = HCons (42, HCons ("ocaml", HCons (3.14, HNil))). The lesson's hlist and the 5.5 Printf.Args are both GADTs that carry values of different types.

  10. Question 10 of 12 Printf.lprintf "%s %d %.02f %c\n" [ "ocaml"; 42; 3.14; 'c' ] printed what in the session?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: ocaml 42 3.14 c

    The format drives the list''s GADT type: a format with %s %d reads a (string -> int -> unit, unit) Printf.Args.t, where each element adds one argument type to the function index.

  11. Question 11 of 12 In the kv_store session, what is the printed type of get Emails store?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: string list option

    The toplevel printed - : string list option = Some ["asha@example.in"; "a2@example.in"]. One get, one type per key, no cast at the call site.

  12. Question 12 of 12 A status variant has three cases, each carrying a (code, message) pair, and no case needs to be ruled out at compile time. Which type fits?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: A plain variant: type status = Ok of int * string | Warn of int * string | Fail of int * string

    Reach for a GADT when a case can be ruled out by its type, when payloads differ by type, or when the standard library already asks for one. Otherwise a plain variant stays simpler and faster to read.

Interview questions

Warm-up (fresher to 2–5 years): what is a GADT, and how does it differ from a plain variant?

A GADT, or generalised algebraic data type, is a variant whose constructors can each have a different return type, written on the right of a colon: Int : int -> int expr and Bool : bool -> bool expr build values of two different index types that share the name expr. A plain variant forces every constructor into one type, so a match’s cases all return the same thing and the compiler cannot rule out a case by its payload. In the GADT, pattern matching on an int expr only type-checks arms that return an int expr, which lets eval be total without a run-time guard, and Add (Int 1, Bool true) is a compile-time error rather than a Failure from assert.

Two harder questions follow, about a typed evaluator whose match the compiler refuses, and about choosing between a GADT and a plain variant for an expression language that is growing. For each, say what the first error message reads like and what you would change to make it compile, then open its first step:

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

  1. Mid to senior

    You are modelling a filter clause with And, Or, Not and primitives such as Equals and Contains. Every primitive takes string arguments and every clause evaluates to a bool. A teammate wants a GADT; another argues for a plain variant. How would you decide?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. 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 is a GADT, and how does it differ from a plain variant?

    A variant whose constructors each name their own return type after a colon, such as Int : int -> int expr and Bool : bool -> bool expr. A plain variant puts every constructor in one type.

  2. What goes wrong when a tiny language's evaluator is built on an ordinary variant?

    Add has to assert that both sub-terms are integers and If that its condition is a boolean, so a bad input surfaces at run time as Failure "int in Add".

  3. What does Add : int expr * int expr -> int expr do to the term Add (Int 1, Bool true)?

    It makes the term a compile-time error instead of a run-time failure, and a match on an int expr needs only the arms that return an int expr.

  4. Why does a function over a GADT need type a. in let f : type a. a expr -> a = function …?

    The locally abstract type lets each arm refine a to the type its own constructor built.

  5. What does type showable = Show : 'a * ('a -> string) -> showable hide, and what may code do with it?

    It hides the type of the value, packed together with its printer; the only safe move is to call that printer on the value.

  6. Where does the OCaml 5.5 standard library itself use GADTs?

    Printf.lprintf reads a heterogeneous list driven by its format, and type _ Effect.t += Ask : string -> int Effect.t extends an open GADT.

Key takeaways

  • A GADT puts a type on each constructor’s return: Add : int expr * int expr -> int expr makes Add (Int 1, Bool true) a compile-time error, not a run-time failure.
  • A function that matches a GADT needs a locally abstract type, let f : type a. a expr -> a = function ..., so each arm can refine a to the type its constructor built.
  • An existential constructor hides a type variable inside the box: type showable = Show : 'a * ('a -> string) -> showable holds a value and its printer, and the only safe move is to call that printer.
  • The 5.5 standard library uses GADTs in a few places: Printf.lprintf reads a heterogeneous list driven by its format string, and type _ Effect.t += Ask : string -> int Effect.t extends an open GADT.

Terms in this lesson

existential type
A type variable a constructor introduces for its own fields that no caller can name, so values of different inner types share one visible type.
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.
heterogeneous list
A list whose elements may have different types, each recorded in the list's own type, as in the lesson's (string * (bool * unit)) hlist; OCaml 5.5's Printf.lprintf reads one whose types its format string fixes.
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.

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.