Type Classes

Defining a Type Class

i
-- Define a type class with one required method
class Describable (α : Type) where
  describe : α  String

Writing an Instance

i
-- An instance teaches Lean how to describe a specific type
instance : Describable Bool where
  describe b := if b then "yes" else "no"
i
instance : Describable Int where
  describe n := s!"the number {n}"
i
#eval Describable.describe true
#eval Describable.describe (42 : Int)
i
"yes"
"the number 42"

Polymorphic Functions

i
-- [Describable α] is a constraint: α must have a Describable instance
def showAll [Describable α] (xs : List α) : String :=
  String.intercalate ", " (xs.map Describable.describe)
i
#eval showAll [true, false, true]
#eval showAll [(1 : Int), 2, 3]
#eval showAll ["hello", "world"]
i
"yes, no, yes"
"the number 1, the number 2, the number 3"
"'hello', 'world'"

Multiple Type Classes

i
-- Two independent type classes
class Describable (α : Type) where
  describe : α  String

class Weighable (α : Type) where
  weight : α  Nat
i
inductive Fruit where | Apple | Banana | Cherry
  deriving Repr

instance : Describable Fruit where
  describe
    | Fruit.Apple  => "apple"
    | Fruit.Banana => "banana"
    | Fruit.Cherry => "cherry"

instance : Weighable Fruit where
  weight
    | Fruit.Apple  => 180
    | Fruit.Banana => 120
    | Fruit.Cherry => 5
i
-- A function can require multiple type class constraints
def report [Describable α] [Weighable α] (x : α) : String :=
  s!"{Describable.describe x} weighs {Weighable.weight x}g"
i
#eval report Fruit.Apple
#eval report Fruit.Banana
#eval report Fruit.Cherry
i
"apple weighs 180g"
"banana weighs 120g"
"cherry weighs 5g"

Extending Type Classes

i
-- extends inherits all methods of the parent class
class Describable (α : Type) where
  describe : α  String

class Verbose (α : Type) extends Describable α where
  verboseDescribe : α  String
i
-- An instance of Verbose must provide both methods
instance : Verbose Int where
  describe n      := s!"{n}"
  verboseDescribe n :=
    if n < 0 then s!"negative: {n}"
    else if n == 0 then "zero"
    else s!"positive: {n}"
i
-- Any function expecting Describable also accepts Verbose
def label [Describable α] (x : α) : String :=
  s!"[{Describable.describe x}]"

#eval label (42 : Int)
#eval label (-7 : Int)
#eval Verbose.verboseDescribe (0 : Int)
#eval Verbose.verboseDescribe (5 : Int)
i
"[42]"
"[-7]"
"zero"
"positive: 5"

deriving Revisited

i
-- Repr and BEq are type classes just like any user-defined class
-- deriving Repr generates an instance of the Repr class automatically
inductive Color where
  | Red | Green | Blue
  deriving Repr, BEq
i
-- Some classes cannot be derived — you write the instance by hand
class Describable (α : Type) where
  describe : α  String

instance : Describable Color where
  describe
    | Color.Red   => "red"
    | Color.Green => "green"
    | Color.Blue  => "blue"
i
-- Repr (from deriving) drives #eval output
#eval Color.Red
-- BEq (from deriving) enables ==
#guard Color.Red == Color.Red
#guard Color.Red != Color.Blue
-- Describable (written by hand) gives a custom string
#eval Describable.describe Color.Green
i
Color.Red
"green"

Exercises

Fix: Missing String Instance

i
-- Call showAll on a list of strings, but String has no Describable instance.
-- Add the missing instance so the guards pass.
class Describable (α : Type) where
  describe : α  String

instance : Describable Bool where
  describe b := if b then "yes" else "no"

instance : Describable Int where
  describe n := s!"the number {n}"

-- BUG: no instance for String, so showAll ["a", "b"] fails
def showAll [Describable α] (xs : List α) : String :=
  String.intercalate ", " (xs.map Describable.describe)

#guard showAll [true, false] == "yes, no"
#guard showAll [(1 : Int), 2] == "the number 1, the number 2"
#guard showAll ["a", "b"] == "a, b"
hint
  • showAll ["a", "b"] fails because there is no Describable String instance
  • Add an instance that returns the string itself: describe s := s
  • Like Python's __str__ for built-in types

Fix: Wrong Method Return Type

i
-- The Describable instance for Nat has the wrong method signature.
-- describe should return String, but this one returns Nat. Fix it.
class Describable (α : Type) where
  describe : α  String

-- BUG: describe returns Nat instead of String
instance : Describable Nat where
  describe n := n

#guard Describable.describe (0 : Nat) == "0"
#guard Describable.describe (42 : Nat) == "42"
hint
  • The class declares describe : α → String but the instance returns Nat
  • The return type must match the class declaration
  • Use s!"{n}" to convert the Nat to a String

Fix: Missing Type Class Constraint

i
-- report requires both Describable and Weighable constraints, but the
-- function signature only lists Describable. Add the missing constraint.
class Describable (α : Type) where
  describe : α  String

class Weighable (α : Type) where
  weight : α  Nat

inductive Fruit where | Apple | Banana
  deriving Repr

instance : Describable Fruit where
  describe | .Apple => "apple" | .Banana => "banana"

instance : Weighable Fruit where
  weight | .Apple => 180 | .Banana => 120

-- BUG: [Weighable α] constraint is missing
def report [Describable α] (x : α) : String :=
  s!"{Describable.describe x} weighs {Weighable.weight x}g"

#guard report Fruit.Apple == "apple weighs 180g"
#guard report Fruit.Banana == "banana weighs 120g"
hint
  • report uses Weighable.weight but the signature only lists [Describable α]
  • The compiler can't find the Weighable instance because you didn't ask for it
  • Add [Weighable α] as a second constraint in the square brackets

Fix: Missing Inherited Method

i
-- Verbose extends Describable, so an instance must provide both methods.
-- This instance only provides verboseDescribe. Add the missing method.
class Describable (α : Type) where
  describe : α  String

class Verbose (α : Type) extends Describable α where
  verboseDescribe : α  String

-- BUG: missing the inherited `describe` method
instance : Verbose Int where
  verboseDescribe n :=
    if n < 0 then s!"negative: {n}"
    else if n == 0 then "zero"
    else s!"positive: {n}"

#guard Describable.describe (42 : Int) == "42"
#guard Verbose.verboseDescribe (42 : Int) == "positive: 42"
#guard Verbose.verboseDescribe (-3 : Int) == "negative: -3"
hint
  • Verbose extends Describable, so the instance must provide describe AND verboseDescribe
  • The compiler error says which method is missing
  • Add describe n := s!"{n}" to the instance body

Fix: deriving Needs Parent Instances

i
-- trying to derive Repr on a type that uses a field without a Repr instance.
-- Repr can't be derived automatically here. Fix the error.
inductive Toy where
  | car
  | doll
  deriving Repr

-- BUG: MyBox can't derive Repr because Toy doesn't derive Repr
-- The error chain is: MyBox → needs Repr → Toy doesn't have it → fail
structure MyBox where
  toy : Toy
  label : String
  deriving Repr

#guard ({ toy := Toy.car, label := "red" : MyBox }).label == "red"
hint
  • MyBox tries to derive Repr, but its field toy : Toy needs Repr too
  • deriving chains: if a type contains another type, both must derive the class
  • Add deriving Repr to the Toy definition as well

Write: A Summarizable Class

i
-- Write a Summarizable type class with a `summary` method that returns
-- a one-line summary of a value. Then write an instance for String that
-- returns the first 10 characters followed by "..." if the string is longer.

class Summarizable (α : Type) where
  summary : α  String

-- TODO: write an instance for String

#guard Summarizable.summary "hello" == "hello"
#guard Summarizable.summary "hello world this is long" == "hello worl..."
#guard Summarizable.summary "" == ""
hint
  • Define instance : Summarizable String where …
  • If the string is 10 chars or shorter, return it as-is
  • Otherwise return s.extract 0 10 ++ "..." — use String.extract to slice

Write: A Doubleable Class

i
-- Write a Doubleable type class with a `twice` method that returns
-- the doubled value. Then write an instance for Nat.

class Doubleable (α : Type) where
  twice : α  α

-- TODO: write an instance for Nat

#guard Doubleable.twice (0 : Nat) == 0
#guard Doubleable.twice (21 : Nat) == 42
#guard Doubleable.twice (100 : Nat) == 200
hint
  • Define instance : Doubleable Nat where …
  • The twice method should multiply by 2: twice n := n * 2
  • Simple arithmetic: this is the "hello world" of type class instances

Write: A Function with Two Constraints

i
-- Write a function `describeWeight` that has BOTH Describable and Weighable
-- constraints and returns a string like "an apple weighs 180g".

class Describable (α : Type) where
  describe : α  String

class Weighable (α : Type) where
  weight : α  Nat

inductive Fruit where | Apple | Banana
  deriving Repr

instance : Describable Fruit where
  describe | .Apple => "an apple" | .Banana => "a banana"

instance : Weighable Fruit where
  weight | .Apple => 180 | .Banana => 120

-- TODO: write describeWeight with both constraints
def describeWeight (x : Fruit) : String :=
  ""

#guard describeWeight Fruit.Apple == "an apple weighs 180g"
#guard describeWeight Fruit.Banana == "a banana weighs 120g"
hint
  • Change the signature to def describeWeight [Describable α] [Weighable α] (x : α) : String
  • Use Describable.describe x for the name and Weighable.weight x for the weight
  • Format with s!"{...} weighs {...}g"

Write: Extend a Class

i
-- Extend the Measurable class with a Comparable class that adds
-- a `biggerThan` method. Write an instance for Nat.

class Measurable (α : Type) where
  size : α  Nat

class Comparable (α : Type) extends Measurable α where
  biggerThan : α  α  Bool

-- TODO: write an instance of Comparable for Nat

#guard Measurable.size (42 : Nat) == 42
#guard Comparable.biggerThan (10 : Nat) (5 : Nat) == true
#guard Comparable.biggerThan (3 : Nat) (7 : Nat) == false
hint
  • Write instance : Comparable Nat where …
  • size n := n — the size of a Nat is itself
  • biggerThan a b := a > b — use the built-in > operator
  • Both methods must be provided; Comparable inherits size from Measurable

Write: A Manual Display Instance

i
-- Instead of deriving Repr, write a manual Display instance for the
-- TrafficLight type. The `display` method should return the colour name.

class Display (α : Type) where
  display : α  String

inductive TrafficLight where
  | Red | Yellow | Green

-- TODO: write a manual instance of Display for TrafficLight
-- (do NOT use deriving)

#guard Display.display TrafficLight.Red == "red"
#guard Display.display TrafficLight.Yellow == "yellow"
#guard Display.display TrafficLight.Green == "green"
hint
  • Write instance : Display TrafficLight where …
  • Use pattern matching on the three constructors
  • | .Red => "red", | .Yellow => "yellow", | .Green => "green"
  • This is what deriving Repr does automatically — you're doing it by hand