wiki

Algebraic data type

An algebraic data type is a type built from sums, where a value is one of several alternatives, and products, where a value has several components at once, possibly recursively. The name comes from counting: the number of values of a sum type is the sum of the numbers for its alternatives, and of a product the product, so types obey the laws of algebra. Values are taken apart by pattern matching, which the compiler checks for missing and unreachable cases.

§ 01

Overview

A product type holds several values at once: a pair int * string or a record { name : string; age : int }. A sum type holds one of several alternatives, each with its own data: Circle of float | Rect of float * float is a circle with a radius or a rectangle with two sides, never both. Combining the two, recursively, gives lists, trees, syntax trees, protocol messages and most of the data a program works with. In OCaml sums are called variants, and the alternatives constructors.

A recursive sum of products: arithmetic expressions, an evaluator, and a simplifier using nested patterns.

(* A recursive sum of products: arithmetic expressions. *)
type expr =
| Num of int
| Var of string
| Add of expr * expr
| Mul of expr * expr
| Neg of expr
let rec eval env = function
| Num n -> n
| Var x -> List.assoc x env
| Add (a, b) -> eval env a + eval env b
| Mul (a, b) -> eval env a * eval env b
| Neg a -> - eval env a
(* Nested patterns match on the shape of a whole subtree at once. *)
let rec simplify e =
match e with
| Add (a, b) -> (
match (simplify a, simplify b) with
| Num 0, x | x, Num 0 -> x
| Num x, Num y -> Num (x + y)
| a, b -> Add (a, b))
| Mul (a, b) -> (
match (simplify a, simplify b) with
| Num 0, _ | _, Num 0 -> Num 0
| Num 1, x | x, Num 1 -> x
| Num x, Num y -> Num (x * y)
| a, b -> Mul (a, b))
| Neg a -> (match simplify a with Neg b -> b | Num n -> Num (-n) | b -> Neg b)
| Num _ | Var _ -> e
let rec show = function
| Num n -> string_of_int n
| Var x -> x
| Add (a, b) -> "(" ^ show a ^ " + " ^ show b ^ ")"
| Mul (a, b) -> show a ^ " * " ^ show b
| Neg a -> "-" ^ show a

Running it.

e = (1 * x + (2 + 3) * --y)
simplify e = (x + 5 * y)
eval [x=4; y=10] e = 54

Each function is a match with one case per constructor, and simplify matches on the shape of two subtrees at once. The same definition in an object-oriented language would be a class hierarchy with one subclass per constructor and one method per operation.

§ 02

Sums and products

Writing for the number of values of a type:

where is the unit type, with one value, and the empty type, with none. bool is , 'a option is , and result is a sum of the success and error types. The counts can be checked by listing every value:

Enumerating the values of small types, including function types as tables.

(* Enumerate every value of a few small types and count them. *)
let bools = [ false; true ]
let units = [ () ]
let options xs = None :: List.map Option.some xs
let pairs xs ys = List.concat_map (fun x -> List.map (fun y -> (x, y)) ys) xs
let eithers xs ys = List.map Either.left xs @ List.map Either.right ys
(* A function from a finite type is its table of outputs, one per input. *)
let rec functions dom cod =
match dom with
| [] -> [ [] ]
| _ :: rest -> List.concat_map (fun y -> List.map (fun t -> y :: t) (functions rest cod)) cod

Running it.

unit 1 1
bool 2 1 + 1 = 2
bool option 3 1 + 2 = 3
bool * bool 4 2 * 2 = 4
(bool, unit) Either.t 3 2 + 1 = 3
bool option * bool 6 3 * 2 = 6
bool -> bool option 9 3 ^ 2 = 9
bool option -> bool 8 2 ^ 3 = 8

Function types are the exponentials: a function from a type with three values to one with two is a table of three entries, each with two choices.

§ 03

The algebra of types

The laws of arithmetic hold for types up to isomorphism, where each law is witnessed by a pair of functions inverse to each other:

Three of the laws as pairs of functions, with the round trips checked.

(* The laws of arithmetic hold for types up to isomorphism: each law is a
pair of functions that are inverse to each other. *)
(* a * (b + c) = a * b + a * c *)
let distribute (a, e) = match e with Either.Left b -> Either.Left (a, b) | Either.Right c -> Either.Right (a, c)
let factor = function Either.Left (a, b) -> (a, Either.Left b) | Either.Right (a, c) -> (a, Either.Right c)
(* 1 + 1 = 2 *)
let to_bool = function Either.Left () -> false | Either.Right () -> true
let of_bool b = if b then Either.Right () else Either.Left ()
(* a^(b + c) = a^b * a^c: a function out of a sum is a pair of functions. *)
let split f = ((fun b -> f (Either.Left b)), fun c -> f (Either.Right c))
let join (g, h) = function Either.Left b -> g b | Either.Right c -> h c

Running it.

factor (distribute v) = v -> true
to_bool (of_bool b) = b -> true
join (split f) agrees with f -> true

The exponential laws say that a function out of a sum is a pair of functions, one per alternative, which is what a pattern match is, and that a function of a pair is a function returning a function, which is currying.

Recursive types

A recursive type is a fixed point of an equation. Lists satisfy , and unfolding the equation gives

a list of length 0, 1, 2 and so on. Binary trees satisfy , and the coefficient of in the solution is the th Catalan number, the number of tree shapes with nodes. Formal manipulations of these power series are meaningful: the derivative is the type of lists with one element removed, and in general the derivative of a type is its type of one-hole contexts[7], which is the zipper.

§ 04

Pattern matching

A pattern match takes a value apart by its constructor and binds the components. Because the constructors of a sum are known, the compiler can check that a match handles all of them, and that no case is unreachable[4]:

A match that forgets a constructor, and one with a case that can never be reached.

type connection =
| Disconnected
| Connecting of { attempt : int }
| Connected of { address : string }
let is_up = function
| Connected _ -> true
| Disconnected -> false
let label = function
| Disconnected -> "down"
| Connecting _ -> "retrying"
| Connected _ -> "up"
| Connecting { attempt = 1 } -> "first try"

The compiler's warnings.

File "d4_warn.ml", lines 6-8, characters 12-25:
6 | ............function
7 | | Connected _ -> true
8 | | Disconnected -> false
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
Connecting _
File "d4_warn.ml", line 14, characters 4-30:
14 | | Connecting { attempt = 1 } -> "first try"
^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning 11 [redundant-case]: this match case is unused.

The first warning is the practical benefit of sum types. When a constructor is added to a type, every match that does not handle it is reported, so the compiler lists the code that needs updating. Matches are compiled into decision trees or backtracking automata that test each part of the value once[5].

Illegal states

A record of optional fields, such as an address that is present only when connected, can represent combinations that should never happen: an address while disconnected, or a retry count while connected. A sum type with one constructor per state, each carrying only the data that exists in that state, cannot. "Make illegal states unrepresentable" is the usual summary[8].

§ 05

Representation

A sum type is a tagged union: a value records which alternative it is and holds that alternative's data. In OCaml, constructors without arguments are stored as immediate integers numbered in order, and constructors with arguments as heap blocks whose header tag is the constructor's index among the non-constant ones:

Inspecting the runtime representation of a variant.

(* How OCaml stores a variant: constructors without arguments are
immediate integers, numbered in order; constructors with arguments are
heap blocks whose tag is their index among the non-constant ones. *)
type shape = Point | Circle of float | Rect of float * float | Empty
let describe v =
let r = Obj.repr v in
if Obj.is_int r then Printf.sprintf "immediate %d" (Obj.obj r : int)
else Printf.sprintf "block, tag %d, %d field(s)" (Obj.tag r) (Obj.size r)

Running it.

Point immediate 0
Empty immediate 1
Circle 1. block, tag 0, 1 field(s)
Rect (2., 3.) block, tag 1, 2 field(s)

A match therefore tests whether the value is an immediate or a block, and then its integer or tag, often through a jump table. In C the same structure is written by hand, as a struct with an enum field and a union, and nothing checks that the right member is read. Rust's enums are tagged unions checked by the compiler, and use unused bit patterns of the payload as the tag where possible, so Option<&T> is the size of a pointer.

§ 06

Relation to other features

GADTs let each constructor refine the type parameter, so a type can carry invariants about the value. Polymorphic variants are sums whose constructors exist independently of any declaration. Church and Scott encodings represent algebraic data as functions: the Church encoding of a value is its fold, and the Scott encoding its pattern match.

Algebraic data types and class hierarchies split the same table in opposite directions. With a sum type, adding an operation is one new function and adding a case means editing every existing match; with a class hierarchy, adding a case is one new class and adding an operation means editing every class. Wadler called the problem of doing both without editing existing code the expression problem[6].

§ 07

In programming languages

Sum and product types with pattern matching are central in ML, OCaml, Haskell, F#, Rust (enums), Swift (enums with associated values) and Scala (sealed traits with case classes). Kotlin has sealed classes, TypeScript discriminated unions of object types with a common tag field, and Java sealed interfaces with records and pattern matching in switch.

§ 08

History

Burstall introduced data types defined by constructors with pattern matching on them in the language NPL in 1977[2], and the language HOPE of 1980 gave them the form used since[1]. They were adopted by Standard ML, Miranda and Haskell, and, much later, by mainstream languages. The type-theoretic account, with sums, products and recursive types as fixed points, is in Pierce's textbook[3].

see also

further reading

  1. [1]R. M. Burstall, D. B. MacQueen, D. T. Sannella, “HOPE: an experimental applicative language”, LISP Conference (1980).
  2. [2]R. M. Burstall, “Design considerations for a functional programming language”, Infotech State of the Art Conference (1977).
  3. [3]B. C. Pierce, Types and Programming Languages, ch. 11 and 20, MIT Press (2002).
  4. [4]L. Maranget, “Warnings for pattern matching”, Journal of Functional Programming 17 (2007).
  5. [5]L. Augustsson, “Compiling pattern matching”, FPCA (1985).
  6. [6]P. Wadler, “The expression problem”, message to the java-genericity mailing list (1998).
  7. [7]C. McBride, “The derivative of a regular type is its type of one-hole contexts” (2001).
  8. [8]Y. Minsky, “Effective ML”, lecture at Harvard (2010); the maxim “make illegal states unrepresentable”.

last updated