-- 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


