Sunday, July 20, 2014

Equational reasoning at scale

Haskell programmers care about the correctness of their software and they specify correctness conditions in the form of equations that their code must satisfy. They can then verify the correctness of these equations using equational reasoning to prove that the abstractions they build are sound. To an outsider this might seem like a futile, academic exercise: proving the correctness of small abstractions is difficult, so what hope do we have to prove larger abstractions correct? This post explains how to do precisely that: scale proofs to large and complex abstractions.

If you saw "components" and thought "functions", think again! We can compose things that do not even remotely resemble functions, such as proofs! In fact, Haskell programmers prove large-scale properties exactly the same way we build large-scale programs:

We build small proofs that we can verify correct in isolation

We compose smaller proofs into larger proofs

The following sections illustrate in detail how this works in practice, using Monoids as the running example. We will prove the Monoid laws for simple types and work our way up to proving the Monoid laws for much more complex types. Along the way we'll learn how to keep the proof complexity flat as the types grow in size.

That completes the proof of the three Monoid laws, but I'm not satisfied with these proofs.

Generalizing proofs

I don't like the above proofs because they are disposable, meaning that I cannot reuse them to prove other properties of interest. I'm a programmer, so I loathe busy work and unnecessary repetition, both for code and proofs. I would like to find a way to generalize the above proofs so that I can use them in more places.

We improve proof reuse in the same way that we improve code reuse. To see why, consider the following sort function:

sort :: [Int] -> [Int]

This sort function is disposable because it only works on Ints. For example, I cannot use the above function to sort a list of Doubles.

Fortunately, programming languages with generics let us generalize sort by parametrizing sort on the element type of the list:

sort :: Ord a => [a] -> [a]

That type says that we can call sort on any list of as, so long as the type a implements the Ord type class (a comparison interface). This works because sort doesn't really care whether or not the elements are Ints; sort only cares if they are comparable.

Similarly, we can make the proof more "generic". If we inspect the proof closely, we will notice that we don't really care whether or not the tuple contains Ints. The only Int-specific properties we use in our proof are:

0 + x = x
x + 0 = x
(x + y) + z = x + (y + z)

However, these properties hold true for all Monoids, not just Ints. Therefore, we can generalize our Monoid instance for tuples by parametrizing it on the type of each field of the tuple:

The above Monoid instance says that we can combine tuples so long as we can combine their individual fields. Our original Monoid instance was just a special case of this instance where both the a and b types are Ints.

Note: The mempty and mappend on the left-hand side of each equation are for tuples. The memptys and mappends on the right-hand side of each equation are for the types a and b. Haskell overloads type class methods like mempty and mappend to work on any type that implements the Monoid type class, and the compiler distinguishes them by their inferred types.

We can similarly generalize our original proofs, too, by just replacing the Int-specific parts with their more general Monoid counterparts.

This is our first example of a "scalable proof". We began from three primitive building blocks:

Int is a Monoid

[a] is a Monoid

(a, b) is a Monoid if a is a Monoid and b is a Monoid

... and we connected those three building blocks to assemble a variety of new Monoid instances. No matter how many tuples we nest the result is still a Monoid and obeys the Monoid laws. We don't need to re-prove the Monoid laws every time we assemble a new permutation of these building blocks.

However, these building blocks are still pretty limited. What other useful things can we combine to build new Monoids?

IO

We're so used to thinking of Monoids as data, so let's define a new Monoid instance for something entirely un-data-like:

We can very easily reason that the type (IO String, IO (Int, Int)) obeys the Monoid laws because:

String is a Monoid

If String is a Monoid then IO String is also a Monoid

Int is a Monoid

If Int is a Monoid, then (Int, Int) is also a `Monoid

If (Int, Int) is a Monoid, then IO (Int, Int) is also a Monoid

If IO String is a Monoid and IO (Int, Int) is a Monoid, then (IO String, IO (Int, Int)) is also a Monoid

However, we don't really have to reason about this at all. The compiler will automatically assemble the correct Monoid instance for us. The only thing we need to verify is that the primitive Monoid instances obey the Monoid laws, and then we can trust that any larger Monoid instance the compiler derives will also obey the Monoid laws.

The Unit Monoid

Haskell Prelude also provides the putStrLn function, which echoes a String to standard output with a newline:

putStrLn :: String -> IO ()

Is putStrLn combinable? There's only one way to find out!

>>> putStrLn "Hello" <> putStrLn "World"
Hello
World

Interesting, but why does that work? Well, let's look at the types of the commands we are combining:

putStrLn "Hello" :: IO ()
putStrLn "World" :: IO ()

Well, we said that IO b is a Monoid if b is a Monoid, and b in this case is () (pronounced "unit"), which you can think of as an "empty tuple". Therefore, () must form a Monoid of some sort, and if we dig into Data.Monoid, we will discover the following Monoid instance:

This says that empty tuples form a trivial Monoid, since there's only one possible value (ignoring bottom) for an empty tuple: (). Therefore, we can derive that IO () is a Monoid because () is a Monoid.

Functions

Alright, so we can combine putStrLn "Hello" with putStrLn "World", but can we combine naked putStrLn functions?

>>> (putStrLn <> putStrLn) "Hello"
Hello
Hello

Woah, how does that work?

We never wrote a Monoid instance for the type String -> IO (), yet somehow the compiler magically accepted the above code and produced a sensible result.

This says: "If b is a Monoid, then any function that returns a b is also a Monoid".

The compiler then deduced that:

() is a Monoid

If () is a Monoid, then IO () is also a Monoid

If IO () is a Monoid then String -> IO () is also a Monoid

The compiler is a trusted friend, deducing Monoid instances we never knew existed.

Monoid plugins

Now we have enough building blocks to assemble a non-trivial example. Let's build a key logger with a Monoid-based plugin system.

The central scaffold of our program is a simple main loop that echoes characters from standard input to standard output:

main = do
hSetEcho stdin False
forever $ do
c <- getChar
putChar c

However, we would like to intercept key strokes for nefarious purposes, so we will slightly modify this program to install a handler at the beginning of the program that we will invoke on every incoming character:

This says: "If f is an Applicative and b is a Monoid, then f b is also a Monoid." In other words, we can automatically extend any existing Monoid with some new feature f and get back a new Monoid.

Note: The above instance is bad Haskell because it overlaps with other type class instances. In practice we have to duplicate the above code once for each Applicative. Also, for some Applicatives we may want a different Monoid instance.

We can prove that the above instance obeys the Monoid laws without knowing anything about f and b, other than the fact that f obeys the Applicative laws and b obeys the Applicative laws. These proofs are a little long, so I've included them in Appendix B.

In the interest of brevity, I will skip the proofs of the Applicative laws, but I may cover them in a subsequent post.

The beauty of ApplicativeFunctors is that every new Applicative instance we discover adds a new building block to our Monoid toolbox, and Haskell programmers have already discovered lots of ApplicativeFunctors.

Composing applicatives

Haskell programmers prove large-scale properties exactly the same way we build large-scale programs:

We build small proofs that we can verify correct in isolation

We compose smaller proofs into larger proofs

I don't like to use the word compose lightly. In the context of category theory, compose has a very rigorous meaning, indicating composition of morphisms in some category. This final section will show that we can actually compose Monoid proofs in a very rigorous sense of the word.

So in our Plugin example, we began on the proof that () was a Monoid and then composed three Applicative morphisms to prove that Plugin was a Monoid. I will use the following diagram to illustrate this:

Conclusion

You can equationally reason at scale by decomposing larger proofs into smaller reusable proofs, the same way we decompose programs into smaller and more reusable components. There is no limit to how many proofs you can compose together, and therefore there is no limit to how complex of a program you can tame using equational reasoning.

This post only gave one example of composing proofs within Haskell. The more you learn the language, the more examples of composable proofs you will encounter. Another common example is automatically deriving Monad proofs by composing monad transformers.

As you learn Haskell, you will discover that the hard part is not proving things. Rather, the challenge is learning how to decompose proofs into smaller proofs and you can cultivate this skill by studying category theory and abstract algebra. These mathematical disciplines teach you how to extract common and reusable proofs and patterns from what appears to be disposable and idiosyncratic code.

Appendix A - Missing Monoid instances

These Monoid instance from this post do not actually appear in the Haskell standard library:

instance Monoid b => Monoid (IO b)
instance Monoid Int

The first instance was recently proposed here on the Glasgow Haskell Users mailing list. However, in the short term you can work around it by writing your own Monoid instances by hand just by inserting a sufficient number of pures and liftA2s.

For example, suppose we wanted to provide a Monoid instance for Plugin. We would just newtype Plugin and write:

This lifting guarantees that if a obeys the semiring laws then so will f a. Of course, you will have to specialize the above instance to every concrete Applicative because otherwise you will get overlapping instances.

Appendix B

These are the proofs to establish that the following Monoid instance obeys the Monoid laws:

Yes, there is a better solution, which is to use the `Managed` applicative instead of the `IO` applicative for that particular part. This is an `Applicative` that my `mvc` library defines, but I just split it out into a separate library today because this use case for it comes up a lot:

Thank, Gabriel, you for another great post. When I try to understand some [relatively small] piece of code I really miss some "automated rewriting system" specifically for Haskell and equational-reasoning (yes, I know that there are COQ, Agda Isabelle and so on). Have you ever thought about such a system (maybe it even exists somewhere but I don't really know).

The interface for Applicative comes from replacing the explicit types and constructors () and (,) in Monoidal with things that those types and constructors can be passed in to to recover Monoidal. Applicative then takes the weird step of replacing liftA2 in the interface with function application lifted into the Monoidal

(<*>) = liftA2

From the category theory side of things, Applicative makes more sense if you consider it in terms of pure and liftA2.

I just used monoids to compose functions for the first time. So thank you for the super helpful mind extension. Also, I've read some of your posts here and on reddit about using categories for program architecture. While this seems to be appropriate for library design (like your pipes library), I'm struggling to see where this fits into classic enterprise problems. Things like User, PurchaseOrder, LineItem, and Account aren't really monoids or applicatives. Just wondering if you'd had any thoughts about this type of problem.

Yeah, I have thought about more mundane data types like that. You can always turn any type into a `Monoid` just by wrapping it with `Maybe` and then defining `mappend` to take the first `Just` result (or `Nothing` if neither of them are `Just`) and `mempty` is `Nothing`. This is sort of like the monoid of last resort, and it already exists in `Data.Monoid` as the `First` monoid. It also exists for `Maybe`, too, as the `Alternative`/`MonadPlus` instances for `Maybe`, which also do the same thing.

That particular monoid is useful for combining a bunch of partially-completed records into a new record. However, you lose information, specifically you no longer record in the types whether or not a field is defined or not. There is a variation on this monoid that does record that information in the types so that you can see at a glance from the inferred type which fields are complete and which fields are still incomplete, but it's more difficult to implement because it requires advanced type extensions. However, I'll give a rough sketch of what the types would look like. You'd have records whose types would indicate which fields are defined or undefined:

-- First field defined record1 :: Record True False

-- Second field defined record2 :: Record False True

... and then you would have something analogous to `mappend` that combines records together and deduces which fields are still defined:

So if you combined the above two records, the compiler would deduce that the resulting type was fully defined:

mappend record1 record2 :: Record True True

You can actually make that valid Haskell using the `DataKinds` and `TypeFamilies` extension, so it's totally possible to implement. The hard part is making it work recursively over nested records or tuples of defined values the way that the `Monoid` instances worked in this post.

So it's possible in theory and it gets much better in a dependently typed language like Idris.