Motivation
A while ago, I decided to implement comonad support into my purescript-transformerless library. Unfortunately, I’ve never had a moment where I thought “you know … this situation calls for a comonad!” Hence, since my experience with them was limited, I wasn’t sure I was providing the “correct” instances and definitions. Moreover, I definitely did NOT want to manually transform the comonads in Control.Comonad.{Env, Store, Traced} by changing w to Identity and seeing what came out – although this is in the spirit of transformerless comonads, it’s definitely not fun.
Luckily for me, there are laws for comonads (and laws for Traced), so with a little luck it should be enough to prove my implementations satisfied those laws.
Background: Logic
You may have heard that “propositions are types”, also known as the Curry-Howard isomorphism. It basically relates certain type systems used in programming with certain logics: a type can be seen as a proposition in the corresponding logic, and a value inhabiting that type can be seen as a proof of said proposition.
However, this by itself is not enough to prove that an instance is law-abiding: this is because compilers tend not to have the ability to prove instances are law-abiding, they can merely confirm an expression is well-typed or that it has the type you claim it does. Just because your custom data-type MyMonad has an instance of Monad doesn’t mean it satisfies the ap = apply law, for example.
On the other hand, we can use the same equivalent logic to (dis)prove our instances’ lawfulness.
An easy example
Let’s start with something simple: showing that the Env comonad is a lawful Functor, without delegating the instance to the compiler via derive instance.
newtype Env e a = Env (Tuple e a)
instance functorEnv :: Functor (Env e) where
map :: forall a b. (a -> b) -> Env e a -> Env e b
map f (Env (Tuple e a)) = Env (Tuple e (f a))You may notice this instance is equivalent to that of the underlying Tuple one. In other words, we could have defined it as map f (Env e) = Env (map f e).
So, the Functor typeclass has two laws:
map id = idmap f <<< map g = map (f <<< g)
This is where “equational reasoning” comes in handy: it’s very powerful, while also being quite simple.
In the following proof, each line is equivalent to the one after via equality. If you wish, you can mentally insert an = after each line, linking it to the next.
Proof of 1)
map id– Left-hand side\ (Env (Tuple e a)) -> map id (Env (Tuple e a))– η expansion\ (Env (Tuple e a)) -> Env (Tuple e (id a))– Definition ofmap\ (Env (Tuple e a)) -> Env (Tuple e a))– Definition ofid(\ x -> x) :: forall e a. Env e a -> Env e a– Renamingid– Right-hand side
Proof of 2)
map f <<< map g– Left-hand side\ (Env (Tuple e a)) -> (map f <<< map g) (Env (Tuple e a))– η expansion\ (Env (Tuple e a)) -> map f (map g (Env (Tuple e a)))– Definition ofcompose\ (Env (Tuple e a)) -> map f (Env (Tuple e (g a)))– Definition ofmap g\ (Env (Tuple e a)) -> Env (Tuple e (f (g a)))– Definition ofmap f\ (Env (Tuple e a)) -> Env (Tuple e ((f <<< g) a))– Definition ofcompose\ (Env (Tuple e a)) -> map (f <<< g) (Env (Tuple e a))– Definition ofmapmap (f <<< g)– η reduction
WIP