Type classes in Haskell are used to implement ad-hoc polymorphism, or overloading [Wadler89]. They allow programmers to define functions which behave differently depending on the types of their arguments. A type-class declares a set of (abstract) functions and their types, and any datatype declared an instance of the class has to provide implementations for those functions. Below is the Haskell library type class Monoid, which declares the three abstract functions mempty, mappend and mconcat:
Usually, when we define a type-class we have some expectation on what properties instances of this type class should satisfy when implementing the functions specified. These are captured by type class laws. For monoids, any instance is supposed to satisfy the laws below:
The last law is in fact a specification for the default implementation of the mconcat function, which is commonly used (unless the user wants to provide their own, optimised, implementation). The last law then becomes a trivial identity.
The most obvious instance of Monoid
is probablyLists, but we may for instance also declare the natural numbers to be monoids, with + corresponding to mappend:
We could also have declared Nat a monoid in a different manner, with with * corresponding to mappend:
These instances of the Monoid class are quite simple. By just looking at them, we might convince ourselves that they behave in accordance with the type class laws for monoids, and settle for that. But what if we had a more complicated instance or had made a mistake? Unfortunately, in Haskell, type class laws are typically only stated in comments or documentation, if at all, and there is no support for checking that an instance of a type class actually behaves in accordance with the laws. Furthermore, type class laws could be used in, for example, compiler optimisations. Any violation of the laws could then cause inconsistencies between the original code and the optimised version, which is clearly undesirable.
To address these problems, Jeuring et al. developed a framework for expressing and testing type class laws in Haskell using the QuickCheck tool [Jeuring2012, quickcheck]. As further work, they identify the need to also provide stronger guarantees by also proving type class laws using an automated theorem prover. However, the type class laws typically present in Haskell programs often involve recursive functions and datatypes, which means that we might need induction to prove them. While there has been much success using SMT-solvers and first-order theorem provers for reasoning about programs, such provers, e.g. Z3 and E [z3, eprover], typically do not support induction. Some of the difficulties with inductive proofs is that they often require auxiliary lemmas, which themselves require induction. A system built to handle these kind of problems is HipSpec [hipspecCADE], a state-of-the-art automated inductive theorem for Haskell. Our contributions combine the ideas from earlier work on testing type class laws with inductive theorem proving, and allow us to:
Write down type class laws in Haskell as abstract properties (§LABEL:sec:expressing), including support for types with class constraints.
Automatically instantiate these abstract properties when new instances of a type class is declared, and translate them into a intermediate language called TIP [tip], suitable for passing on to automated theorem provers (§LABEL:sec:spec).
Send the generated conjectures to an automated inductive theorem prover for proof, or output the result as a TIP-problem file. In the experiments reported in this paper, we use the aforementioned HipSpec system for proofs (§LABEL:sec:eval).
This allows us to state the type class laws abstractly only once, and automatically infer and prove the concrete properties that any new instance need to satisfy to comply with the type class laws.
2 Background: HipSpec and TIP
HipSpec allows the user to write down properties to prove in the Haskell source code, in a similar manner to how Haskell programmers routinely write QuickCheck properties. HipSpec supports a subset of the core Haskell language, with the caveat that functions are currently assumed to be terminating and values assumed to be finite. HipSpec can, if required, apply induction to the conjectures it is given, and then send the resulting proof obligations to an external theorem prover, such as Z3, E or Waldmeister [z3, eprover, waldmeister]. The power of HipSpec comes from its theory exploration phase: when given a conjecture to prove, HipSpec first use its subsystem QuickSpec [quickspec, quickspec2], which explores the functions occurring in the problem by automatically suggesting a set of potentially useful basic lemmas, which HipSpec then proves by induction. These can then be used in the proof of the main conjecture. However, as theory exploration happens first, HipSpec sometimes also proves some extra properties, perhaps not strictly needed. We consider this a small price for the extra power theory exploration provides.