Associated Type Synonyms: Manuel M. T. Chakravarty Gabriele Keller Simon Peyton Jones
Associated Type Synonyms: Manuel M. T. Chakravarty Gabriele Keller Simon Peyton Jones
Associated Type Synonyms: Manuel M. T. Chakravarty Gabriele Keller Simon Peyton Jones
Manuel M. T. Chakravarty
Gabriele Keller
Abstract
Haskell programmers often use a multi-parameter type class in
which one or more type parameters are functionally dependent on
the first. Although such functional dependencies have proved quite
popular in practice, they express the programmers intent somewhat
indirectly. Developing earlier work on associated data types, we
propose to add functionally-dependent types as type synonyms to
type-class bodies. These associated type synonyms constitute an
interesting new alternative to explicit functional dependencies.
Categories and Subject Descriptors D.3.3 [Programming Languages]: Language Constructs and Features
General Terms Design, Languages, Theory
Keywords Type classes; Type functions; Associated types; Type
inference; Generic programming
1. Introduction
Suppose you want to define a family of containers, where the representation type of the container defines (or constrains) the type of its
elements. For example, suppose we want containers supporting at
least insertion, union, and a membership test. Then a list can contain elements of any type supporting equality; a balanced tree can
only contain elements that have an ordering; and a bit-set might
represent a collection of characters. Here is a rather natural type for
the insertion function over such collections:
insert :: Collects c Elem c c c
The type class Collects says that insert is overloaded: it will work
on a variety of collection types c, namely those types for which
the programmer writes an instance declaration for Collects. But
what is Elem? The intent is obviously that Elem c is the element
type for collection type c; you can think of Elem as a typelevel function that transforms the collection type to the element
type. However, just as insert is non-parametric (its implementation
varies depending on c), so is Elem. For example, Elem [e] is e, but
Elem BitSet is Char .
The core idea of this paper is to extend traditional Haskell type
classes with the ability to define associated type synonyms. In our
example, we might define Collects like this:
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 not made or distributed
for profit or commercial advantage and that copies bear this notice and the full citation
on the first page. To copy otherwise, to republish, to post on servers or to redistribute
to lists, requires prior specific permission and/or a fee.
ICFP05 September 2628, 2005, Tallinn, Estonia.
c 2005 ACM 1-59593-064-7/05/0009. . . $5.00.
Copyright
The collections abstraction Collects from Section 1 is an example of a generic data structureothers include sequences, graphs,
and so on. Several very successful C++ libraries, such as the Standard Template Library [29] and the Boost Graph Library [28], provide highly-parameterised interfaces to these generic data structures, along with a wide range of implementations of these interfaces with different performance characteristics. Recently, Garcia
et al. [8] published a qualitative comparison of six programming
2 The infix operator f x in Haskell is function application f x at a lesser
$
precedence.
languages when used for this style of programming. In their comparison Haskell, including multi-parameter type classes and functional dependencies, was rated very favourably, except for its lack
of support for associated types.
Here is part of the interface to a graph library, inspired by their
paper; although, we have simplified it considerably:
type Edge g = (Node g, Node g)
-- We simplify by fixing the edge representation
class Graph g where
type Node g
outEdges :: Node g g [Edge g]
class Graph g BiGraph g where
inEdges :: Node g g [Edge g]
Using an associated type synonym, we can make the type of nodes,
Node g, a function of the graph type g. Basic graphs only support
traversals along outgoing edges, whereas bi-graphs also support going backwards by following incoming edges. A graph representation based on adjacency lists would only implement the basic interface, whereas one based on an adjacency matrix can easily support
the bi-graph interface, as the following instances illustrate:
data AdjList v = AdjList [[v ]]
instance Enum v Graph (AdjList v ) where
type Node (AdjList v ) = v
outEdges v g = [(v , w ) | w g!!fromEnum v ]
type AdjMat = Array.Array (Int, Int) Bool
instance Graph AdjMat where
type Node AdjMat = Int
outEdges v g = let ((from, ), (to, )) = bounds g
in [w | w [from..to], g!(v , w )]
instance BiGraph AdjMat where
inEdges v g = let ((from, ), (to, )) = bounds g
in [w | w [from..to], g!(w , v )]
By making Edge, as well as Node, an associated type synonym
of Graph and by parameterising over traversals and the data structures used to maintain state during traversals, the above class can
be made even more flexible, much as the Boost Graph Library, or
the skeleton used as a running example by Garcia et al. [8].
The part to the left of the = is called the definition head. The
head must repeat the type parameters of the instance declaration
exactly (here [a]); and any additional parameters of the synonym
must be simply type variables (k , in our example). The overall
number of parameters, called the synonyms arity, must be the
same as in the class declaration. All applications of associated
type synonyms must be saturated; i.e., supplied with as many type
arguments as prescribed by their arity.
We omit here the discussion of toplevel data type declarations
involving associated types, as we covered these in detail previously [1]. In all syntactic restrictions in this section, we assume
that any toplevel type synonyms have already been replaced by their
right-hand sides.
3.1
Equality constraints
Instance declarations
tractable. In particular, they must be confluent; i.e., if a type expression can be reduced in two different ways, there must be further
reduction steps that join the two different reducts again. Moreover,
type functions must be terminating; i.e., applications must reach
an irreducible normal form after a finite number of reduction steps.
The first condition, confluence, is already standard on the level of
values, but the second, termination, is a consequence of the desire
to keep type checking decidable.
Similar requirements arise already for vanilla type classes as
part of a process known as context reduction. In a declaration
instance (1 , . . . , n ) C 1 m
we call C 1 m the instance head and (1 , . . . , n ) the instance context, where each i is itself a class constraint. Such an
instance declaration implies a context reduction rule that replaces
the instance head by the instance context. The critical point is that
the constraints i can directly or indirectly trigger other context
reduction rules that produce constraints involving C again. Hence,
we have recursive reduction rules and the same issues of confluence
and termination as for associated type synonyms arise. Haskell 98
carefully restricts the formation rules for instance declarations such
that the implied context reduction rules are confluent and terminating. It turns out, that we can use the same restrictions to ensure
these properties for associated type synonyms. In the following, we
discuss these restrictions, but go beyond Haskell 98 by allowing
multi-parameter type classes. We will also see how the standard
formation rules for instances affect the type functions induced by
associated synonym definitions.
Restrictions on instance heads. Haskell 98 imposes the following three restrictions. Restriction (1): Heads must be constructorbased; i.e., the type patterns in the head may only contain variables
and data type constructors, synonyms are not permitted. Restriction (2): Heads must be specific; i.e., at least one type parameter
must be a non-variable term. Restriction (3): Heads must be nonoverlapping; i.e., there may be no two declarations whose heads
are unifiable.
Given that the heads of associated synonyms must repeat the
type parameters of the instance head exactly, the above three restrictions directly translate to associated synonyms. Restriction (1)
is familiar from the value level, and we will discuss Restriction (2)
a little later. The value level avoids Restriction (3) by defining that
the selection of equations proceeds in textual order (i.e., if two
equations overlap, the textually earlier takes precedence). However,
there is no clear notion of textual order for instance declarations,
which may be spread over multiple modules.
Restrictions on instance contexts. Haskell 98 imposes one more
restriction. Restriction (4): Instance contexts must be decreasing.
More specifically, Haskell 98 requires that the parameters of the
constraints i occurring in an instance context are variables. If
we have multi-parameter type classes, we need to further require
that these variable parameters of a single constraint are distinct.
Restriction (4) and (2) work together to guarantee that each context
reduction rule simplifies at least one type parameter. As type terms
are finite, this guarantees termination of context reduction.
In the presence of associated types, we generalise Restriction (4) slightly. Assuming 1 , . . . , n are each either a type variable or an associated type applied to type variables, a context constraint i can either be a class constraint of the form D 1 n or
be an equality constraint of the form S 1 m = .
The right-hand sides of the associated type synonyms of an instance are indirectly constrained by Restriction (4), as they may
only contain applications of synonyms whose associated class appears in the instance context. So, if we have
instance (1 , . . . , n ) C where
type SC = [SD ]
Ambiguity
Symbol Classes
, , htype variablei
T
htype constructori
Sk
hassociated type synonym, arity ki
D
htype classi
x, f, d hterm variablei
Source declarations
(whole program)
pgm
cls; inst; val
cls
class .D D where (class decl)
tsig; vsig
inst
instance . where (instance declaration)
atype; val
val
x = e
(value binding)
(assoc. type signature)
tsig
type S k k
vsig
x ::
(method signature)
(k1)
= (assoc. type synonym)
atype type S k
Source terms
e x | e1 e2 | x.e | let x = e1 in e2 | e ::
Source types
, T | | 1 2 |
Sk k
.
Constraints
c D
= =
c | =
. | . =
(monotype)
(associated type)
(qualified type)
(type scheme)
(class constraint)
(equality constraint)
(simple constraint)
(qualified constraint)
(constraint scheme)
Environments
x : (type environment)
(instance environment)
U =
(set of equality constraints)
Figure 1: Syntax of expressions and types
times use equivalent curried notation, thus:
n
n
n .
1 n
1 n
1 n .
|e:
(x : )
(var )
|x:
1 | e1 : 1
2 | [x : 1 ] e2 : 2
(let)
1 , 2 | let x = e1 in e2 : 2
| e1 : 2 1
| e2 : 2
(E)
| e1 e2 : 1
| [x : 1 ] e2 : 2
1
(I)
| x.e2 : 1 2
|e:
(E)
|e:
, | e :
(I)
|e:
|e:
6 Fv Fv
(I)
| e : .
| e : 1
1 = 2
(conv )
| e : 2
| e : .
(E)
| e : [ /]
|e:
Fv =
(sig)
| (e :: ) :
(mono)
.
(spec)
[ /]
(mp)
2 = 1
(eqsymm )
1 = 2
(eqrefl )
[1 /]
1 = 2
(eqsubst )
[2 /]
1 = 2
2 = 3
(eqtrans )
1 = 3
D 0
S is an associated type of D
S k 0 (k1)
(wfsyn )
1
2
(wfapp )
1 2
6 Fv
(wfspec )
.
(wfvar )
,
(wfmp )
(wfcon )
| cls : ,
, D
(cls)
n
class .D D where
n
| type S ::
: [.D D ], [f : .D ]
f ::
c , i | inst :
for each D where (.D D ) c , we have c , i
. D
c ,
(f : .D )
c , i , | e : [ /]
instance . D where
: [. D , .S = ]
c , i | type S =
f =e
| val :
|e:
(val )
| (x = e) : [x : ]
(inst)
pgm
= c , i
| cls : c , c
= c , v
| inst : i
cls; inst; val
| val : v
5. Type Inference
The addition of Rule (conv ) to type expressions and of equality
axioms to constraint entailment has a significant impact on type
inference. Firstly, we need to normalise type terms involving associated types according to the equations defining associated types.
Secondly, type terms involving type variables often cannot be completely normalised until some type variables are further instantiated. Consequently, we need to extend the standard definition of
unification in the Hindley-Milner system to return partially-solved
equality constraints in addition to a substitution.
To illustrate this, reconsider the definition of the class Collects
with associated type Elem from Section 1. The unification problem
(Int, a) = (Elem c, Bool ) implies the substitution [Bool/a],
but also the additional constraint Int = Elem c. We cannot decide whether the latter constraint is valid without knowing more
about c, so we call such constraints pending equality constraints. In
the presence of associated types, the type inference algorithm has
to maintain pending equality constraints together with class predicates.
overlapping.
Instance contexts must be decreasing.
To guarantee a coherent (i.e., unambiguous) translation of welltyped source programs to an explicitly-typed language in the style
of System F, which implements type classes by dictionaries, we
require two further constraints:
Equality constraints (in programmer-supplied type annotations)
Fixv .
Here Fixv , the set of fixed variables of a signature , is defined
as follows:
Fixv T
= {}
Fixv
= {}
Fixv (1 2 )
= Fixv 1 Fixv 2
Fixv
= {}
Fixv (( = ) ) = Fixv Fixv
Fixv (D )
= Fixv
Fixv (.)
= Fixv \ {}
Intuitively, the fixed variables of a type (or signature) are those
free variables that will be constrained when we unify the type (or
type component of the signature) with a ground type; provided it
matches.
A program that meets all of the listed constraints is well-formed.
The declaration typing rules of Figure 3 determine, for a given program, the validity of a program context P and typing environment
. Both are inputs to type inference for expressions and, if they are
without superfluous elements, we call them well-formed. As in the
typing rules, we implicitly assume all types to be well-kinded. It
is straight-forward to add the corresponding judgements to the presented rules.
We call an expression well-formed if any signature annotation
of the form (e :: ) obeys the listed constraints. For the rest of
the paper, we confine ourselves to well-formed programs, program
contexts, typing environments, and expressions. This in particular
Type inference
Unification
, U | T e :
(x : . )
fresh
(var W )
[/] | x : [/]
W
fresh
P
U U1 , U2 , (T2 1 = 2 )
1 , U1 | T1 e1 : 1
2 , U2 | T2 (T1 ) e2 : 2
U; R
(EW )
R(T2 1 , 2 ), U | (RT2 T1 ) e1 e2 : R
W
, U | T [x : ] e :
fresh
(IW )
, U | T x.e : T
W
1 , U1 | T1 e1 : 1
2 , U2 | T2 (T1 [x : ]) e2 : 2
= Gen(T1 ; (1 , U1 ) 1 )
(let W )
2 , U2 | (T2 T1 ) (let x = e1 in e2 ) : 2
W
, U | T e : 1
P (. 2 )
= Gen(T ; (, U ) 1 )
fresh
Fv ( 2 )
[/] | T (e :: . 2 ) : [/]2
Gen(; ) = .
(sig W )
, where = Fv \ Fv
1
1 2
i
1ik
Sk
i
k
2
1 2
1
(lapp R )
1 2
j = j , j 6= i
Sk
U =
U 2 = 1
U 1 = 2
(refl U )
U 1 2 = 1 2
{1 = 1 , 2 = 2 };
U;
U; R
U; R
(symm U )
U; R
(app U )
U U
U U
(. = )
(syn R )
[/]
[/]
(arg R )
U 1 = 2
2
(rapp R )
1 2
6 Fv
(var U )
U =
; [ /]
2
(red U )
U = 1
{2 = 1 };
U ; R
U 1 = 2
U ; R
U R U, U
U U, 1 = 2 U ; R R
U ; R
E
E D
(. D )
(bchain E )
E [/]D
[/];
E P
U
U P
P;
P;R
(red E )
{D };
U 1 = 2
E 1 = 2
U; R
(eq E )
U; R
P ; R
P ; R
U R P, P
U P, P ; R R
P ; R
E P
P ; R. Finally, we turn the closure into a deter!
ministic function by requiring that for P
E P
P ; R, the
6. Functional Dependencies
Associated type synonyms cover much the same applications as
functional dependencies. It is too early to say which of the two
is better; hence, we will simply contrast the properties of both.
The comparison is complicated by the existence of at least three
variants of functional dependencies: (1) The system described by
Mark Jones [15], (2) the generalised system implemented in the
Glasgow Haskell Compiler (GHC) [31], and (3) an even more
permissive system formalised by Stuckey & Sulzmann [30]. Like
our associated type synonyms, the first two of these systems permit
an implementation by a dictionary translation to an explicitly-typed
language akin to System F. Stuckey and Sulzmanns system, based
on an encoding into constraint handling rules, gains additional
generality by giving up on a type-preserving translation and on
separate compilation.
As pointed out by Duck et al. [6], GHCs system is not decidable, as it permits recursive instance declarations that, for some programs that should be rejected, leads to non-termination of type inference. Jones original system is decidable. The Stuckey-Sulzmann
system, and the associated type synonyms we describe here, both
ensure decidability by a suitable set of restrictions on the admissible constraint handling rules and associated type synonyms, respectively; both systems can handle more general rule sets if decidable
inference is not required.
Readability
Expressiveness
definitions, as well as method definitions. Doing so directly addresses Garcia et als primary concern about large-scale programming in Haskell. It also fills out Haskells existing ability to define
open functions at the value level using type classes, with a complementary type-level facility.
There is clearly a big overlap between functional dependencies
and associated type synonyms, and no language would want both.
We do not yet have enough experience to know what difficulties (either of expressiveness or convenience), if any, programmers would
encounter if functional dependencies were replaced by associated
type synonymsbut we regard that as an attractive possibility.
Acknowledgements. We particularly thank Martin Sulzmann for
his detailed and thoughtful feedback, which helped to identify
and characterise several potential pitfalls. We are also grateful to
Roman Leshchinskiy and Stefan Wehr who suggested significant
improvements to the presentation and the formal system; moreover,
Stefan Wehr throughly improved and extended the prototype type
checker. We also thank the anonymous referees for their helpful
comments. The first two authors have been partly funded by the
Australian Research Council under grant number DP0211203.
References
[1] Manuel M. T. Chakravarty, Gabriele Keller, Simon Peyton Jones,
and Simon Marlow. Associated types with class. In Martin Abadi,
editor, Conference Record of POPL 2005: The 32nd ACM SIGPLANSIGACT Symposium on Principles of Programming Languages, pages
113. ACM Press, 2005.
[2] James Cheney and Ralf Hinze. First-class phantom types. Technical
Report TR2003-1901, Cornell University, 2003.
[3] Dominique Clement, Thierry Despeyroux, Gilles Kahn, and Joelle
Despeyroux. A simple applicative language: mini-ML. In LFP 86:
Proceedings of the 1986 ACM Conference on LISP and Functional
Programming, pages 1327, New York, NY, USA, 1986. ACM Press.
[4] Olivier Danvy. Functional unparsing. J. Funct. Program., 8(6):621
625, 1998.
[5] Derek Dreyer, Karl Crary, and Robert Harper. A type system for
higher-order modules. In Proceedings of the 30th ACM SIGPLANSIGACT Symposium on Principles of Programming Languages, pages
236249, 2003.
[6] Gregory J. Duck, Simon Peyton Jones, Peter J. Stuckey, and Martin
Sulzmann. Sound and decidable type inference for functional
dependencies. In ESOP;04, LNCS. Springer-Verlag, 2004.
[7] Karl-Filip Faxen. A static semantics for Haskell. Journal of
Functional Programming, 12(4+5), 2002.
[8] Ronald Garcia, Jaakko Jarvi, Andrew Lumsdaine, Jeremy Siek, and
Jeremiah Willcock. A comparative study of language support for
generic programming. In Proceedings of the 18th ACM SIGPLAN
Conference on Object-Oriented Programing, Systems, Languages,
and Applications, pages 115134. ACM Press, 2003.
[9] Cordelia V. Hall, Kevin Hammond, Simon L. Peyton Jones, and
Philip L. Wadler. Type classes in Haskell. ACM Trans. Program.
Lang. Syst., 18(2):109138, 1996.
[10] Ralf Hinze. Formatting: A class act. Journal of Functional
Programming, 13:935944, 2003.
[11] Ralf Hinze and Simon Peyton Jones. Derivable type classes. In
Graham Hutton, editor, Proceedings of the 2000 ACM SIGPLAN
Haskell Workshop, volume 41.1 of Electronic Notes in Theoretical
Computer Science. Elsevier Science, 2001.
[12] Mark P. Jones. A theory of qualified types. In ESOP92: Symposium
proceedings on 4th European symposium on programming, pages
287306, London, UK, 1992. Springer-Verlag.
[13] Mark P. Jones. Simplifying and improving qualified types. In
FPCA 95: Conference on Functional Programming Languages and
Computer Architecture. ACM Press, 1995.
[14] Mark P. Jones. A system of constructor classes: Overloading
and implicit higher-order polymorphism. Journal of Functional