Beyond Type Classes
Total Page:16
File Type:pdf, Size:1020Kb
Beyond Type Classes Andreas Rossberg Martin Sulzmann Programming Systems Department of Computer Science and Software Universit¨at des Saarlandes, Germany Engineering [email protected] University of Melbourne, Vic. 3010, Australia [email protected] ABSTRACT (==) :: Eq a => a -> a -> Bool We discuss type classes in the context of the Chameleon which has a constraint component Eq a and a type compo- language, a Haskell-style language where overloading reso- nent a -> a -> Bool. lution is expressed in terms of the meta-language of Con- Constructor classes [11] allow the programmer to define straint Handling Rules (CHRs). In a first step, we show relations not just over types but also over type constructors. how to encode Haskell’s single-parameter type classes into A typical example is the Functor class: Chameleon. The encoding works by providing an approrpri- ate set of CHRs which mimic the Haskell conditions. We also class Functor f where consider constructor classes, multi-parameter type classes fmap :: (a -> b) -> (f a -> f b) and functional dependencies. Chameleon provides a test- where the class parameter f ranges over type constructors bed to experiment with new overloading features. We show such as [] and Tree. how some novel features such as universal quantification in Multi–parameter type classes [13] allow for multiple class context can naturally be expressed in Chameleon. parameters. For example, 1. INTRODUCTION class Collects ce e where empty :: ce Type classes [14, 23] are one of the most prominent fea- insert :: ce -> e -> ce tures of Haskell [18]. They are also found in other languages such as Mercury [6, 9] , HAL [3] and Clean [19]. In partic- The Collects class defines a relation between element types ular Haskell has become the most popular playing field for e and the type ce of the collection itself. Unfortunately, this type class acrobats. Advanced features such as construc- example has problems; we can’t determine which instance tor [11], multi-parameter [13] classes and functional depen- declaration to use for empty because its type (ce) won’t allow dencies [12] are found in most Haskell implementations. us to deduce an element type e. Therefore, empty is con- The idea behind type classes is to allow the programmer sidered ambiguous. This can be resolved by adding another to define relations over types. For single–parameter type feature, functional dependencies [12] to retain unambiguity. classes, the type class relation simply states set membership. The functional dependency ce e states that for all instance Consider the Eq type class, the declaration declarations of Collects the element type can be uniquely determined from the collection type. In this case empty is class Eq a where (==) :: a -> a -> Bool unambiguous. states that every type a in type class Eq has an equality Further type class proposals can be found in [2, 8]. This is function ==. Instance declarations “prove” that a type is in not an exhaustive list, we observe that there are many pos- the class, by providing appropriate functions for the class sibilities for additional features. For example, we might like methods. For example, Int is in Eq: to say that the Integral and Fractional type classes are instance Eq Int where (==) = primIntEq disjoint (since we know a number can’t be both). This can’t be expressed in any current type class system and hence the which states that the equality function for Ints is primIntEq function where primIntEq is a built-in primitive function on integers. The == function can only be used on values with types that f x y = x / y + x ‘div‘ y are in Eq. This is reflected by the function’s constrained has an inferred type of f :: (Integral a, Fractional type: a) => a -> a -> a rather than immediately causing a type error. As another example consider the following class dec- laration class (forall a . (Eq a => Eq (s a))) => Sequence s Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are Our intention is to state that for any instance type S of not made or distributed for profit or commercial advantage and that copies the Sequence class there must be an instance for Eq (S T) bear this notice and the full citation on the first page. To copy otherwise, to whenever there is an instance Eq T for some type T. To our republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. knowledge such a property can’t be expressed by any type Copyright 2001 ACM X-XXXXX-XX-X/XX/XX ...$5.00. class system to date. 1 Stuckey and Sulzmann introduce in [21] a general over- else x:y:ys loading framework based on Constraint Handling Rules (CHRs). Essential algorithms such as type inference and unambigu- overload insert :: Leq (a->a->Bool) => [a]->a->[a] ity checks can be performed by running the underlying CHR insert = insList evaluation engine. The system is user-programmable in the sense that the user is able to state additional properties via rule Leq a ==> a = b->b->Bool CHRs. That is, CHRs serve as a meta-language to impose rule Insert a ==> a = ce->e->ce conditions on the set of constraints allowed to appear. rule Insert (ce->e->ce), In this paper, we make more explicit the connection be- Insert (ce->e’->ce) ==> e = e’ tween the form of overloading provided by the CHR-based rule Insert ([a]->b->[a]) ==> a = b system and type classes as found in Haskell. The ideas of the CHR-based overloading system have been incorporated Roughly speaking, an overloaded definition in Chameleon into the Chameleon [22] language. The Chameleon syn- corresponds to an instance for an implicit type class in Haskell. tax mostly follows Haskell syntax. Overloaded identifiers For each overloaded identifier we introduce a predicate sym- are defined using the overload keyword which is similar to bol. For example, the overloaded identifier insert intro- instance in Haskell. However, no class declarations are nec- duces the unary predicate symbol Insert. We can refer essary. The user can design her own system by providing to such predicates in type signatures and rule definitions. CHRs via the rule keyword. For example, the functional Note that predicates in Chameleon are mostly of the form dependency declaration class Collects ce e | ce e can P (t1->...->tn->t). Assuming that P corresponds to an be specified as follows: overloaded identifier p, then P (t1->...->tn->t) indicates a possible invocation of p on argument types t1,...,tn and rule Collects ce e, Collects ce e’ ==> e = e’ result type t. Each overloaded definitions must be anno- tated with a valid type signature. Type annotations are We can prevent types from being a member of the Integral optional for regular function definitions. and Fractional types class by the following CHR: For convenience, Chameleon also provides a constraint ab- rule Integral a, Fractional a ==> False breviation mechanism similar to type synonyms in Haskell. For example, the statement The class declaration from before containing a universal quantifier can be expressed as follows: constraint Collects (ce,e) = Empty ce, Insert (ce->e->ce) rule Sequence s, Eq a ==> Eq (s a) introduces Collects (ce,e) as an abbreviation for the pred- We continue in Section 2 by giving an overview of the icate set on the right-hand side of =.1 Constraint synonyms Chameleon language. In Section 3 we show how to en- are expanded statically and can be referred to in type sig- code Haskell’s type classes in Chameleon. The encoding natures and user-definable CHR rules. They may not be is faithful, i.e. if the Haskell 98 program is typable then so cyclic. will be the respective Chameleon program, with the same A user-definable CHR propagation rule is of the form type. Section 4 considers more sophisticated type class fea- tures such as an alternative treatment of constructor classes rule C ==> D and universal quantification in contexts. We conclude in Section 7 More details on Chameleon can be found under where C and D consist of a set of predicates. D may addition- http://www.cs.mu.oz.au/~sulzmann/chameleon ally consist of equality constraints. We often use the term “constraint” to refer either to a predicate, or an equality 2. CHAMELEON constraint, or a set thereof. Assume C = c1, ...,cn and D = d1,...,dm. Then, the logical meaning of such a CHR A Chameleon program consists of a set of data types and is as follows: (possibly overloaded) function definitions. Furthermore, we find user-definable CHR rules. Currently, we restrict all defi- [[c1,...,cn =⇒ d1,...,dm]] nitions to the top-level of the program, i.e. nested definitions = ¯ are not permitted. ∀α.¯ (c1 ∧···∧ cn → (∃β.d1 ∧···∧ dm)) Example 1. Here is an example Chameleon program: whereα ¯ = fv(c1 ∧···∧ cn) and β¯ = fv(d1 ∧···∧ dm) − α¯. We assume fv is a function returning the free variables in a data Nat = Zero | Succ Nat constraint. In Example 1, rule Leq a ==> a=b->b->Bool enforces that leq arguments must be of the same type and leqNat Zero Zero = True the result type is Bool. A similar condition is enforced by leqNat Zero (Succ n) = False rule Insert a ==> a=ce->e->ce. The CHR leqNat (Succ n) Zero = False leqNat (Succ n1) (Succ n2) = leqNat n1 n2 rule Insert (ce->e->ce), Insert (ce->e’->ce) ==> e = e’ overload leq :: Nat->Nat->Bool leq = leqNat essentially states a functionally dependency, i.e. the first input argument uniquely determines the second argument.