-- Type Classes -- -- The first two lectures have demonstrated many of Lean's core features, but we have yet to touch on -- another important one: type classes. -- So far we have shown you how to define types, and functions which act on them but we often want to abstract -- over *many* types with similar functionality, and ideally implement functionality in terms of its interface. -- Lean's mechanism for this kind of abstraction (known as "ad-hoc polymorphism") is type classes. -- A type class is just a signature, or interface. -- Foe example we can define a type class which describes a single type class method `print` which for some -- type `a` tells us how to print it, similar to toString in other programming languages. class has_print (a : Type) := (print : a → string) -- We can then write a function which generically uses this class to implement code for all types which -- *implement* this type class. -- -- For example the bottom definition computes the string representation of a data type, and appends a newline character to -- it. def print_ln {a : Type} [has_print a] (value : a) : string := has_print.print value ++ "\n" -- We can now provide an *instance* of this type class, that is an implementation of it for a specific type. -- -- For strings its easy, just add quotes to the string itself. instance string.has_print : has_print string := { print := fun s, "\"" ++ s ++ "\"" } -- We can cheat here and call the builtin `to_string` function here for numbers. instance nat.has_print : has_print nat := { print := fun n, to_string n } -- We have already seen generic types like `list`, and `option` how do we provide implementations for them? -- We could just implement concrete instances for each specilization `option string`, `option nat` and so on, -- but this quickly becomes frustrating and repetitive. instance string_option.has_print : has_print (option string) := { print := fun s, match s with | none := "none" | some v := "some " ++ v end } instance nat_option.has_print : has_print (option nat) := { print := fun n, match n with | none := "none" | some v := "some " ++ to_string v end } -- We would like to define an instance that is generic over all types `t`, but how do we do it? instance option.has_print_fail (t : Type) : has_print (option t) := { print := fun t, sorry } -- We briefly showed how to ask for an instance of a type class in the above example `[has_print t]`, -- but we didn't show that this can be used on *instance* declarations to describe how we can build -- an instance for a generic type like `option t`. -- -- Here we say I can give you an instance of `has_print (option t)` if you can prove to me that we -- know that `t` `has_print t`. -- -- This means my generic code can just dispatch the responsiblity of how to print `t` to the programmer -- who defined how to print `t`. instance option.has_print (t : Type) [has_print t] : has_print (option t) := { print := fun t, match t with | none := "none" | some v := "some " ++ has_print.print v end } #eval (print_ln $ some 1) #eval (print_ln $ some "hello") -- This last one doesn't work, if we mouse over we will see it can't find `has_print (nat × nat)`, -- this is because we didn't provide an instance for tuples. #eval (print_ln $ (1, 2)) instance prod.has_print (t u : Type) [has_print t] [has_print u] : has_print (t × u) := { print := fun p, match p with | (x, y) := "(" ++ has_print.print x ++ "," ++ has_print.print y ++ ")" end } #eval (print_ln $ (1, 2)) #eval (print_ln $ (1, "hello")) #eval (print_ln $ (some (1, "foo"), "hello")) -- Type classes are used heavily through out Lean's standard library and -- allow many of the language constructs to be overloaded, we want to briefly -- introduce them, but will not focus on using the heavily in class. #check (if _ then _ else _) #check ite