Types

Sum Types

i
-- A sum type that can hold either a String or an Int
inductive StringOrInt where
  | str : String  StringOrInt
  | num : Int  StringOrInt
deriving Repr

-- Construct a value of each kind
def greeting : StringOrInt := StringOrInt.str "hello"
def answer : StringOrInt := StringOrInt.num 42

#eval greeting
#eval answer

-- A list can hold a mix of both
def mixed : List StringOrInt := [
  StringOrInt.str "hello",
  StringOrInt.num 42,
  StringOrInt.str "world"
]

#eval mixed

-- Use pattern matching to extract the value
def describe (x : StringOrInt) : String :=
  match x with
  | StringOrInt.str s => s!"string: \"{s}\""
  | StringOrInt.num n => s!"number: {n}"

#eval describe greeting
#eval describe answer
i
StringOrInt.str "hello"
StringOrInt.num 42
[StringOrInt.str "hello", StringOrInt.num 42, StringOrInt.str "world"]
"string: \"hello\""
"number: 42"

Trees

i
-- A binary tree with strings at the leaves
inductive Tree where
  | leaf : String  Tree
  | node : Tree  Tree  Tree
deriving Repr

-- Build a tree
def myTree : Tree :=
  Tree.node
    (Tree.node
      (Tree.leaf "alpha")
      (Tree.leaf "beta"))
    (Tree.leaf "gamma")

#eval myTree

-- Calculate total length of all strings in the tree
def totalLength (t : Tree) : Nat :=
  match t with
  | Tree.leaf s => s.length
  | Tree.node left right => totalLength left + totalLength right

#eval totalLength myTree
i
Tree.node (Tree.node (Tree.leaf "alpha") (Tree.leaf "beta")) (Tree.leaf "gamma")
14

Polymorphism

i
-- A polymorphic binary tree: leaves can be any type α
inductive Tree (α : Type) where
  | leaf : α  Tree α
  | node : Tree α  Tree α  Tree α
deriving Repr

-- A function that works for trees of any type
def countLeaves (t : Tree α) : Nat :=
  match t with
  | Tree.leaf _ => 1
  | Tree.node left right => countLeaves left + countLeaves right

-- A tree of numbers
def numTree : Tree Nat :=
  Tree.node
    (Tree.node
      (Tree.leaf 1)
      (Tree.leaf 2))
    (Tree.leaf 3)

#eval numTree
#eval countLeaves numTree
i
Tree.node (Tree.node (Tree.leaf 1) (Tree.leaf 2)) (Tree.leaf 3)
3

Enumerations

i
-- A sum type with no data: just labels
inductive Season where
  | spring | summer | fall | winter
deriving BEq, Repr

#eval Season.summer
#eval Season.summer == Season.winter
#eval Season.summer == Season.summer
i
Season.summer
false
true

Sum Types with Data

i
-- A sum type where each constructor carries different data
inductive Shape where
  | circle (radius : Float)
  | rect (width : Float) (height : Float)
deriving Repr

def s1 : Shape := Shape.circle 3.0
def s2 : Shape := Shape.rect 2.0 5.0

#eval s1
#eval s2
i
Shape.circle 3.000000
Shape.rect 2.000000 5.000000

Pattern Matching on Sum Types

i
-- Pattern matching must handle every constructor
inductive Shape where
  | circle (radius : Float)
  | rect (width : Float) (height : Float)

def area : Shape  Float
  | Shape.circle r => 3.14159 * r * r
  | Shape.rect w h => w * h

#eval area (Shape.circle 3.0)
#eval area (Shape.rect 2.0 5.0)
i
28.274310
10.000000

deriving Explained

i
-- 'deriving' generates boilerplate for common type classes
inductive Color where
  | red | green | blue
deriving BEq, Repr

-- 'deriving BEq' lets us compare values with ==
#eval Color.red == Color.green
#eval Color.red == Color.red

-- 'deriving Repr' makes #eval display values readably
#eval Color.blue
i
false
true
Color.blue

Option is a Sum Type

i
-- Option is defined in the standard library as:
-- inductive Option (α : Type) where
--   | none : Option α
--   | some (val : α) : Option α

-- So you can pattern match on it like any other sum type
def describeOpt (x : Option Int) : String :=
  match x with
  | Option.none => "nothing"
  | Option.some n => s!"got {n}"

#eval describeOpt (Option.some 42)
#eval describeOpt Option.none
i
"got 42"
"nothing"

List is a Recursive Inductive Type

i
-- List is defined in the standard library as:
-- inductive List (α : Type) where
--   | nil : List α
--   | cons (head : α) (tail : List α) : List α

-- [] is shorthand for List.nil
-- head :: tail is shorthand for List.cons head tail

#check []
#check 1 :: [2, 3]
#check List.cons 1 (List.cons 2 (List.cons 3 List.nil))
i
[] : List ?m.1
[1, 2, 3] : List Nat
[1, 2, 3] : List Nat

Computing on Inductive Types

i
-- A tiny expression language: numbers, addition, multiplication
inductive Expr where
  | num (n : Int)
  | add (left : Expr) (right : Expr)
  | mul (left : Expr) (right : Expr)
deriving Repr

-- Evaluate an expression by recursing on its structure
def eval : Expr  Int
  | Expr.num n => n
  | Expr.add l r => eval l + eval r
  | Expr.mul l r => eval l * eval r

def e : Expr := Expr.add (Expr.num 3) (Expr.mul (Expr.num 4) (Expr.num 5))
#eval e
#eval eval e
i
Expr.add (Expr.num 3) (Expr.mul (Expr.num 4) (Expr.num 5))
23

Generic Sum Types

i
-- A generic sum type for success or failure
inductive Result (α : Type) (β : Type) where
  | ok (val : α)
  | err (msg : β)
deriving Repr

def divide (a : Int) (b : Int) : Result Int String :=
  if b == 0 then
    Result.err "division by zero"
  else
    Result.ok (a / b)

#eval divide 10 2
#eval divide 10 0
i
Result.ok 5
Result.err "division by zero"

match with Conditions

i
-- Use conditionals inside match branches when constructors aren't enough
def describe (n : Int) : String :=
  match n with
  | n' =>
    if n' > 0 then "positive"
    else if n' < 0 then "negative"
    else "zero"

#eval describe 5
#eval describe (-3)
#eval describe 0
i
"positive"
"negative"
"zero"

Nested Patterns

i
-- Match on nested types: destructure an Option containing a List
def describeOptList (x : Option (List Int)) : String :=
  match x with
  | Option.none => "nothing"
  | Option.some [] => "empty list"
  | Option.some [n] => s!"singleton: {n}"
  | Option.some _ => "longer list"

#eval describeOptList Option.none
#eval describeOptList (Option.some [])
#eval describeOptList (Option.some [7])
#eval describeOptList (Option.some [1, 2, 3])
i
"nothing"
"empty list"
"singleton: 7"
"longer list"

Product Types are Inductive

i
-- A structure groups related values under one name
structure Point where
  x : Float
  y : Float
deriving Repr

-- A product type is really an inductive with a single constructor
inductive Point' where
  | mk (x : Float) (y : Float)
deriving Repr

def p1 : Point := { x := 1.0, y := 2.0 }
def p2 : Point' := Point'.mk 1.0 2.0

#eval p1
#eval p2
i
{ x := 1.000000, y := 2.000000 }
Point'.mk 1.000000 2.000000

A Command Interpreter

i
-- A tiny command language in three lines of type definition
inductive Cmd where
  | forward (steps : Nat)
  | turn (degrees : Int)
  | say (msg : String)
deriving Repr

-- Pattern matching plus recursion equals an interpreter
def run (cmds : List Cmd) : String :=
  match cmds with
  | [] => "done"
  | Cmd.say msg :: rest => s!"said '{msg}'; {run rest}"
  | Cmd.forward n :: rest => s!"moved {n}; {run rest}"
  | Cmd.turn d :: rest => s!"turned {d}°; {run rest}"

def program : List Cmd := [
  Cmd.forward 3,
  Cmd.say "hello",
  Cmd.turn 90
]

#eval run program
i
"moved 3; said 'hello'; turned 90°; done"

Exercises

Fix: Missing Constructor in Match

i
-- This function should describe a StringOrInt, but the match
-- is missing a case. Fix it.
inductive StringOrInt where
  | str : String  StringOrInt
  | num : Int  StringOrInt

def describe (x : StringOrInt) : String :=
  match x with
  | StringOrInt.str s => s!"string: \"{s}\""
hint
  • The describe function only handles StringOrInt.str but not StringOrInt.num
  • Add a branch for the num constructor that formats the integer value

Fix: Missing deriving Clause

i
-- Comparing Color values should work, but the code won't compile.
-- Fix it by adding a `deriving` clause.
inductive Color where
  | red | green | blue

def isRed (c : Color) : Bool :=
  c == Color.red
hint
  • Color is missing a deriving clause
  • The == operator needs a BEq instance to compare values
  • Add deriving BEq (and Repr wouldn't hurt) to the inductive line

Fix: Missing Base Case in Recursion

i
-- This function should sum all numbers in a tree, but the match
-- is missing the leaf case. Fix it.
inductive IntTree where
  | leaf : Int  IntTree
  | node : IntTree  IntTree  IntTree

def totalSum (t : IntTree) : Int :=
  match t with
  | IntTree.node left right => totalSum left + totalSum right
hint
  • totalSum handles IntTree.node but not IntTree.leaf
  • The compiler rejects incomplete pattern matches on inductive types
  • Add a branch for IntTree.leaf n that returns n

Fix: Unhandled Error Case

i
-- This function should describe both success and failure,
-- but the match only handles the ok case. Fix it.
inductive Result (α : Type) (β : Type) where
  | ok (val : α)
  | err (msg : β)

def describe (r : Result Int String) : String :=
  match r with
  | Result.ok n => s!"success: {n}"
hint
  • describe only matches Result.ok but not Result.err
  • The compiler checks that every constructor of a sum type is handled
  • Add a branch for Result.err msg that formats the error message

Fix: Missing Case in eval

i
-- This eval function should handle all expression types,
-- but the mul case is missing. Fix it.
inductive Expr where
  | num (n : Int)
  | add (left : Expr) (right : Expr)
  | mul (left : Expr) (right : Expr)

def eval : Expr  Int
  | Expr.num n => n
  | Expr.add l r => eval l + eval r
hint
  • eval handles Expr.num and Expr.add but not Expr.mul
  • The compiler reports which constructors are missing
  • Add a line for Expr.mul l r that calls eval on both sides and multiplies

Fix: Wrong Return Type

i
-- Count the leaves in a polymorphic tree. The return type
-- annotation is wrong. Fix it.
inductive Tree (α : Type) where
  | leaf : α  Tree α
  | node : Tree α  Tree α  Tree α

def countLeaves (t : Tree α) : String :=
  match t with
  | Tree.leaf _ => 1
  | Tree.node left right => countLeaves left + countLeaves right
hint
  • The countLeaves function body returns Nat values (1, + on Nat)
  • But the type annotation says String
  • Fix the return type annotation to match what the body actually computes

Write: Weekday or Weekend

i
-- Write a function 'isWeekend' that returns true for Saturday and Sunday.
inductive Day where
  | monday | tuesday | wednesday | thursday | friday | saturday | sunday
deriving BEq, Repr

def isWeekend (d : Day) : Bool :=
  false

#guard isWeekend Day.saturday == true
#guard isWeekend Day.sunday == true
#guard isWeekend Day.monday == false
hint
  • Use match on the Day argument to check which day it is
  • Return true for Day.saturday and Day.sunday, false for everything else
  • Use _ as a catch-all for the five weekdays to keep the code short

Write: Tree Height

i
-- Write a function 'height' that computes the height of a Tree.
-- The height of a leaf is 0; a node's height is 1 + max of children.
inductive Tree (α : Type) where
  | leaf : α  Tree α
  | node : Tree α  Tree α  Tree α
deriving Repr

def height (t : Tree α) : Nat :=
  0

#guard height (Tree.leaf "x") == 0
#guard height (Tree.node (Tree.leaf "a") (Tree.leaf "b")) == 1
#guard height (Tree.node (Tree.node (Tree.leaf "a") (Tree.leaf "b")) (Tree.leaf "c")) == 2
hint
  • Use match t with to handle the two constructors: Tree.leaf and Tree.node
  • A leaf has height 0; a node's height is 1 plus the larger of its children
  • Use Nat.max to compare two Nat values

Write: Map from Scratch

i
-- Write 'myMap' that works like List.map, but using the inductive
-- List constructors directly (List.nil and List.cons) instead of
-- the [] and :: shorthand. This reveals what's really happening.
def myMap (f : α  β) (xs : List α) : List β :=
  []

#guard myMap (fun x => x + 1) [] == []
#guard myMap (fun x => x + 1) [1, 2, 3] == [2, 3, 4]
hint
  • Use match on the list with the inductive constructors: List.nil and List.cons
  • List.nil is the empty list; List.cons h t puts element h at the front of list t
  • In the cons case, apply f to the head and recurse on the tail

Write: Safe Division

i
-- Write 'safeDiv' that returns an Option Int:
-- Option.some of the quotient if b ≠ 0, Option.none otherwise.
def safeDiv (a : Int) (b : Int) : Option Int :=
  Option.none

#guard safeDiv 10 2 == Option.some 5
#guard safeDiv 10 0 == Option.none
#guard safeDiv 0 5 == Option.some 0
hint
  • Check if b is zero using an if/else expression
  • Return Option.none when b == 0, Option.some (a / b) otherwise
  • This is like Python's None but the compiler forces callers to handle it

Write: Mirror a Tree

i
-- Write 'mirrorTree' that swaps left and right subtrees of every node.
-- A leaf is unchanged.
inductive Tree (α : Type) where
  | leaf : α  Tree α
  | node : Tree α  Tree α  Tree α
deriving BEq, Repr

def mirrorTree (t : Tree α) : Tree α :=
  t

#guard mirrorTree (Tree.leaf 1) == Tree.leaf 1
#guard mirrorTree (Tree.node (Tree.leaf "a") (Tree.leaf "b")) == Tree.node (Tree.leaf "b") (Tree.leaf "a")
hint
  • Use match t with to distinguish Tree.leaf from Tree.node
  • A leaf is unchanged: return it as-is
  • For a node, create a new node with left and right subtrees swapped using recursion

Write: Get or Else

i
-- Write 'getOrElse' that returns the value inside an Option if it's 'some',
-- or a default value if it's 'none'.
def getOrElse (opt : Option α) (default : α) : α :=
  default

#guard getOrElse (Option.some 5) 0 == 5
#guard getOrElse (Option.none : Option Int) 0 == 0
#guard getOrElse (Option.some "hello") "world" == "hello"
hint
  • Use match opt with to handle Option.some and Option.none
  • Option.some val means return val (ignore the default)
  • Option.none means return the default argument