Advanced Functional Programming
Advanced Functional Programming
Editorial Board
David Hutchison
Lancaster University, UK
Takeo Kanade
Carnegie Mellon University, Pittsburgh, PA, USA
Josef Kittler
University of Surrey, Guildford, UK
Jon M. Kleinberg
Cornell University, Ithaca, NY, USA
Friedemann Mattern
ETH Zurich, Switzerland
John C. Mitchell
Stanford University, CA, USA
Moni Naor
Weizmann Institute of Science, Rehovot, Israel
Oscar Nierstrasz
University of Bern, Switzerland
C. Pandu Rangan
Indian Institute of Technology, Madras, India
Bernhard Steffen
University of Dortmund, Germany
Madhu Sudan
Massachusetts Institute of Technology, MA, USA
Demetri Terzopoulos
New York University, NY, USA
Doug Tygar
University of California, Berkeley, CA, USA
Moshe Y. Vardi
Rice University, Houston, TX, USA
Gerhard Weikum
Max-Planck Institute of Computer Science, Saarbruecken, Germany
Varmo Vene Tarmo Uustalu (Eds.)
Advanced
Functional
Programming
13
Volume Editors
Varmo Vene
University of Tartu
Department of Computer Science
J. Liivi 2, EE-50409 Tartu, Estonia
E-mail: [email protected]
Tarmo Uustalu
Institute of Cybernetics
Akadeemia tee 21, EE-12618 Tallinn, Estonia
E-mail: [email protected]
ISSN 0302-9743
ISBN-10 3-540-28540-7 Springer Berlin Heidelberg New York
ISBN-13 978-3-540-28540-3 Springer Berlin Heidelberg New York
This work is subject to copyright. All rights are reserved, whether the whole or part of the material is
concerned, specifically the rights of translation, reprinting, re-use of illustrations, recitation, broadcasting,
reproduction on microfilms or in any other way, and storage in data banks. Duplication of this publication
or parts thereof is permitted only under the provisions of the German Copyright Law of September 9, 1965,
in its current version, and permission for use must always be obtained from Springer. Violations are liable
to prosecution under the German Copyright Law.
Springer is a part of Springer Science+Business Media
springeronline.com
© Springer-Verlag Berlin Heidelberg 2005
Printed in Germany
Typesetting: Camera-ready by author, data conversion by Scientific Publishing Services, Chennai, India
Printed on acid-free paper SPIN: 11546382 06/3142 543210
Preface
This volume contains the revised lecture notes corresponding to nine of the
lecture courses presented at the 5th International School on Advanced Functional
Programming, AFP 2004, held in Tartu, Estonia, August 14–21, 2004.
The goal of the AFP schools is to inform the wide international communities
of computer science students and software production professionals about the
new and important developments in the area of functional programming. The
schools put a special emphasis on practical applications of advanced techniques.
The Tartu school was preceded by four earlier schools in Båstad, Sweden (1995,
LNCS 925), Olympia, WA, USA (1996, LNCS 1129), Braga, Portugal (1998,
LNCS 1608) and Oxford, UK (2002, LNCS 2638).
The scientific programme of AFP 2004 consisted of five preparatory (“in-
termediate”) courses, given by John Hughes (Chalmers University of Technol-
ogy, Göteborg, Sweden), Doaitse Swierstra (Universiteit Utrecht, The Nether-
lands) and Rinus Plasmeijer (Radboud Universiteit Nijmegen, The Netherlands),
and nine regular (“advanced”) courses, presented by Atze Dijkstra (Universiteit
Utrecht, The Netherlands), Doaitse Swierstra, John Hughes, Conor McBride
(University of Nottingham, UK), Alberto Pardo (Universidade de la República,
Montevideo, Uruguay), Rinus Plasmeijer, Bernard Pope (University of Mel-
bourne, Australia), Peter Thiemann (Universität Freiburg, Germany), and Si-
mon Thompson (University of Kent, UK). There was also a student session.
The school attracted a record number of 68 participants from 16 countries
(inclusive of the lecturers and organizers).
This volume contains the notes for the advanced courses. Following the school,
the lecturers revised the notes they had prepared for the school. The revised
notes were each carefully checked by two or three second readers selected from
among the most qualified available and then revised once more by the lecturers.
We are proud to commend the final texts to everyone wishing to acquire first-
hand knowledge about some of the exciting and trendsetting developments in
functional programming.
We are grateful to our sponsors, to the Faculty of Mathematics and Com-
puter Science of the University of Tartu, to the lecturers and the second readers
for their hard work on the oral presentations, and the notes, and to all our
participants. You made the school what it was.
Host Institution
AFP 2004 was organized by the Department of Computer Science of the Univer-
sity of Tartu in cooperation with the Center for Dependable Computing (CDC),
an Estonian center of excellence in research.
Programme Committee
Varmo Vene (University of Tartu, Estonia) (chairman)
Johan Jeuring (Universiteit Utrecht, The Netherlands)
Tarmo Uustalu (Institute of Cybernetics, Tallinn, Estonia)
Organizing Committee
Varmo Vene (University of Tartu, Estonia) (chairman)
Härmel Nestra (University of Tartu, Estonia)
Vesal Vojdani (University of Tartu, Estonia)
Tarmo Uustalu (Institute of Cybernetics, Tallinn, Estonia)
Second Readers
Venanzio Capretta (University of Ottawa, Canada)
James Cheney (University of Edinburgh, UK)
Catarina Coquand (Chalmers University of Technology, Sweden)
Jeremy Gibbons (University of Oxford, UK)
Thomas Hallgren (Oregon Graduate Institute, Portland, OR, USA)
Michael Hanus (Christian-Albrechts-Universität zu Kiel, Germany)
Johan Jeuring (Universiteit Utrecht, The Netherlands)
Jerzy Karczmarczuk (Université Caen, France)
Ralf Lämmel (CWI, Amsterdam, The Netherlands)
Andres Löh (Universiteit Utrecht, The Netherlands)
Nicolas Magaud (University of New South Wales, Sydney, Australia)
Simon Marlow (Microsoft Research, Cambridge, UK)
Ross Paterson (City University, London, UK)
Simon Peyton Jones (Microsoft Research, Cambridge, UK)
Colin Runciman (University of York, UK)
Tim Sheard (Portland State University, Portland, OR, USA)
Joost Visser (Universidade do Minho, Braga, Portugal)
Eric Van Wyk (University of Minnesota, Minneapolis, MN, USA)
VIII Organization
Sponsoring Institutions
Tiigriülikool programme of the Estonian Information Technology Foundation
National Centers of Excellence programme of the Estonian Ministry of Education
and Research
EU FP5 IST programme via the thematic network project APPSEM II
Table of Contents
Abstract. A great deal has been written about type systems. Much less has been
written about implementing them. Even less has been written about implementa-
tions of complete compilers in which all aspects come together. This paper fills
this gap by describing the implementation of a series of compilers for a simplified
variant of Haskell. By using an attribute grammar system, aspects of a compiler
implementation can be described separately and added in a sequence of steps,
thereby giving a series of increasingly complex (working) compilers. Also, the
source text of both this paper and the executable compilers come from the same
source files by an underlying minimal weaving system. Therefore, source and
explanation is kept consistent.
Haskell98 [31] is a complex language, not to mention its more experimental incarna-
tions. Though also intended as a research platform, realistic compilers for Haskell [1]
have grown over the years and understanding and experimenting with those compilers
is not an easy task. Experimentation on a smaller scale usually is based upon relatively
simple and restricted implementations [20], often focusing only on a particular aspect
of the language and/or its implementation. This paper aims at walking somewhere be-
tween this complexity and simplicity by
– Describing the implementation of essential aspects of Haskell (or any other (func-
tional) programming language), hence the name Essential Haskell (EH) used for
simplified variants of Haskell1 in these notes.
– Describing these aspects separately in order to provide a better understanding.
– Adding these aspects on top of each other in an incremental way, thus leading to
a sequence of compilers, each for a larger subset of complete Haskell (and exten-
sions).
– Using tools like the Utrecht University Attribute Grammar (UUAG) system [3],
hereafter referred to as the AG system, to allow for separate descriptions for the
various aspects.
1 The ’E’ in EH might also be expanded to other aspects of the compiler, like being an Example.
V. Vene and T. Uustalu (Eds.): AFP 2004, LNCS 3622, pp. 1–72, 2005.
c Springer-Verlag Berlin Heidelberg 2005
2 A. Dijkstra and S.D. Swierstra
The remaining sections of this introduction will expand on this by looking at the inten-
tions, purpose and limitations of these notes in more detail. This is followed by a short
description of the individual languages for which we develop compilers throughout
these notes. The last part of the introduction contains a small tutorial on the AG system
used in these notes. After the introduction we continue with discussing the implementa-
tion of the first three compilers (sections 2, 3 and 4) out of a (currently) sequence of ten
compilers. On the web site [11] for this project the full distribution of the code for these
compilers can be found. We conclude these notes by reflecting upon our experiences
with the AG system and the creation of these notes (section 5).
1.1 Purpose
– For students who wish to learn more about the implementation of functional lan-
guages. This paper also informally explains the required theory, in particular about
type systems.
– For researchers who want to build (e.g.) a prototype and to experiment with ex-
tensions to the type system and need a non-trivial and realistic starting point. This
paper provides documentation, design rationales and an implementation for such a
starting point.
– For those who wish to study a larger example of the tools used to build the com-
pilers in these notes. We demonstrate the use of the AG system, which allows us to
separately describe the various aspects of a language implementation. Other tools
for maintaining consistency between different versions of the resulting compilers
and the source code text included in these notes are also used, but will not be dis-
cussed.
Although informally and concisely introduced where necessary, familiarity with the
following will make reading and understanding these notes easier:
For those not familiar with the AG system a short tutorial has been included at the end of
this introduction (see section 1.4). It also demonstrates the use of the parser combinators
used throughout the implementation of all EH versions.
We expect that by findinga balance between theory and implementation, we serve both
those who want to learn and those who want to do research. It is also our belief that by
splitting the big problem into smaller aspects the combination can be explained in an
easier way.
In the following sections we give examples of the Haskell features present in the series
of compilers described in the following chapters. Only short examples are given, so the
reader gets an impression of what is explained in more detail and implemented in the
relevant versions of the compiler.
Though all compilers described in these notes deal with a different issue, they all have
in common that they are based on the λ-calculus, most of the time using the syntax
and semantics of Haskell. The first version of our series of compilers therefore accepts
a language that most closely resembles the λ-calculus, in particular typed λ-calculus
extended with let expressions and some basic types and type constructors such as Int,
Char and tuples.
4 A. Dijkstra and S.D. Swierstra
let i :: Int
i=5
in i
Functions accept one parameter only, which can be a pattern. All types are monomor-
phic.
let id = λx → x
in let v = id 3
in id
let id = λx → x
in id 3
Also missing are features which fall in the category syntactic sugar, programming in the
large and the like. Haskell incorporates many features which make programming easier
and/or manageable. Just to mention a few:
We have deliberately not dealt with these issues. Though necessary and convenient we
feel that these features should be added after all else has been dealt with, so as not to
make understanding and implementating essential features more difficult.
The remaining part of the introduction contains a small tutorial on the AG system. The
tutorial explains the basic features of the AG system. The explanation of remaining
features is postponed to its first use throughout the main text. These places are marked
with AG. The tutorial can safely be skipped if the reader is already familiar with the
AG system.
Haskell and Attribute Grammars (AG). Attribute grammars can be mapped onto func-
tional programs [23,19,4]. Vice versa, the class of functional programs (catamorphisms
6 A. Dijkstra and S.D. Swierstra
[39]) mapped onto can be described by attribute grammars. The AG system exploits this
correspondence by providing a notation (attribute grammar) for computations over trees
which additionally allows program fragments to be described separately. The AG com-
piler gathers these fragments, combines these fragments, and generates a corresponding
Haskell program.
In this AG tutorial we start with a small example Haskell program (of the right form)
to show how the computation described by this program can be expressed in the AG
notation and how the resulting Haskell program generated by the AG compiler can be
used. The ‘repmin’ problem [4] is used for this purpose. A second example describing a
‘pocket calculator’ (that is, expressions) focusses on more advanced features and typical
AG usage patterns.
Repmin a la Haskell. Repmin stands for “replacing the integer valued leaves of a tree
by the minimal integer value found in the leaves”. The solution to this problem requires
two passes over a tree structure, computing the miminum and computing a new tree
with the minimum as its leaves respectively. It is often used as the typical example of
a circular program which lends itself well to be described by the AG notation. When
described in Haskell it is expressed as a computation over a tree structure:
The computation itself simultaneously computes the minimum of all integers found in
the leaves of the tree and the new tree with this minimum value. The result is returned
as a tuple computed by function r:
tr = Tree Bin (Tree Leaf 3) (Tree Bin (Tree Leaf 4) (Tree Leaf 5))
tr = repmin tr
main :: IO ()
main = print tr
The computation of the new tree requires the minimum. This minimum is passed as a
parameter m to r at the root of the tree by extracting it from the result of r. The result
tuple of the invocation r t tmin depends on itself via the minimum tmin so it would
seem we have a cyclic definition. However, the real dependency is not on the tupled
result of r but on its elements because it is the element tmin of the result tuple which
is passed back and not the tuple itself. The elements are not cyclically dependent so
Haskell’s laziness prevents a too eager computation of the elements of the tuple which
might otherwise have caused an infinite loop during execution. Note that we have two
more or less independent computations that both follow the tree structure, and a weak
interaction, when passing the tmin value back in the tree.
Repmin a la AG. The structure of repmin is similar to the structure required by a com-
piler. A compiler performs several computations over an abstract syntax tree (AST), for
example for computing its type and code. This corresponds to the Tree structure used by
repmin and the tupled results. In the context of attribute grammars the elements of this
tuple are called attribute’s. Occasionaly the word aspect is used as well, but an aspect
may also refer to a group of attributes associated with one particular feature of the AST,
language or problem at hand.
Result elements are called synthesized attributes. On the other hand, a compiler may
also require information that becomes available at higher nodes in an AST to be avail-
able at lower nodes in an AST. The m parameter passed to r in repmin is an example of
this situation. In the context of attribute grammars this is called an inherited attribute.
Using AG notation we first define the AST corresponding to our problem (for which
the complete compilable solution is given in Fig. 1):
DATA Tree
| Leaf int : {Int }
| Bin lt : Tree
rt : Tree
The DATA keyword is used to introduce the equivalent of Haskell’s data type. A
DATAnode defines a node node (or nonterminal) of an AST. Its alternatives, enu-
merated one by one after the vertical bar |, are called variants, productions. The term
constructor is occasionally used to stress the similarity with its Haskell counterpart.
Each variant has members, called children if they refer to other nodes of the AST and
fields otherwise. Each child and field has a name (before the colon) and a type (after
the colon). The type may be either another DATA node (if a child) or a monomorphic
Haskell type (if a field), delimited by curly braces. The curly braces may be omitted if
the Haskell type is a single identifier. For example, the DATA definition for the repmin
problem introduces a node (nonterminal) Tree, with variants (productions) Leaf and
Bin. A Bin has children lt and rt of type Tree. A Leaf has no children but contains only
a field int holding a Haskell Int value.
8 A. Dijkstra and S.D. Swierstra
The keyword ATTR is used to declare an attribute for a node, for instance the synthe-
sized attribute min:
DATA Tree
| Leaf int : {Int }
| Bin lt : Tree
rt : Tree
ATTR Tree [|| min : Int ]
SEM Tree
| Leaf lhs . min = @int
| Bin lhs . min = @lt.min ‘min‘ @rt.min
ATTR Tree [rmin : Int ||]
-- The next SEM may be generated automatically
SEM Tree
| Bin lt . rmin = @lhs.rmin
rt . rmin = @lhs.rmin
DATA Root
| Root tree : Tree
SEM Root
| Root tree. rmin = @tree.min
ATTR Root Tree [|| tree : Tree]
SEM Tree
| Leaf lhs . tree = Tree Leaf @lhs.rmin
| Bin lhs . tree = Tree Bin @lt.tree @rt.tree
-- The next SEM may be generated automatically
SEM Root
| Root lhs . tree = @tree.tree
DERIVING Tree : Show
{
tr = Tree Bin (Tree Leaf 3) (Tree Bin (Tree Leaf 4) (Tree Leaf 5))
tr = sem Root (Root Root tr)
main :: IO ()
main = print tr
}
A synthesized attribute is declared for the node after ATTR. Multiple declarations of
the same attribute for different nonterminals can be grouped on one line by enumerat-
ing the nonterminals after the ATTR keyword, separated by whitespace. The attribute
declaration is placed inside the square brackets at one or more of three different pos-
sible places. All attributes before the first vertical bar | are inherited, after the last bar
synthesized, and in between both inherited and synthesized. For example, attribute min
is a result and therefore positioned as a synthesized attribute, after the last bar.
Rules relating an attribute to its value are introduced using the keyword SEM. For
each production we distinguish a set of input attributes, consisting of the synthesized
attributes of the children referred to by @child.attr and the inherited attributes of
the parent referred to by @lhs.attr. For each output attribute we need a rule that
expresses its value in terms of input attributes and fields.
The computation for a synthesized attributefora node hasto be defined for each variant
individually as it usually will differ between variants. Each rule is of the form
If multiple rules are declared for a variant of a node, the variant part may be shared.
The same holds for multiple rules for a child (or lhs) of a variant, the child (or lhs)
may then be shared.
The text representing the computation for an attribute has to be a Haskell expression and
will end up almost unmodified in the generated program, without any form of checking.
Only attribute and field references, starting with a @, have meaning to the AG system.
The text, possibly stretching over multiple lines, has to be indented at least as far as its
first line. Otherwise it is to be delimited by curly braces.
The basic form of an attribute reference is @node.attr referring to a synthesized
attribute attr of child node node. For example, @lt.min refers to the synthesized
attribute min of child lt of the Bin variant of node Tree.
The node. part of @node.attr may be omitted. For example, min for the Leaf
alternative is defined in terms of @int. In that case @attr refers to a locally (to
a variant for a node) declared attribute, or to the field with the same name as defined
in the DATA definition for that variant. This is the case for the Leaf variant’s int. We
postpone the discussion of locally declared attributes.
The minimum value of repmin passed as a parameter corresponds to an inherited at-
tribute rmin:
The value of rmin is straightforwardly copied to its children. This “simply copy” be-
havior occurs so often that we may omit its specification. The AG system uses so called
copy rules to automically generate code for copying if the value of an attribute is not
specified explicitly. This is to prevent program clutter and thus allows the programmer
10 A. Dijkstra and S.D. Swierstra
to focus on programming the exception instead of the usual. We will come back to this
later; for now it suffices to mention that all the rules for rmin might as well have been
omitted.
The original repmin function did pass the minimum value coming out r back into r
itself. This did happen at the top of the tree. Similarly we define a Root node sitting on
top of a Tree:
DATA Root
| Root tree : Tree
At the root the min attribute is passed back into the tree via attribute rmin:
SEM Root
| Root tree.rmin = @tree.min
SEM Root
| Root lhs.tree = @tree.tree
For each DATA the AG compiler generates a corresponding Haskell data type declara-
tion. For each node node a data type with the same name node is generated. Since
Haskell requires all constructors to be unique, each constructor of the data type gets a
name of the form node variant.
In our example the constructed tree is returned as the one and only attribute of Root. It
can be shown if we tell the AG compiler to make the generated data type an instance of
the Show class:
Similarly to the Haskell version of repmin we can now show the result of the attribute
computation as a plain Haskell value by using the function sem Root generated by the
AG compiler:
{
tr = Tree Bin (Tree Leaf 3) (Tree Bin (Tree Leaf 4) (Tree Leaf 5))
tr = sem Root (Root Root tr)
main :: IO ()
main = print tr
}
Typing Haskell with an Attribute Grammar 11
Because this part is Haskell code it has to be delimited by curly braces, indicating that
the AG compiler should copy it unchanged into the generated Haskell program.
In order to understand what is happening here, we take a look at the generated Haskell
code. For the above example the following code will be generated (edited to remove
clutter):
In general, generated code is not the most pleasant2 of prose to look at, but we will have
to use the generated functions in order to access the AG computations of attributes from
the Haskell world. The following observations should be kept in mind when doing so:
– For node node also a type T node is generated, describing the function type
that maps inherited to synthesized attributes. This type corresponds one-to-one to
the attributes defined for node: inherited attributes to parameters, synthesized at-
tributes to elements of the result tuple (or single type if exactly one synthesized
attribute is defined).
– Computation of attribute values is done by semantic functions with a name of the
form sem node variant. These functions have exactly the same type as their
constructor counterpart of the generated data type. The only difference lies in the
parameters which are of the same type as their constructor counterpart, but prefixed
with T . For example, data constructor Tree Bin :: Tree → Tree → Tree corresponds
to the semantic function sem Tree Bin :: (T Tree) → (T Tree) → (T Tree).
– A mapping from the Haskell data type to the corresponding semantic function is
available with the name sem node.
In the Haskell world one now can follow different routes to compute the attributes:
– First construct a Haskell value of type node, then apply sem node to this value
and the additionally required inherited attributes values. The given function main
from AG variant of repmin takes this approach.
– Circumvent the construction of Haskell values of type node by using the semantic
functions sem node variant directly when building the AST instead of the data
constructor node variant (This technique is called deforestation [42].).
In both cases a tuple holding all synthesized attributes is returned. Elements in the
tuple are sorted lexicographically on attribute name, but it still is awkward to extract
an attribute via pattern matching because the size of the tuple and position of elements
changes with adding and renaming attributes. For now, this is not a problem as sem Root
will only return one value, a Tree. Later we will see the use of wrapper functions to pass
inherited attributes and extract synthesized attributes via additional wrapper data types
holding attributes in labeled fields.
Parsing directly to semantic functions. The given main function uses the first approach:
construct a Tree, wrap it inside a Root, and apply sem Root to it. The following example
takes the second approach; it parses some input text describing the structure of a tree
and directly invokes the semantic functions:
2 In addition, because generated code can be generated differently, one cannot count on it being
generated in a specific way. Such is the case here too, this part of the AG implementation may
well change in the future.
Typing Haskell with an Attribute Grammar 13
The parser recognises the letter ’B’ as a Bin alternative and a single digit as a Leaf .
Fig. 2 gives an overview of the parser combinators which are used [38]. The parser is
invoked from an alternative implementation of main:
main :: IO ()
main = do tr ← parseIOMessage show pRepmin "B3B45"
print tr
We will not discuss this alternative further nor will we discuss this particular variant of
parser combinators. However, this approach is taken in the rest of these notes wherever
parsing is required.
More features and typical usage: a pocket calculator. We will continue with looking at
a more complex example, a pocket calculator which accepts expressions. The calculator
prints a pretty printed version of the entered expression, its computed value and some
statistics (the number of additions performed). An interactive terminal session of the
pocket calculator looks as follows:
14 A. Dijkstra and S.D. Swierstra
$ build/bin/expr
Enter expression: 3+4
Expr=’3+4’, val=7, add count thus far=1
Enter expression: [a=3+4:a+a]
Expr=’[a=3+4:a+a]’, val=14, add count thus far=3
Enter expression: ˆCexpr: interrupted
$
This rudimentary calculator allows integer values, their addition and binding to
identifiers. Parsing is character based, no scanner is used to transform raw text into
tokens. No whitespace is allowed and a let expression is syntactically denoted by
[<nm>=<expr>:<expr>].
The example will allow us to discuss more AG features as well as typical use of AG.
We start with integer constants, addition followed by an attribute computation for the
pretty printing:
DATA AGItf
| AGItf expr : Expr
DATA Expr
| IConst int : {Int }
| Add e1 : Expr e2 : Expr
SET AllNT = AGItf Expr
The root of the tree is now called AGItf to indicate (as a naming convention) that this is
the place where interfacing between the Haskell world and the AG world takes place.
The definition demonstrates the use of the SET keyword which allows the naming of
a group of nodes. This name can later be used to declare attributes for all the named
group of nodes at once.
The computation of a pretty printed representation follows the same pattern as the
computation of min and tree in the repmin example, because of its compositional and
bottom-up nature. The synthesized attribute pp is synthesized from the values of the pp
attribute of the children of a node:
The pretty printing uses a pretty printing library with combinators for values of type
PP Doc representing pretty printed documents. The library is not further discussed
here; an overview of some of the available combinators can be found in Fig. 3.
As a next step we add let expressions and use of identifiersin expressions.This demon-
strates an important feature of the AG system: we may introduce new alternatives for
a node as well as may introduce new attribute computations in a separate piece of
program text. We first add new AST alternatives for Expr:
Typing Haskell with an Attribute Grammar 15
Combinator Result
p1 > < p2 p1 besides p2 , p2 at the right
p1 >#< p2 same as > < but with an additional space in between
p1 >−< p2 p1 above p2
pp parens p p inside parentheses
text s string s as PP Doc
pp x pretty print x (assuming instance PP x) resulting in a PP Doc
DATA Expr
| Let nm : {String} val : Expr body : Expr
| Var nm : {String}
One should keep in mind that the exensibility offered is simplistic of nature, but sur-
prisingly flexible at the same time. The idea is that node variants, attribute declarations
and attribute rules for node variants can all occur textually separated. The AG com-
piler gathers all definitions, combines, performs several checks (e.g. are attribute rules
missing), and generates the corresponding Haskell code. All kinds of declarations can
be distributed over several text files to be included with a INCLUDE directive (not
discussed any further).
Any addition of new node variants requires also the corresponding definitionsof already
introduced attributes:
SEM Expr
| Let lhs.pp = "[" > < @nm > < "=" > < @val.pp > < ":" > < @body.pp > < "]"
| Var lhs.pp = pp @nm
The scope is enforced by extending the inherited attribute env top-down in the AST.
Note that there is no need to specify a value for @val.env because of the copy rules
discussed later. In the Let variant the inherited environment, which is used for evaluating
the right hand sided of the bound expression, is extended with the new binding, before
being used as the inherited env attribute of the body. The environment env is queried
when the value of an expression is to be computed:
16 A. Dijkstra and S.D. Swierstra
The attribute val holds this computed value. Because its value is needed in the ‘out-
side’ Haskell world it is passed through AGItf (as part of SET AllNT) as a synthesized
attribute. This is also the case for the previously introduced pp attribute as well as the
following count attribute used to keep track of the number of additions performed.
However, the count attribute is also passed as an inherited attribute. Being both inher-
ited and synthesized it is defined between the two vertical bars in the ATTR declaration
for count:
The attribute count is said to be threaded through the AST, the AG solution to a global
variable or the use of state monad. This is a result of the attribute being inherited as well
as synthesized and the copy rules. Its effect is an automatic copying of the attribute in a
preorder traversal of the AST.
Copy rules are attribute rules inserted by the AG system if a rule for an attribute attr
in a production of node is missing. AG tries to insert a rule that copies the value of
another attribute with the same name, searching in the following order:
1. Local attributes.
2. The synthesized attribute of the children to the left of the child for which an inher-
ited attr definition is missing, with priority given to the nearest child fulfilling the
condition. A synthesized attr of a parent is considered to be at the right of any
child’s attr .
3. Inherited attributes (of the parent).
In our example the effect is that for the Let variant of Expr
Similar copy rules are inserted for the other variants. Only for variant Add of Expr a
different rule for @lhs.count is explicitly specified, since here we have a non-trivial
piece of semantics: i.e. we actually want to count something.
Automatic copy rule insertion can be both a blessing and curse. A blessing because it
takes away a lot of tedious work and minimises clutter in the AG source text. On the
Typing Haskell with an Attribute Grammar 17
other hand it can be a curse, because a programmer may have forgotten an otherwise
required rule. If a copy rule can be inserted the AG compiler will silently do so, and the
programmer will not be warned.
As with our previous example we can let a parser map input text to the invocations of
semantic functions. For completeness this source text has been included in Fig. 4. The
result of parsing combined with the invocation of semantic functions will be a function
taking inherited attributes to a tuple holding all synthesized attributes. Even though the
order of the attributes in the result tuple is specified, its extraction via pattern matching
should be avoided. The AG system can be instructed to create a wrapper function which
knows how to extract the attributes out of the result tuple:
WRAPPER AGItf
The attribute values are stored in a data type with labeled fields for each attribute. The
attributes can be accessed with labels of the form attr Syn node. The name of the
wrapper is of the form wrap node; the wrapper function is passed the result of the
semantic function and a data type holding inherited attributes:
run :: Int → IO ()
run count
= do hPutStr stdout "Enter expression: "
hFlush stdout
l ← getLine
r ← parseIOMessage show pAGItf l
let r = wrap AGItf r (Inh AGItf {count Inh AGItf = count })
putStrLn ("Expr=’" ++ disp (pp Syn AGItf r ) 40 "" ++
"’, val=" ++ show (val Syn AGItf r ) ++
", add count thus far=" ++ show (count Syn AGItf r )
)
run (count Syn AGItf r )
main :: IO ()
main = run 0
We face a similar problem with the passing of inherited attributes to the semantic func-
tion. Hence inherited attributes are passed to the wrapper function via a data type with
name Inh node and a constructor with the same name, with fields having labels of the
form attr Inh node. The count attribute is an example of an attribute which must be
passed as an inherited attribute as well as extracted as a synthesized attribute.
This concludes our introduction to the AG system. Some topics have either not been
mentioned at all or only shortly touched upon. We provide a list of those topics together
with a reference to the first use of the features which are actually used later in these
notes. Each of these items is marked with AG to indicate that it is about the AG system.
2 EH 1: Typed λ- Calculus
In this section we build the first version of our series of compilers: the typed λ-calculus
packaged in Haskell syntax in which all values need to explicitly be given a type. The
compiler checks if the specified types are in agreement with actual value definitions.
For example
let i :: Int
i=5
in i
is accepted, whereas
let i :: Char
i=5
in i
produces a pretty printed version of the erroneous program, annotated with errors:
Typing Haskell with an Attribute Grammar 19
let i :: Char
i = 5
{- ***ERROR(S):
In ‘5’:
Type clash:
failed to fit: Int <= Char
problem with : Int <= Char -}
{- [ i:Char ] -}
in i
Type signatures have to be specified for identifiers bound in a let expression. For λ-
expressions the type of the parameter can be extracted from these type signatures unless
a λ-expression occurs at the position of an applied function. In that case a type signature
for the λ-expression is required in the expression itself. This program will not typecheck
because this EH version does not allow polymorphic types in general and on higher
ranked (that is, parameter) positions in particular.
The implementation of a type system will be the main focus of this and following sec-
tions. As a consequence the full environment/framework needed to build a compiler
will not be discussed. This means in particular that error reporting, generation of a
pretty printed annotated output, parsing and the compiler driver are not described.
We start with the definition of the AST and how it relates to concrete syntax, followed
by the introduction of several attributes required for the implementation of the type
system.
DATA Expr
| IConst int : {Int }
| CConst char : {Char }
| Con nm : {HsName}
| Var nm : {HsName}
| App func : Expr
arg : Expr
| Let decls : Decls
body : Expr
| Lam arg : PatExpr
body : Expr
| AppTop expr : Expr
| Parens expr : Expr
| TypeAs tyExpr : TyExpr
expr : Expr
AG: Type synonyms (for lists). The AG notation allows type synomyms for one spe-
cial case, AG’s equivalent of a list. It is an often occurring idiom to encode a list of
nodes, say DATA L with elements node as:
DATA L
| Cons hd : node
tl : L
| Nil
TYPE L = [node]
The EH fragment (which is incorrect for this version of because type signatures are
missing)
AGItf_AGItf
Expr_Let
Decls_Cons
Typing Haskell with an Attribute Grammar 21
Decl_Val
PatExpr_VarAs "ic"
PatExpr_AppTop
PatExpr_App
PatExpr_App
PatExpr_Con ",2"
PatExpr_Var "i"
PatExpr_Var "c"
Expr_AppTop
Expr_App
Expr_App
Expr_Con ",2"
Expr_IConst 5
Expr_CConst ’x’
Decls_Cons
Decl_Val
PatExpr_Var "id"
Expr_Lam
PatExpr_Var "x"
Expr_Var "x"
Decls_Nil
Expr_AppTop
Expr_App
Expr_Var "id"
Expr_Var "i"
The example also demonstrates the use of patterns, which is almost the same as in
Haskell: EH does not allow a type signature for the elements of a tuple.
Looking at this example and the rest of the abstract syntax in Fig. 5 we can make several
observations of what one is allowed to write in EH and what can be expected from the
implementation.
– There is a striking similarity between the structure of expressions Expr and patterns
PatExpr (and as we will see later type expressions TyExpr): they all contain App
and Con variants. This similarity will sometimes be exploited to factor out common
code, and, if factoring out cannot be done, leads to similarities between pieces of
code. This is the case with pretty printing(not included in these notes), which is
quite similar for the different kinds of constructs.
– Type signatures (Decl TySig) and value definitions (Decl Val) may be freely mixed.
However, type signatures and value definitions for the same identifier are still re-
lated.
– Because of the textual decoupling of value definitions and type signatures, a type
signature may specify the type for an identifier occurring inside a pattern:
22 A. Dijkstra and S.D. Swierstra
let a :: Int
(a, b) = (3, 4)
in ...
DATA AGItf
| AGItf expr : Expr
DATA Decl
| TySig nm : {HsName}
tyExpr : TyExpr
| Val patExpr : PatExpr
expr : Expr
TYPE Decls = [Decl]
SET AllDecl = Decl Decls
DATA PatExpr
| IConst int : {Int }
| CConst char : {Char }
| Con nm : {HsName}
| Var nm : {HsName}
| VarAs nm : {HsName}
patExpr : PatExpr
| App func : PatExpr
arg : PatExpr
| AppTop patExpr : PatExpr
| Parens patExpr : PatExpr
SET AllPatExpr = PatExpr
DATA TyExpr
| Con nm : {HsName}
| App func : TyExpr
arg : TyExpr
| AppTop tyExpr : TyExpr
| Parens tyExpr : TyExpr
SET AllTyExpr = TyExpr
SET AllExpr = Expr
SET AllNT = AllTyExpr AllDecl AllPatExpr AllExpr
– In EH composite values are created by tupling, denoted by (. . , . .). The same no-
tation is also used for patterns (for unpacking a composite value) and types (de-
scribing the structure of the composite). In all these cases the corresponding AST
consists of a Con applied to the elements of the tuple. For example, the value (2, 3)
corresponds to
Expr App (Expr App (Expr Con ",2") (Expr IConst 2)) (Expr IConst 3)
– For now there is only one value constructor: for tuples. The EH constructor for
tuples also is the one which needs special treatment because it actually stands for a
infinite family of constructors. This can be seen in the encoding of the name of the
constructor which is composed of a "," together with the arity of the constructor.
For example, the expression (3, 4) is encoded as an application App of Con ",2"
to the two Int arguments: (,2 3 4). In our examples we will follow the Haskell
convention, in which we write (,) instead of ‘,2’. By using this encoding we also
get the unit type () as it is encoded by the name ",0".
– The naming convention for tuples and other naming conventions are available
through the following definitions for Haskell names HsName.
– Each application is wrapped on top with an AppTop. This has no meaning in itself
but it simplifies the pretty printing of expressions3 . We need AppTop for patterns,
but for the rest it can be ignored.
– The location of parentheses around an expression is remembered by a Parens alter-
native. We need this for the reconstruction of the parenthesis in the input.
3 As it also complicates parsing it may disappear in future versions of EH.
24 A. Dijkstra and S.D. Swierstra
– AGItf is the top of a complete abstract syntax tree. As noted in the AG primer this
is the place where interfacing with the ‘outside’ Haskell world takes place. It is
a convention in these notes to give all nonterminals in the abstract syntax a name
with AGItf in it, if it plays a similar role.
2.2 Types
We will now turn our attention to the way the type system is incorporated into EH1.
We focus on the pragmatics of the implementation and less on the corresponding type
theory.
σ = Int | Char
| (σ, ..., σ)
| σ→σ
The following definition however is closer to the one used in our implementation:
where it will prove useful in expressing the application of type constructors to types.
Here we just have to make sure no types like Int Int will be created; in a (omitted) later
version of EH we perform kind inferencing/checking to prevent the creation of such
types from showing up.
The corresponding encoding using AG notation differs in the presence of an Any type,
also denoted by . In section 2.3 we will say more about this. It is used to smoothen the
type checking by (e.g.) limiting the propagation of erroneous types:
DATA TyAGItf
| AGItf ty : Ty
DATA Ty
| Con nm : {HsName}
| App func : Ty
arg : Ty
| Any
The formal system and implementation of this system use different symbols to refer
to the same concept. For example, Any in the implementation is the same as in the
typing rules. Not always is such a similarity pointed out explicitly but instead a notation
name1 ∥name2 is used to simultaneously refer to both symbols name1 and name2 , for
example Any∥. The notation also implies that the identifiers and symbols separated
by ’∥’ are referring to the same concept.
The definition of Ty will be used in both the Haskell world and the AG world. InHaskell
we use the corresponding data type generated by the AG compiler, for example in the
derived type TyL:
type TyL = [Ty]
The data type is used to construct type representations. In the AG world we define
computations over the type structure in terms of attributes. The corresponding semantic
functions generated by the AG system can then be applied to Haskell values.
Type rules. For example, the following is the typing rule (taken from Fig. 6) for function
application
expr
Γ e 2 : σa
expr
Γ e1 : σa → σ
expr
(e-app1)
Γ e1 e2 : σ
26 A. Dijkstra and S.D. Swierstra
It states that an application of e1 to e2 has type σ provided that the argument has type
σa and the function has a type σa → σ.
expr
Γ e:σ
expr
Γ e2 : σa
expr expr
Γ e1 : σ a → σ i → σi , Γ e : σe
expr (e-app1) expr (e-lam1)
Γ e1 e2 : σ Γ λi → e : σi → σe
expr expr
Γ e2 : σ 2 i → σi , Γ ei : σi
expr expr
Γ e1 : σ 1 i→ σi , Γ e : σe
expr (e-prod1) expr (e-let1)
Γ (e1 , e2 ) : (σ1 , σ2 ) Γ let i :: σi ; i = ei in e : σe
(i → σ) ∈ Γ
expr (e-ident1) expr (e-int1)
Γ i:σ Γ minint . . maxint : Int
The part “ more results” needs not always be present if there are no more results for
a judgement. The notation reads as
Typing Haskell with an Attribute Grammar 27
If the context or more results itself consists of multiple parts, these parts are separated
by a semicolon ’;’. An underscore ’ ’ has a similar role as in Haskell to indicate a
property is not relevant for a type rule (see rule e-app1B, Fig. 7)
Although a rule formally is to be interpreted purely equational, it may help to realise
that from an implementors point of view this (more or less) corresponds to an imple-
mentation template, either in the form of a function judgetype:
judgetype = λconstruct →
λcontext → ...(property, more results)
or a piece of AG:
Typing rules and implementation templates differ in that the latter prescribes the order in
which the computation of a property takes place, whereas the former simply postulates
relationships between parts of a rule. In general typing rules presented throughout these
notes will be rather explicit in the flow of information and thus be close to the actual
implementation.
Environment. The rules in Fig. 6 refer to Γ, which is often called assumptions, envi-
ronment or context because it provides information about what may be assumed about
identifiers. Identifiers ξ are distinguished on the case of the first character, capitalized
I’s starting with an uppercase, uncapitalized i’s otherwise
ξ=i
| I
For type constants we will use capitalized identifiers I, whereas for identifiers bound to
an expression in a let-expression we will use lower case identifiers (i, j, ...).
An environment Γ is a vector of bindings, a partial finite map from identifiers to types:
Γ=ξ→σ
match. For rules this does not make a difference, for the implementation there is a
direction involved as we either construct from smaller parts or deconstruct (pattern
match) into smaller parts.
If shadowing is involved, that is duplicate entries are added, left /first (w.r.t. to the
comma ’,’) entries shadow right/later entries. In particular, when we locate some vari-
able in a Γ the first occurrence will be taken.
If convenient we will also use a list notation:
Γ = [ξ → σ]
This will be done if specific properties of a list are used or if we borrow from Haskell’s
repertoire of list functions. For simplicity we also use (assocation) lists in our imple-
mentation.
A list structure suffices to encode the presence of an identifier in a Γ, but it cannot
be used to detect multiple occurrences caused by duplicate introductions. Thus in our
implementation we use a stack of lists instead:
emptyGam = Gam [ [ ] ]
gamUnit kv = Gam [ [ (k, v) ] ]
gamLookup k (Gam ll) = foldr (λl mv → maybe mv Just (lookup k l))
Nothing ll
gamToAssocL (Gam ll) = concat ll
gamPushNew (Gam ll) = Gam ([ ] : ll)
gamPushGam g1 (Gam ll2) = Gam (gamToAssocL g1 : ll2)
gamAddGam g1 (Gam (l2 : ll2)) = Gam ((gamToAssocL g1 ++ l2) : ll2)
gamAdd kv = gamAddGam (k → v)
A specialization ValGam of Gam is used to store and lookup the type of value identifiers.
data ValGamInfo = ValGamInfo{vgiTy :: Ty} deriving Show
type ValGam = Gam HsName ValGamInfo
The type is wrapped in a ValGamInfo. Later versions of EH can add additional fields to
this data type.
valGamLookup :: HsName → ValGam → Maybe ValGamInfo
valGamLookup = gamLookup
Later the variant valGamLookup will do additional work, but for now it does not dif-
fer from gamLookup. The additional variant valGamLookupTy is specialized further to
produce an error message in case the identifier is missing from the environment.
Checking Expr. The rules in Fig. 6 do not provide much information about how the
type σ in the consequence of a rule is to be computed; it is just stated that it should relate
in some way to other types. However, type information can be made available to parts
of the abstract syntax tree, either because the programmer has supplied it somewhere or
because the compiler can reconstruct it. For types given by a programmer the compiler
has to check if such a type correctly describes the value of an expression for which the
type is given. This is called type checking. If no type information has been given for a
value, the compiler needs to reconstruct or infer this type based on the structure of the
abstract syntax tree and the semantics of the language as defined by the typing rules.
This is called type inferencing. In EH1 we exclusively deal with type checking.
We now can tailor the type rules in Fig. 6 towards an implementation which performs
type checking, in Fig. 7. We also start with the discussion of the corresponding AG
implementation. The rules now take an additional context, the expected (or known)
type σk (attribute knTy, simultaneously referred to by σk ∥knTy) as specified by the
programmer, defined in terms of AG as follows:
The basic idea underlying this implementation for type checking, as well as in later
versions of EH also for type inferencing, is that
– A known (or expected) type σk ∥knTy is passed top-down through the syntax tree of
an expression, representing the maximal type (in terms of , see Fig. 8 and discus-
30 A. Dijkstra and S.D. Swierstra
expr
Γ; σk e:σ
expr
Γ; σa e2 :
expr expr
Γ; → σk e1 : σ a → σ i → σi , Γ; σr e : σe
expr (e-app1B) expr (e-lam1B)
Γ; σk e1 e2 : σ Γ; σi → σr λi → e : σi → σe
expr expr
Γ; σk2 e2 : σ 2 i → σi , Γ; σi ei :
expr expr
Γ; σk1 e1 : σ 1 i→ σi , Γ; σk e : σe
expr (e-prod1B) expr (e-let1B)
Γ; (σk1 , σk2 ) (e1 , e2 ) : (σ1 , σ2 ) Γ; σk let i :: σi ; i = ei in e : σe
(i → σi ) ∈ Γ
fit fit
σi σk : σ Int σk : σ
expr (e-ident1B) expr (e-int1B)
Γ; σk i:σ Γ; σk minint . . maxint : σ
sion below) the type of an expression can be. At all places where this expression is
used it also is assumed that the type of this expression equals σk .
– A result type σ∥ty is computed bottom-up for each expression, representing the
minimal type (in terms of ) the expression can have.
– At each node in the abstract syntax tree it is checked whether σ σk holds. The
result of lhs rhs is rhs which is subsequently used by the type checker, for
example to simply return or use in constructing another, usually composite, type.
– In general, for lhs rhs the rhs is an expected type whereas lhs is the bottom-up
computed result type.
fit
σl σr : σ
fit
σa2 σa1 : σa
fit
σr1 σr2 : σr
(f-arrow1)
fit
σa1 → σr1 σa2 → σr2 : σa → σr
fit
σl1 σl2 : σl
fit
σr1 σr2 : σr I 1 ≡ I2
(f-prod1) (f-con1)
fit fit
(σl1 , σr1 ) (σl2 , σr2 ) : (σl , σr ) I 1 I2 : I 2
(f-anyl1) (f-anyr1)
fit fit
σ:σ σ :σ
The rules for also specify a result type. Strictly this result is not required for the fit
judgement to hold but in the implementation it is convenient to have the implementation
fitsIn of return the smallest type σ for which of σ1 σ and σ2 σ hold. This is
useful in particular in relation to the use of in in rule f-anyl1 and rule f-anyr1; we will
come back to this later.
For example, is used in rule e-int1B which checks that its actual Int type matches the
known type σk . The implementation of the type rule e-int1B performs this check and
returns the type σ in attribute ty:
AG: Set notation for variants. The rule for (e.g.) attribute fo is specified for IConst
and CConst together. Instead of specifying only one variant a whitespace separated
list of variant names may be specified after the vertical bar ’|’. It is also allowed to
32 A. Dijkstra and S.D. Swierstra
specify this list relative to all declared variants by specifying for which variants the
rule should not be declared. For example: ∗ − IConst CConst if the rule was to be
defined for all variants except IConst and CConst.
AG: Local attributes. The attribute fTy is declared locally. In this context ‘local’
means that the scope is limited to the variant of a node. Attribute fTy defined for
variant IConst is available only for other attribute rules for variant IConst of Expr.
Note that no explicit rule for synthesized attribute ty is required; a copy rule is
inserted to use the value of the locally declared attribute ty. This is a common AG
idiom when a value is required for later use as well or needs to be redefined in later
versions of EH.
The local attribute fTy (by convention) holds the type as computed on the basis of the ab-
stract syntax tree. This type fTy is subsequently compared to the expected type lhs.knTy
via the implementation fitsIn of the rules for fit∥ . In infix notation fitsIn prints as .
The function fitsIn returns a FIOut (fitsIn output) data structure in attribute fo. FIOut
consists of a record containing amongst other things field foTy:
Using a separate attribute fTy instead of using its value directly has been done in order
to prepare for a redefinition of fTy in later versions4 .
Ty Any∥Any∥ plays a special role. This type appears at two places in the implemen-
tation of the type system as a solution to the following problems:
– An error occurs at a place where the implementation of the type system needs a
type to continue (type checking) with. In that case is used to prevent further
errors from occurring. In this use of it represents a “dont’t care” of the type
system implementation. As such will be replaced by more a more specific type
as soon as it matches (via ) such a type.
SEM AGItf
| AGItf expr.knTy = Ty Any
The rule f-arrow1 in Fig. 8 for comparing function types compares the types for argu-
ments in the opposite direction. Only in later versions of EH when really behaves
asymmetrically we will discuss this aspect of the rules which is named contravariance.
In the rules in Fig. 8 the direction makes no difference; the correct use of the direction
for now only anticipates issues yet to come.
f it
The Haskell counterpart of σ1 σ2 : σ is implemented by fitsIn:
fitsIn :: Ty → Ty → FIOut
fitsIn ty1 ty2
= f ty1 ty2
where
res t = emptyFO{foTy = t }
f Ty Any t2 = res t2
f t1 Ty Any = res t1
f t1 @(Ty Con s1)
t2 @(Ty Con s2)
| s1 ≡ s2 = res t2
The function fitsIn checks whether the Ty App structure and all type constants Ty Con
are equal. If not, a non-empty list of errors is returned as well as type Ty Any∥Any∥.
Matching a composite type is split in two cases for Ty App, one for function types (the
first case), and one for the remaining type applications (the second case). For the current
EH version the second case only concerns tuple types. Both matches for composite
types use comp wich performs multiple ’s and combines the results. The difference
lies in the treatment of contravariant behavior as discussed earlier.
The type rules leave in the open how to handle a situation when a required constraint is
broken. For a compiler this is not good enough, being the reason fitsIn gives a “will-do”
type Ty Any back together with an error for later processing. Errors themselves are also
described via AG:
DATA Err
| UnifyClash ty1 : {Ty} ty2 : {Ty}
ty1detail : {Ty} ty2detail : {Ty}
DATA Err
| NamesNotIntrod nmL : {[HsName]}
The Err datatype is available as a datatype in the same way Ty is. The error datatype is
also used for signalling undeclared identifiers:
SEM Expr
| Var loc.(gTy, nmErrs)
= valGamLookupTy @nm @lhs.valGam
.fTy = @gTy
.fo = @fTy @lhs.knTy
.ty = foTy @fo
AG: Left hand side patterns. The simplest way to define a value for an attribute is to
define one value for one attribute at a time. However, if this value is a tuple, its fields
are to be extracted and assigned to individual attributes (as in tyArrowArgRes). AG
allows a pattern notation of the form(s) to make the notation for this situation more
concise:
Typing Haskell with an Attribute Grammar 35
Again, the error condition is signalled by a non empty list of errors if a lookup in Γ fails.
These errors are gathered so they can be incorporated into an annotated pretty printed
version of the program.
Typing rule e-ident1B uses the environment Γ to retrieve the type of an identifier. This
environment valGam for types of identifiers simply is declared as an inherited attribute,
initialized at the top of the abstrcat syntax tree. It is only extended with new bindings
for identifiers at a declaration of an identifier.
What is the knTy against which 3 will be checked? It is the argument type of the type of
id. However, in rule e-app1B and its AG implementation, the type of id is not the (top-
to-bottom travelling) σk ∥knTy, but it will be the argument part of the (bottom-to-top
travelling) resulting function type of e1 ∥func.ty:
SEM Expr
| App loc .knFunTy = [Ty Any] ‘mkTyArrow‘ @lhs.knTy
func.knTy = @knFunTy
(arg.knTy, loc.fTy) = tyArrowArgRes @func.ty
loc .ty = @fTy
The idea here is to encode the partially known function type as → σk (passed to
fun.knTy) and let fitsIn fill in the missing details, that is to find a type for . This is
the place where it is convenient to have fitsIn return a type in which ∥Ty Any’s are
replaced by a more concrete type. From that result the known/expected type of the
argument can be extracted.
Note that we are already performing a little bit of type inferencing. This is however
only done locally to App as the in → σk is guaranteed to have disappeared in the
result type of fitsIn. If this is not the case, the EH program contains an error. This is a
mechanism we repeatedly use, so we summarize it here:
36 A. Dijkstra and S.D. Swierstra
The type construction and inspection done in the App variant of Expr requires some
additional type construction functions, of which we only include mkTyArrow:
algTy :: MkConApp Ty
algTy = (Ty Con, Ty App, id, id)
mkTyArrow :: TyL → Ty → Ty
mkTyArrow = flip (foldr (mkArrow algTy))
mkArrow :: MkConApp t → t → t → t
mkArrow alg @(con, , , ) a r = mkApp alg [con hsnArrow, a, r ]
mkApp :: MkConApp t → [t ] → t
mkApp ( , app, top, ) ts
= case ts of
[t ] → t
→ top (foldl1 app ts)
A MkConApp contains four functions, for constructing a value similar to Con, App,
AppTop and IConst respectively. These functions are used by mkApp to build an App
like structure and by mkArrow to build function like structures. The code for (e.g.)
parsers (omitted from these notes), uses these functions parameterized with the proper
four semantics functions as generated by the AG system. So this additional layer of
abstraction improves code reuse. Similarly, function mkTyProdApp constructs a tuple
type out of types for the elements.
The functions used for scrutinizing a type are given names in which (by convention)the
following is encoded:
– What is scrutinized.
– What is the result of scrutinizing.
Typing Haskell with an Attribute Grammar 37
For example, tyArrowArgRes dissects a function type into its argument and result type.
If the scrutinized type is not a function, “will do” values are returned:
tyArrowArgRes t
= case t of
Ty App (Ty App (Ty Con nm) a) r
| hsnIsArrow nm → (a, r)
→ (Ty Any, t)
Similarly tyProdArgs is defined to return the types of the elements of a tuple type. The
code for this and other similar functions have been omitted for brevity.
Constructor Con, tuples. Apart from constructing function types only tupling allows us
to build composite types. The rule e-prod1B for tupling has no immediate counterpart
in the implementation because a tuple (a, b) is encoded as the application (, ) a b. The
alternative Con takes care of producing a type a → b → (a, b) for (, ).
SEM Expr
| Con loc.ty = let resTy = tyArrowRes @lhs.knTy
in tyProdArgs resTy ‘mkTyArrow‘ resTy
This type can be constructed from knTy which by definition has the form → →
(a, b) (for this example). The result type of this function type is taken apart and used to
produce the desired type. Also by definition (via construction by the parser) we know
the arity is correct.
Note that, despite the fact that the cartesian product constructors are essentially poly-
morphic, we do not have to do any kind of unification here, since they either appear in
the right hand side of declaration where the type is given by an explcit type declaration,
or they occur at an argument position where the type has been implicitly specified by
the function type. Therefore we indeed can use the a and b from type → → (a, b)
to construct the type a → b → (a, b) for the constructor (, ).
λ-expression Lam. For rule e-lam1B the check whether knTy has the form σ1 → σ2
is done by letting fitsIn match the knTy with → . The result (forced to be a func-
tion type) is split up by tyArrowArgRes into argument and result type. The function
gamPushNew opens a new scope on top of valGam so as to be able to check duplicate
names introduced by the pattern arg:
SEM Expr
| Lam loc .funTy = [Ty Any] ‘mkTyArrow‘ Ty Any
.foKnFun = @funTy @lhs.knTy
(arg.knTy, body.knTy) = tyArrowArgRes (foTy @foKnFun)
arg.valGam = gamPushNew @lhs.valGam
loc .ty = @lhs.knTy
38 A. Dijkstra and S.D. Swierstra
SEM Expr
| TypeAs loc .fo = @tyExpr.ty @lhs.knTy
expr.knTy = @tyExpr.ty
The obligation for the EH programmer to specify a type is dropped in later versions of
EH.
Checking PatExpr. Before we can look into more detail at the way new identifiers
are introduced in let- and λ-expressions we take a look at patterns. The rule e-let1B is
too restrictive for the actual language construct supported by EH because the rule only
allows a single identifier to be introduced. The following program allows inspection of
parts of a composite value by naming its components through pattern matching:
The rule e-let1C from Fig. 9 together with the rules for patterns from Fig. 10 reflects
the desired behaviour. These rules differ from those in Fig. 7 in that a pattern instead
of a single identifier is allowed in a value definition and the parameter position of a
λ-expression.
expr
Γ; σk e:σ
expr
Γp , Γ; σi ei :
expr
Γp , Γ; σk e : σe
pat
σi p : Γp
p ≡ i ∨ p ≡ i @...
expr (e-let1C)
Γ; σk let i :: σi ; p = ei in e : σe
expr
Γp , Γ; σr e : σe
pat
σp p : Γp
expr (e-lam1C)
Γ; σp → σr λp → e : σp → σe
Again the idea is to distribute a known type over the pattern by dissecting it into its con-
stituents. However, patterns do not return a type but type bindings for the identifiers in-
side a pattern instead. The new bindings are subsequently used in let- and λ-expressions
bodies.
A tuple pattern with rule p-prod1 is encoded in the same way as tuple expressions; that
is, pattern (a, b) is encoded as an application (a, b ) with an AppTop on top of it. We
pat
σk p : Γp
p p
dom (Γ1 ) ∩ dom (Γ2 ) = ∅
pat p
σk2 p2 : Γ 2
pat p
σk1 p1 : Γ 1
pat
(p-var1) pat
(p-prod1)
p p
σk i : [i → σk ] (σk1 , σk2 ) (p1 , p2 ) : Γ1 , Γ2
dissect the known type of a tuple in rule p-prod1 into its element types at AppTop using
function tyProdArgs. For this version of EH we only have tuple patterns; we can indeed
assume that we are dealing with a tuple type.
ATTR AllPatExpr [knTy : Ty ||]
ATTR PatExpr [knTyL : TyL ||]
SEM PatExpr
| AppTop loc .knProdTy
= @lhs.knTy
.(knTyL, aErrs)
= case tyProdArgs @knProdTy of
tL | @patExpr.arity ≡ length tL
→ (reverse tL, [ ])
→ (repeat Ty Any
, [Err PatArity
@knProdTy @patExpr.arity])
| App loc .(knArgTy, knTyL)
= hdAndTl @lhs.knTyL
arg.knTy = @knArgTy
The list of these elements is passed through attribute knTyL to all App’s of the pattern.
At each App one element of this list is taken as the knTy of the element AST.
40 A. Dijkstra and S.D. Swierstra
SEM Decl
| Val patExpr.knTyL = [ ]
SEM Expr
| Lam arg .knTyL = [ ]
As a result of this unpacking, at a Var alternative attribute knTy holds the type of the
variable name introduced. The type is added to attribute valGam that is threaded through
the pattern for gathering all introduced bindings:
The addition to valGam is encoded in the attribute addToGam, a function which only
adds a new entry if the variable name is not equal to an underscore ’ ’ and has not
been added previously via a type signature for the variable name, signalled by attribute
inclVarBind (defined later).
let f :: ...
f = λx → ...g ...
g :: ...
g = λx → ...f ...
in ...
In the body of f the type g must be known and vice-versa. There is no ordering of
what can be defined and checked first. In Haskell f and g together would be in the
same binding group.
– Textually separated signatures and value definitions.
let f :: ...
...
f = λx → ...
in ...
Syntactically the signature and value definition for an identifier need not be defined
adjacently or in any specific order.
In Haskell dependency analysis determines that f and g form a so-called binding group,
which contains declarations that have to be subjected to type analysis together. How-
ever, due to the obligatory presence of the type signatures in this version of EH it is
possible to first gather all signatures and only then type check the value definitions.
Therefore, for this version of EH it is not really an issue as we always require a sig-
nature to be defined. For later versions of EH it actually will become an issue, so for
simplicity all bindings in a let-expression are analysed together as a single (binding)
group.
Though only stating something about one combination of type signature and valuedef-
inition, rule e-let1C still describes the basic strategy. First extract all type signatures,
then distribute those signatures over patterns followed by expressions. The difference
lies in doing it simultaneously for all declarations in a let-expression. So, first all signa-
tures are collected:
Attribute gathTySigGam is used to gather type signatures. The gathered signatures are
then passed back into the declarations. Attribute tySigGam is used to distribute the
gathered type signatures over the declarations.
At a value declaration we extract the the type signature from tySigGam and use it to
check whether a pattern has a type signature:
SEM Decl
| Val loc .(sigTy, hasTySig) = case @patExpr.mbTopNm of
Nothing
→ (Ty Any, False)
Just nm
→ case gamLookup nm @lhs.tySigGam of
Nothing → (Ty Any, False)
Just vgi → (vgiTy vgi, True)
This type signature is then used as the known type of the pattern and the expression.
SEM Decl
| Val loc.knTy = @sigTy
The flag hasTySig is used to signal the presence of a type signature for a value and a
correct form of the pattern. We allow patterns of the form ‘ab @(a, b)’ to have a type
signature associated with ab. No type signatures are allowed for ‘(a, b)’ without the
‘ab @’ alias (because there is no way to refer to the anonymous tuple) nor is it allowed
to specify type signature for the fields of the tuple (because of simplicity, additional
plumbing would be required).
The value of hasTySig is also used to decide on the binding of the top level identifier of
a pattern, via inclVarBind.
ATTR PatExpr [inclVarBind : Bool ||]
SEM PatExpr
| AppTop patExpr.inclVarBind = True
SEM Decl
| Val patExpr.inclVarBind = ¬ @hasTySig
SEM Expr
| Lam arg .inclVarBind = True
Typing Haskell with an Attribute Grammar 43
If a type signature for an identifier is already defined there is no need to rebind the
identifier by adding one more binding to valGam.
New bindings are not immediatelyaddedto valGambut are first gathered ina separately
threaded attribute patValGam, much in the same way as gathTySigGam is used.
ATTR AllDecl [| patValGam : ValGam |]
SEM Decl
| Val patExpr.valGam = @lhs.patValGam
lhs .patValGam = @patExpr.valGam
expr .valGam = @lhs.valGam
SEM Expr
| Let decls.patValGam = @decls.gathTySigGam
‘gamPushGam‘ @lhs.valGam
loc .(lValGam, gValGam) = gamPop @decls.patValGam
decls.valGam = @decls.patValGam
body .valGam = @decls.patValGam
Newly gathered bindings are stacked on top of the inherited valGam before passing
them on to both declarations and body.
Some additional functionality for pushing and popping the stack valGam is also needed:
Extracting the top of the stack patValGam gives all the locally introduced bindings in
lValGam. An additional error message is produced if any duplicate bindings are present
in lValGam.
Checking TyExpr. All that is left to do now is to use the type expressions to extract
type signatures. This is straightforward as type expressions (abstract syntax for what
the programmer specified) and types (as internally used by the compiler) have almost
the same structure:
Actually, we need to do more because we also have to check whether a type is defined.
A variant of Gam is used to hold type constants:
44 A. Dijkstra and S.D. Swierstra
At the root of the AST tyGam is initialized with the fixed set of types available in this
version of the compiler:
SEM AGItf
| AGItf loc.tyGam = assocLToGam
[ (hsnArrow, TyGamInfo (Ty Con hsnArrow))
, (hsnInt, TyGamInfo tyInt)
, (hsnChar, TyGamInfo tyChar)
]
The next version of EH drops the requirement that all value definitions need to be ac-
companied by an explicit type signature. For example, the example from the introduc-
tion:
let i = 5
in i
let i = 5
{- [ i:Int ] -}
in i
Typing Haskell with an Attribute Grammar 45
The idea is that the type system implementation has an internal representation for
“knowing it is a type, but not yet which one” which can be replaced by a more spe-
cific type if that becomes known. The internal representation for a yet unknown type is
called a type variable, similar to mutable variables for (runtime) values.
The implementation attempts to gather as much information as possible from a program
to reconstruct (or infer) types for type variables. However, the types it can reconstruct
are limited to those allowed by the used type language, that is, basic types, tuples and
functions. All types are assumed to be monomorphic, that is, polymorphism is not yet
allowed. The next version of EH deals with polymorphism.
So
let id = λx → x
in let v = id 3
in id
will give
let id = \x -> x
{- [ id:Int -> Int ] -}
in let v = id 3
{- [ v:Int ] -}
in id
let id = \x -> x
{- [ id:v_1_1 -> v_1_1 ] -}
in id
let id = \x -> x
{- [ id:Int -> Int ] -}
in let v = (id 3,id ’x’)
{- ***ERROR(S):
In ‘(id 3,id ’x’)’:
... In ‘’x’’:
Type clash:
failed to fit: Char <= Int
problem with : Char <= Int -}
{- [ v:(Int,Int) ] -}
in v
46 A. Dijkstra and S.D. Swierstra
However, the next version of EH dealing with Haskell style polymorphism (section 4 )
accepts this program.
Partial type signatures are also allowed. A partial type signature specifies a type only
for a part, allowing a coöperation between the programmer who specifies what is (e.g.)
already known about a type signature and the type inferencer filling in the unspecified
details. For example:
The type inferencer pretty prints the inferred type instead of the explicity type signature:
The discussion of the implementation of this feature is postponed until section 3.6 in
order to demonstrate the effects of an additional feature on the compiler implementation
in isolation.
In order to be able to represent yet unknown types the type language needs type vari-
ables to represent this:
σ = Int | Char
| (σ, ..., σ)
| σ→σ
| v
The corresponding type structure Ty needs to be extended with an alternative for a
variable:
DATA Ty
| Var tv : {TyVarId }
Typing Haskell with an Attribute Grammar 47
The idea is to thread a counter as global variable through the AST, incrementing it
whenever a new unique value is required. The implementation used throughout all EH
compiler versions is more complex because an UID actually is a hierarchy of counters,
each level counting in the context of an outer level. This is not discussed any further;
we will ignore this aspect and just assume a unique UID can be obtained. However, a
bit of its implementation is visible in the pretty printed representation as a underscore
separated list of integer values, occasionaly visible in sample output of the compiler.
3.2 Constraints
Although the typing rules at Fig. 9 still hold we need to look at the meaning of (or
fitsIn) in the presence of type variables. The idea here is that what is unknown may be
replaced by that which is known. For example, when the check v σ is encountered,
the easiest way to make v σ true is to state that the (previously) unknown type v
equals σ. An alternative way to look at this is that v σ is true under the constraint
that v equals σ.
Remembering and applying constraints. Next we can observe that once a certain type
v is declared to be equal to a type σ this fact has to be remembered.
C = [v → σ]
A set of constraints C (appearing in its non pretty printed form as Cnstr in the source
text) is a set of bindings for type variables, represented as an association list:
newtype C = C (AssocL TyVarId Ty) deriving Show
cnstrTyLookup :: TyVarId → C → Maybe Ty
cnstrTyLookup tv (C s) = lookup tv s
emptyCnstr :: C
emptyCnstr = C [ ]
cnstrTyUnit :: TyVarId → Ty → C
cnstrTyUnit tv t = C [ (tv, t) ]
48 A. Dijkstra and S.D. Swierstra
The operator applies constraints C to a Substitutable. Function ftv extracts the free
type variable references as a set of TVarId’s.
A C can be applied to a type:
instance Substitutable Ty where
() = tyAppCnstr
ftv = tyFtv
This is another place where we use the AG notation and the automatic propagation
of values as attributes throughout the type representation to make the description of
the application of a C to a Ty easier. The function tyAppCnstr is defined in terms of
the following AG. The plumbing required to provide the value of attribute repl (tvs)
available as the result of Haskell function tyAppCnstr (tyFtv) has been omitted:
AG: Attribute of type SELF. The type of an attribute of type SELF depends on the
node in which a rule is defined for the attribute. The generated type of an at-
tribute attr for node is equal to the generated Haskell datatype of the same
name node. The AG compiler inserts code for building node’s from the attr of
the children and other fields. Insertion of this code can be overridden by providing
Typing Haskell with an Attribute Grammar 49
a definition ourselves. In this way a complete copy of the AST can be built as a
Haskell value. For example, via attribute repl a copy of the type is built which only
differs (or, may differ) in the original in the value for the type variable.
AG: Attribute together with USE. A synthesized attribute attr may be declared to-
gether with USE{op}{zero}. The op and zero allow the insertion of copy
rules which behave similar to Haskell’s foldr. The first piece of text op is used to
combine the attribute values of two children by textually placing this text as an op-
erator between references to the attributes of the children. If no child has an attr,
the second piece of text zero is used as a default value for attr. For example,
tvs USE {‘union‘} {[]} (appearing in pretty printed form as tvs USE{∪}{[ ]})
gathers bottom-up the free type variables of a type.
Computing constraints. The only source of constraints is the check fitsIn which deter-
mines whether one type can flow into another one. The previous version of EH could
only do one thing in case a type could not fit in another: report an error. Now, if one
of the types is unknown, which means that it is a type variable, we have the additional
possibility of returning a constraint on that type variable. The implementation fitsIn of
additionaly has to return constraints:
f Ty Any t2 = res t2
f t1 Ty Any = res t1
f t1 @(Ty Con s1)
t2 @(Ty Con s2)
| s1 ≡ s2 = res t2
Although this version of the implementation of fitsIn resembles the previous one it
differs in the following aspects:
– The datatype FIOut returned by fitsIn has an additional field foCnstr holding found
constraints. This requires constraints to be combined for composite types like the
App variant of Ty.
Typing Haskell with an Attribute Grammar 51
– The function bind creates a binding for a type variable to a type. The use of bind
is shielded by occurBind which checks if the type variable for which a binding is
created does not occur free in the bound type too. This is to prevent (e.g.) a a → a
to succeed. This is because it is not clear if a → a → a should be the resulting
constraint or a → (a → a) → (a → a) or one of infinitely many other possible
solutions. A so called infinite type like this is inhibited by the so called occurs
check.
– An application App recursively fits its components with components of another
App. The constraints from the first fit ffo are applied immediately to the follow-
ing component before fitting that one. This is to prevent a → a Int → Char
from finding two conflicting constraints [a → Int, a → Char ] instead of properly
reporting an error.
Constraints are used to make knowledge found about previously unknown types ex-
plicit. The typing rules in Fig. 6 (and Fig. 7, Fig. 9) in principle do not need to be
changed. The only reason to adapt some of the rules to the variant in Fig. 11 is to clarify
the way constraints are used.
expr
Γ; σk e:σ C
expr
Γp , Γ; σr e : σe C3
expr pat
Γ; σa e2 : C2 σp p : ; Γ p C2
expr fit
Γ; v → σk e1 : σa → σ C1 v1 → v2 σk : σp → σr C1
v fresh vi fresh
expr (e-app2) expr (e-lam2)
Γ; σk e1 e2 : C2 σ C2..1 Γ; σk λp → e : C3 σp → σe C3..1
fit
(v1 , v2 , ..., vn ) σr : (σ1 , σ2 , ..., σn ) C
(i → σi ) ∈ Γ
fit
→ ... → σr ≡ σk
σ i σk : σ C vi fresh
expr (e-ident2) expr (e-con2)
Γ; σk i:σ C Γ; σk ,n : σ1 → ... → σn → (σ1 , σ2 , ..., σn ) C
fit
Int σk : σ C
expr (e-int2)
Γ; σk minint . . maxint : σ C
The type rules in Fig. 11 enforcean order in which checking and inferring types has to
be done.
Actually, the rules in Fig. 11 should be even more specific in how constraints flow
around if we want to be closer to the corresponding AG description. The AG specifies
a C to be threaded instead of just returned bottom-up:
ATTR AllExpr [| tyCnstr : C |]
Its use in an expression application is as follows:
SEM Expr
| App loc.knFunTy [mkNewTyVar @lUniq] ‘mkTyArrow‘ @lhs.knTy
.ty @arg.tyCnstr @fTy
AG: Redefining an attribute value. Normally a value for an attribute may be associ-
ated with an attribute only once, using = in a rule. It is an error if multiple rules
for an attribute are present. If is used instead, any previous definition is overrid-
den and no error message is generated. In this context previous means “textually
occurring earlier”. Because the AG system’s flexibility finds its origin in the in-
dependence of textual locations of declarations and definitions, should be used
with care. For these notes the order in which redefinitions appear is the same as
their textual appearance in these notes, which again is the same as the sequence of
versions of EH.
This definition builds on top of the previous version by redefining some attributes (in-
dicated by instead of =). If this happens a reference to the location (in these notes)
of the code on top of which the new code is added can be found5 .
To correspond better with the related AG code the rule e-app2 should be:
expr
C1 ; Γ; σa e2 : C2
expr
Ck ; Γ; v → Ck σk e1 : σa → σ C1
v fresh
expr
(e-app2B)
Ck ; Γ; σk e1 e2 : C2 σ C2
The flow of constraints is made explicit as they are passed through the rules, from
the context (left of ) to a result (right of ). We feel this does not benefit clarity,
even though it is correct. It is our opinion that typing rules serve their purpose best by
providing a basis for proof as well as understanding and discussion. An AG description
serves its purpose best by showing how it really is implemented. Used in tandem they
strengthen each other.
5 This is not an ideal solution to display combined fragments. A special purpose editor would
probably do a better job of browsing textually separated but logically related pieces of code.
Typing Haskell with an Attribute Grammar 53
– A resulting type has all known constraints applied to it, here ty.
but as this invariant is not kept for knTy and valGam it requires to
The type rules in Fig. 11 do not mention the last two constraint applications (rule e-
app2B does), and this will also be omitted for later typing rules. However, the constraint
applications are shown by the AG code for the App alternative and the following Var
alternative:
SEM Expr
| Var loc.fTy @lhs.tyCnstr @gTy
.fo @fTy (@lhs.tyCnstr @lhs.knTy)
lhs.tyCnstr = foCnstr @fo @lhs.tyCnstr
The rules for constants all resemble the one for Int, rule e-int2. Their implementation
additionaly takes care of constraint handling:
SEM Expr
| IConst CConst
loc.fo @fTy (@lhs.tyCnstr @lhs.knTy)
lhs.tyCnstr = foCnstr @fo @lhs.tyCnstr
The handling of products does not differ much from the previous implementation. A
rule e-con2 has been included in the typing rules, as a replacement for rule e-prod1B
(Fig. 7) better resembling its implementation. Again the idea is to exploit that in this
version of EH tupling is the only way to construct an aggregrate value. A proper struc-
ture for its type is (again) enforced by fitsIn.
SEM Expr
| Con loc.fo = let gTy = mkTyFreshProdFrom @lUniq (hsnProdArity @nm)
foKnRes = gTy (@lhs.tyCnstr tyArrowRes @lhs.knTy)
in foKnRes{foTy = tyProdArgs (foTy foKnRes)
‘mkTyArrow‘ (foTy foKnRes)}
.ty foTy @fo
lhs.tyCnstr = foCnstr @fo @lhs.tyCnstr
54 A. Dijkstra and S.D. Swierstra
Finally,
SEM Expr
| Lam loc .(argTy, resTy, funTy)
let [a, r ] = mkNewTyVarL 2 @lUniq
in (a, r, [a] ‘mkTyArrow‘ r)
.foKnFun @funTy (@lhs.tyCnstr @lhs.knTy)
arg .knTy @argTy
.tyCnstr = foCnstr @foKnFun @lhs.tyCnstr
body.knTy @resTy
loc .bodyTyCnstr = @body.tyCnstr
.ty [@bodyTyCnstr @arg.ty] ‘mkTyArrow‘ @body.ty
– The main difference with the previous implementation is the use of type variables
to represent unknown knowledge. Previously was used for that purpose, for ex-
ample, the rule e-lam2 and its implementation show that fresh type variables vi in
ν1 → ν2 are used instead of → to enforce a . . → . . structure. If still would
be used, for example in:
let id = λx → x
in id 3
For this version this is no longer the case so the structure of a pattern reveals already
some type structure. Hence we compute types for patterns too and use this type as the
known type if no type signature is available.
Computation of the type of a pattern is similar to and yet more straightforward than for
expressions. The rule e-pat2 from Fig. 12 binds the identifier to the known type and if
no such known type is available it invents a fresh one, by means of tyEnsureNonAny:
ATTR AllPatExpr [| tyCnstr : C | ty : Ty]
SEM PatExpr
| Var VarAs loc .ty tyEnsureNonAny @lUniq @lhs.knTy
| VarAs patExpr.knTy = @ty
tyEnsureNonAny :: UID → Ty → Ty
tyEnsureNonAny u t = if t Ty Any then t else mkNewTyVar u
pat
σk p : σ; Γ p C
fit
C1 σk σd : σ C2
σd → () ≡ σp
pat
p : σp ; Γp C1
p ≡ p1 p2 ... pn , n 1
pat
(p-apptop2)
σk p : σ; Γp C2..1
p p
dom (Γ1 ) ∩ dom (Γ2 ) = ∅
pat p
σa1 p 2 : ; Γ 2 C2
pat p
p1 : σd → (σa1 , σa2 , ..., σan ); Γ1 C1
pat
(p-app2)
p p
p1 p2 : C2 (σd → (σa2 , ..., σan )); Γ1 , Γ2 C2..1
σk vi fresh
pat
(p-var2) pat
(p-con2)
σk i : σk ; [i → σk ] [] I : σ; (v1 , v2 , ..., vn ) → (v1 , v2 , ..., vn ) [ ]
For tuples we again make use of the fact that the Con alternative will always represent
a tuple. When datatypes are introduced (not part of these notes) this will no longer be
56 A. Dijkstra and S.D. Swierstra
the case. Here, we already make the required rule p-con2 more general than is required
here because we already prepare for datatypes.
A pattern (in essence) can be represented by a function σ → (σ1 , ...) taking a value of
some type σ and dissecting it into a tuple (σ1 , ...) containing all its constituents. For
now, because we have only tuples to dissect, the function returned by the Con alternative
is just the identity on tuples of the correct size. The application rule p-app2 consumes
an element of this tuple representing the dissected value and uses it for checking and
inferring the constituent.
The implementation of this representation convention returns the dissecting function
type in patFunTy:
The dissecting function type patFunTy is constructed from fresh type variables. Each
occurrence of a tuple pattern deals with different unknown types and hence fresh type
variables are needed. The availability of polymorphism in later versions of EH allows
us to describe this in a more general way.
At AppTop of PatExpr the function type σ → (σ1 , ...) describing the dissection is
split into the type σ (attribute knResTy) of the pattern and the tuple type (σ1 , ...) (at-
tribute knProdTy) holding its constituents. The distribution of the types of the fields of
knProdTy was described in the previous version of EH.
SEM PatExpr
| AppTop loc.patFunTy = @patExpr.patFunTy
.(knResTy, knProdTy) tyArrowArgRes @patFunTy
SEM PatExpr
| IConst loc .ty = tyInt
| CConst loc .ty = tyChar
| AppTop loc .fo = @lhs.knTy @knResTy
.ty = foTy @fo
patExpr.tyCnstr = foCnstr @fo @lhs.tyCnstr
lhs .ty = @patExpr.tyCnstr @ty
| App arg .knTy @func.tyCnstr @knArgTy
| Con loc .ty = Ty Any
Typing Haskell with an Attribute Grammar 57
The careful reader may have observed that the direction of for fitting actual (synthe-
sized, bottom-up) and known type (inherited, top-down) is the opposite of the direction
used for expressions. This is a result of a difference in the meaning of an expression and
a pattern. An expression builds a value from bottom to top as seen in the context of an
abstract syntax tree. A pattern dissects a value from top to bottom. The flow of data is
opposite, hence the direction of too.
3.5 Declarations
Again, at the level of declarations all is tied together. Because we first gather infor-
mation about patterns and then about expressions two separate threads for gathering
constraints are used, patTyCnstr and tyCnstr respectively.
SEM Expr
| Let decls.patTyCnstr = @lhs.tyCnstr
.tyCnstr = @decls.patTyCnstr
Partial type signatures allow the programmer to specify only a part of a type in a type
signature. The description of the implementation of this feature is separated from the
discussion of other features to show the effects of an additional feature on the compiler.
In other words, the following is an impact analysis.
First, both abstract syntax and the parser (not included in these notes) contain an addi-
tional alternative for parsing the ”...” notation chosen for unspecified type information
designated by Wild for wildcard:
DATA TyExpr
| Wild
A wildcard type is treated in the same way as a type variable as it also represents un-
known type information:
58 A. Dijkstra and S.D. Swierstra
SEM TyExpr
| Wild loc.tyVarId = @lUniq
.tgi = TyGamInfo (mkNewTyVar @tyVarId)
SEM TyExpr
| Wild lhs.ty = tgiTy @tgi
Changes also have to be made to the omitted parts of the implementation, in particular
the pretty printing of the AST and generation of unique identifiers. We mention the
necessity of this but omit the relevant code.
The pretty printing of a type signature is enhanced a bit further by either printing the
type signature (if no wildcard types are present in it) or by printing the type of the type
signature combined with all found constraints. The decision is based on the presence of
wildcard type variables in the type signature:
ATTR TyExpr [|| tyVarWildL USE{++}{[ ]} : TyVarIdL]
SEM TyExpr
| Wild lhs.tyVarWildL = [@tyVarId ]
The set of all constraints is retrieved at the root of the AST and passed back into the
tree:
let id :: a → a
id = λx → x
v = (id 3, id ’x’)
in v
gives v :: (Int, Char) and id :: ∀ a.a → a. The polymorphic identity function id accepts
a value of any type a, giving back a value of the same type a. Type variables in the type
Typing Haskell with an Attribute Grammar 59
let f :: (a → a) → Int
f = λi → i 3
id :: a → a
id = λx → x
in f id
The problem here is that the polymorphism of f in a means that the caller of f can freely
choose what this a is for a particular call. However, from the viewpoint of the body of
f this limits the choice of a to no choice at all. If the caller has all the freedom to make
the choice, the callee has none. In our implementation this is encoded as a type constant
c_ chosen for a during type checking the body of f . This type constant by definition is
a type a programmer can never define or denote. The consequence is that an attempt to
use i in the body of f , which has type c_..→c_.. cannot be used with an Int. The use
of type constants will be explained later.
Another example of the limitations of polymorphism in this version of EH is the fol-
lowing variation:
60 A. Dijkstra and S.D. Swierstra
let f = λi → i 3
id :: a → a
in let v = f id
in f
let f = \i -> i 3
id :: a -> a
{- [ f:forall a . (Int -> a) -> a, id:forall a . a -> a ] -}
in let v = f id
{- [ v:Int ] -}
in f
EH version 3 allows parametric polymorphism but not yet polymorphic parameters. The
parameter i has a monomorphic type, which is made even more clear when we make an
attempt to use this i polymorphically in:
let f = λi → (i 3, i ’x’)
id = λx → x
in let v = f id
in v
Because i is not allowed to be polymorphic it can either be used on Int or Char, but not
both.
These problems can be overcome by allowing higher ranked polymorphism in type
signatures. Later versions of EH deal with this problem, but this is not included in these
notes. This version of EH resembles Haskell98 in these restrictions.
The reason not to allow explicit types to be of assistance to the type inferencer is that
Haskell98 and this version of EH have as a design principle that all explicitly specified
Typing Haskell with an Attribute Grammar 61
types in a program are redundant. That is, after removal of explicit type signatures, the
type inferencer can still reconstruct all types. It is guaranteed that all reconstructed types
are the same as the removed signatures or more general, that is, the type signatures are
a special case of the inferred types. This guarantee is called the principal type property
[9,26,18]. However, type inferencing also has its limits. In fact, the richer a type system
becomes, the more difficult it is for a type inferencing algorithm to make the right choice
for a type without the programmer specifying additional helpful type information.
The type language for this version of EH adds quantification by means of the universal
quantifier ∀:
σ = Int | Char
| (σ, ..., σ)
| σ→σ
| v|f
| ∀α.σ
A f stands for a fixed type variable, a type variable which may not be constrained but
still stands for an unknown type. A v stands for a plain type variable as used in the
previous EH version. A series of consecutive quantifiers in ∀α1 .∀α2 . ... σ is abbreviated
to ∀α.σ.
The type language suggests that a quantifier may occur anywhere in a type. This is not
the case, quantifiers may only be on the top of a type; this version of EH takes care to
ensure this. A second restriction is that quantified types are present only in a Γ whereas
no ∀’s are present in types used throughout type inferencing expressions and patterns.
This is to guarantee the principle type property.
The corresponding abstract syntax for a type needs additional alternative to represent
a quantified type. For a type variable we also have to remember to which category it
belongs, either plain or fixed:
DATA Ty
| Var tv : {TyVarId }
categ : TyVarCateg
DATA TyVarCateg
| Plain
| Fixed
DATA Ty
| Quant tv : {TyVarId }
ty : Ty
SET AllTyTy = Ty
62 A. Dijkstra and S.D. Swierstra
mkTyQu :: TyVarIdL → Ty → Ty
mkTyQu tvL t = foldr (λtv t → Ty Quant tv t) t tvL
We will postpone the discussion of type variable categories until section 91.
The syntax of this version of EH only allows type variables to be specified as part of a
type signature. The quantifier ∀ cannot be explicitly denoted. We only need to extend
the abstract syntax for types with an alternative for type variables:
DATA TyExpr
| Var nm : {HsName}
Compared to the previous version the type inferencing process does not change much.
Because types used throughout the type inferencing of expressions and patterns do not
contain ∀ quantifiers, nothing has to be changed there.
Changes have to be made to the handling of declarations and identifiers though.This is
because polymorphism is tied up with the way identifiers for values are introduced and
used.
A quantified type, also often named type scheme, is introduced in rule e-let3 and rule e-
let-tysig3 and instantiated in rule e-ident3, see Fig. 13. We will first look at the instan-
tiation.
SEM Expr
| Var loc.fTy @lhs.tyCnstr tyInst @lUniq @gTy
We may freely decide what type the quantified type variables may have as long as each
type variable stands for a monomorphic type. However, at this point it is not known
which type a type variable stands for, so fresh type variables are used instead. This
is called instantiation, or specialization. The resulting instantiated type partakes in the
inference process as usual.
The removal of the quantifier and replacement of all quantified type variables with fresh
type variables is done by tyInst:
Typing Haskell with an Attribute Grammar 63
expr
Γ; σk e:σ C
expr
Γq , Γ; σk e : σe C3
Γ ≡ [ (i → ∀α.σ) | (i → σ) ← C2..1 Γp , α ≡ ftv (σ) − ftv (C2..1 Γ) ]
q
expr
Γp , Γ; σp ei : C2
pat
p : σp ; Γp C1
expr (e-let3)
Γ; σk let p = ei in e : σe C3..1
expr
(Γq − [i → ] ++ [i → σq ]) ++ Γ; σk e : σe C3
Γq ≡ [ (i → ∀α.σ) | (i → σ) ← C2..1 Γp , α ≡ ftv (σ) − ftv (C2..1 Γ) ]
expr
(Γp − [i → ] ++ [i → σq ]) ++ Γ; σj ei : C2
σ ≡ ∀α.σ
q i
σj ≡ [αj → fj ] σi , fj fresh
α ≡ ftv (σi )
p ≡ i ∨ p ≡ i @...
pat
σi p : ; Γp C1
expr (e-let-tysig3)
Γ; σk let i :: σi ; p = ei in e : σe C3..1
(i → ∀[αj ].σi ) ∈ Γ
fit
[αj → vj ] σi σk : σ C
vj fresh
expr (e-ident3)
Γ; σk i:σ C
Function tyInst strips all quantifiers and substitutes the quantified type variables with
fresh ones. It is assumed that quantifiers occur only at the top of a type.
Quantification. The other way around, quantifying a type, happens when a type is
bound to a value identifier and added to a Γ. The way this is done varies with the
presence of a type signature. Rule e-let3 and rule e-let-tysig3 (Fig. 13) specify the
respective variations.
A type signature itself is specified without explicit use of quantifiers. These need to be
added for all introduced type variables, except the ones specified by means of ‘...’ in
a partial type signature:
SEM Decl
| TySig loc.sigTy = tyQuantify (∈ @tyExpr.tyVarWildL) @tyExpr.ty
.gamSigTy @sigTy
A type signature simply is quantified over all free type variables in the type using
tyQuantify :: (TyVarId → Bool) → Ty → Ty
tyQuantify tvIsBound ty = mkTyQu (filter (¬.tvIsBound) (ftv ty)) ty
Type variables introduced by a wildcard may not be quantified over because the type
inferencer will fill in the type for those type variables.
We now run into a problem which will be solved no sooner than the next version of EH.
In a declaration of a value (variant Val of Decl) the type signature acts as a known type
knTy against which checking of the value expression takes place. Which type do we use
for that purpose, the quantified sigTy or the unquantified tyExpr.ty?
let id :: a → a
id = λx → 3
in ...
For now, this can be solved by replacing all quantified type variables of a known type
with type constants:
SEM Decl
| Val loc.knTy tyInstKnown @lUniq @sigTy
Typing Haskell with an Attribute Grammar 65
This changes the category of the fresh type variable replacing the quantified type vari-
able to ‘fixed’. A fixed type variable is like a plain type variable but may not be con-
strained, that is, bound to another type. This means that fitsIn has to be adapted to
prevent this from happening. The difference with the previous version only lies in the
handling of type variables. Type variables now may be bound if not fixed, and to be
equal only if their categories match too. For brevity the new version of fitsIn is omitted.
let id = λx → x
in ...
The only way the value associated with id ever will be used outside the expression
bound to id, is via the identifier id. So, if the inferred type v1 → v1 for the expression
λx → x has free type variables (here: [v1 ]) and these type variables are not used in the
types of other bindings, in particular those in the global Γ, we know that the expression
λx → x nor any other type will constrain those free type variables. The type for such a
type variable apparently can be freely chosen by the expression using id, which is ex-
actly the meaning of the universal quantifier. These free type variables are the candidate
type variables over which quantification can take place, as described by the typing rules
for let-expressions in Fig. 13 and its implementation:
SEM Expr
| Let loc .lSubsValGam = @decls.tyCnstr @lValGam
.gSubsValGam = @decls.tyCnstr @gValGam
.gTyTvL = ftv @gSubsValGam
.lQuValGam = valGamQuantify @gTyTvL @lSubsValGam
body.valGam @lQuValGam
‘gamPushGam‘ @gSubsValGam
All available constraints in the form of decls.tyCnstr are applied to both global
(gValGam) and local (lValGam) Γ. All types in the resulting local lSubsValGam are
then quantified over their free type variables, with the exception of those referred to
more globally, the gTyTvL. We use valGamQuantify to accomplish this:
66 A. Dijkstra and S.D. Swierstra
The condition that quantification only may be done for type variables not occurring in
the global Γ is a necessary one. For example:
let h :: a → a → a
f = λx → let g = λy → (h x y, y)
in g 3
in f ’x’
If the type g :: a → (a, a) would be concluded, g can be used with y an Int parameter,
as in the example. Function f can then be used with x a Char parameter. This would go
wrong because h assumes the types of its parameters x and y are equal. So, this justifies
the error given by the compiler for this version of EH:
The types of the function id1 and value v1 are inferred in the same binding group.
However, in this binding group the type for id1 is v1 → v1 for some type variable
v1 , without any quantifier around the type. The application id1 3 therefore infers an
additional constraint v1 → Int, resulting in type Int → Int for id1
On the other hand, id2 is used after quantification, outside the binding group, with type
∀ a.a → a. The application id2 3 will not constrain id2.
In Haskell binding group analysis will find groups of mutually dependent definitions,
each of these called a binding group. These groups are then ordered according to “define
before use” order. Here, for EH, all declarations in a let-expression automatically form
a binding group, the ordering of two binding groups d1 and d2 has to be done explicitly
using sequences of let expressions: let d1 in let d2 in....
Being togetherin a binding group can create a problemfor inferencing mutually recur-
sive definitions, for example:
let f1 = λx → g1 x
g1 = λy → f1 y
f2 :: a → a
f2 = λx → g2 x
g2 = λy → f2 y
in 3
This results in
let f1 = \x -> g1 x
g1 = \y -> f1 y
f2 :: a -> a
f2 = \x -> g2 x
g2 = \y -> f2 y
{- [ g2:forall a . a -> a, g1:forall a . forall b . a -> b
, f1:forall a . forall b . a -> b, f2:forall a . a -> a ] -}
in 3
For f1 it is only known that its type is v1 → v2 . Similarly g1 has a type v3 → v4 . More
type information cannot be constructed unless more information is given as is done for
f2 . Then also for g2 may the type ∀ a.a → a be reconstructed.
68 A. Dijkstra and S.D. Swierstra
Type expressions. Finally, type expressions need to return a type where all occurrences
of type variable names (of type HsName) coincide with type variables (of type TyVarId).
Type variable names are identifiers just as well so a TyGam similar to ValGam is used
to map type variable names to freshly created type variables.
SEM TyExpr
| Var (loc.tgi, lhs.tyGam) = case tyGamLookup @nm @lhs.tyGam of
Nothing → let t = mkNewTyVar @lUniq
tgi = TyGamInfo t
in (tgi, gamAdd @nm tgi @lhs.tyGam)
Just tgi → (tgi, @lhs.tyGam)
SEM TyExpr
| Var lhs.ty = tgiTy @tgi
Either a type variable is defined in tyGam, in that case the type bound to the identifier
is used, otherwise a new type variable is created.
AG system. At the start of these notes we did make a claim that our “describe sepa-
rately” approach contributes to a better understood implementation of a compiler, in
particular a Haskell compiler. Is this true? We feel that this is the case, and thus the
benefits outweigh the drawbacks, based on some observations made during this project:
The AG system provides mechanisms to split a description into smaller fragments, com-
bine those fragments and redefine part of those fragments. An additional fragment man-
agement system did allow us to do the same with Haskell fragments. Both are essential
in the sense that the simultaneous ‘existence’ of a sequence of compiler versions, all in
working order when compiled, with all aspects described with the least amount of du-
plication, presentable in a consistent form in these notes could not have been achieved
without these mechanisms and supporting tools.
The AG system allows focusing on the places where something unusual needs to be
done, similar to other approaches [24]. In particular, copy rules allow us to forget about
a large amount of plumbing.
The complexity of the language Haskell, its semantics, and the interaction between fea-
tures is not reduced. However, it becomes manageable and explainable when divided
into small fragments. Features which are indeed independent can also be described
independently of each other by different attributes. Features which evolve through dif-
ferent versions, like the type system, can also be described separately, but can still be
looked upon as a group of fragments. This makes the variation in the solutions explicit
and hence increases the understanding of what really makes the difference between two
subsequent versions.
Typing Haskell with an Attribute Grammar 69
On the downside, fragments for one aspect but for different compiler versions end up in
different sections of these notes. This makes their understanding more difficult because
one now has to jump between pages. This is a consequence of the multiple dimensions
we describe: variation in language elements (new AST), additional semantics (new at-
tributes) and variation in the implementation. Paper, on the other hand, provides by
definition a linear, one dimensional rendering of this multidimensional view. We can
only expect this to be remedied by the use of proper tool support (like a fragment ed-
itor or browser). On paper, proper cross referencing, colors, indexing or accumulative
merging of text are most likely to be helpful.
The AG system, though in its simplicity surprisingly usable and helpful, could beim-
proved in many areas. For example, no type checking related to Haskell code for at-
tribute definitions is performed, nor will the generated Haskell code when compiled by
a Haskell compiler produce sensible error messages in terms of the original AG code.
The AG system also lacks features necessary for programming in the large. For exam-
ple, all attributes for a node live in a global namespace for that node instead of being
packaged in some form of module.
Performance is expected to give problems for large systems. This seems to be primarily
caused by the simple translation scheme in which all attributes together live in a tuple
just until the program completes. This inhibits garbage collection of intermediate at-
tributes that are no longer required. It also stops GHC from performing optimizations;
informal experimentation with a large AG program resulted in GHC taking approxi-
mately 10 times more time with optimization flags on. The resulting program only ran
approximately 15% faster. The next version of the AG system will be improved in this
area [35].
About these notes EH and its code. The linear presentation of code and explanation
might suggest that this is also the order in which the code and these notes came into
existence. This is not the case. A starting point was created by programming a final
version (at that time EH version 6, not included in these notes). From this version the
earlier versions were constructed. After that, later versions were added. However, these
later versions usually needed some tweaking of earlier versions. The consequence of
this approach is that the rationale for design decisions in earlier versions become clear
only in later versions. For example, an attribute is introduced only so later versions only
70 A. Dijkstra and S.D. Swierstra
need to redefine the rule for this single attribute. However, the initial rule for such an
attribute often just is the value of another attribute. At such a place the reader is left
wondering. This problem could be remedied by completely redefining larger program
fragments. This in turn decreases code reuse. Reuse, that is, sharing of common code
turned out to be beneficial for the development process as the use of different contexts
provides more opportunities to test for correctness. No conclusion is attached to this
observation, other than being another example of the tension between clarity of expla-
nation and the logistics of compiler code management.
Combining theory and practice. Others have described type systems in a practical set-
ting as well. For example, Jones [21] describes the core of Haskell98 by a monadic style
type inferencer. Pierce [34] explains type theory and provides many small implemen-
tations performing (mainly) type checking for the described type systems in his book.
On the other hand, only recently the static semantics of Haskell has been described
formally [14]. Extensions to Haskell usually are formally described but once they find
their way into a production compiler the interaction with other parts of Haskell is left
in the open or is at best described in the manual.
The conclusion of these observations might be that a combined description of a lan-
guage, its semantics, its formal analysis (like the type system), and its implementation
is not feasible. Whatever the cause of this is, certainly one contributing factor is the
sheer size of all these aspects in combination. We feel that our approach contributes
towards a completer description of Haskell, or any other language if described by the
AG system. Our angle of approach is to keep the implementation and its explanation
consistent and understandable at the same time. However, this document clearly is not
complete either. Formal aspects are present, let alone a proof that the implementation is
sound and complete with respect to the formal semantics. Of course one may wonder if
this is at all possible; in that case our approach may well be a feasible second best way
of describing a compiler implementation.
References
1. The Glasgow Haskell Compiler. http://www.haskell.org/ghc/, 2004.
2. Martin Abadi and Luca Cardelli. A Theory of Objects. Springer, 1996.
3. Arthur Baars. Attribute Grammar System. http://www.cs.uu.nl /groups/ST/twiki/
bin/view/Center/AttributeGrammarSystem, 2004.
Typing Haskell with an Attribute Grammar 71
4. Richard S. Bird. Using Circular Programs to Eliminate Multiple Traversals of Data. Acta
Informatica, 21:239–250, 1984.
5. Urban Boquist. Code Optimisation Techniques for Lazy Functional Languages, PhD Thesis.
Chalmers University of Technology, 1999.
6. Urban Boquist and Thomas Johnsson. The GRIN Project: A Highly Optimising Back End
For Lazy Functional Languages. In Selected papers from the 8th International Workshop on
Implementation of Functional Languages, 1996.
7. Didier Botlan, Le and Didier Remy. ML-F, Raising ML to the Power of System F. In ICFP,
2003.
8. Luis Damas and Robin Milner. Principal type-schemes for functional programs. In Pro-
ceedings of Principles of Programming Languages (POPL), pages 207–212. ACM, ACM,
1982.
9. Luis Damas and Robin Milner. Principal type-schemes for functional programs. In 9th
symposium Principles of Programming Languages, pages 207–212. ACM Press, 1982.
10. Iavor S. Diatchki, Mark P. Jones, and Thomas Hallgren. A Formal Specification of the
Haskell 98 Module System. In Haskell Workshop, pages 17–29, 2002.
11. Atze Dijkstra. EHC Web. http://www.cs.uu.nl/groups/ST/Ehc/WebHome, 2004.
12. Atze Dijkstra and Doaitse Swierstra. Explicit implicit parameters. Technical Report UU-
CS-2004-059, Institute of Information and Computing Science, 2004.
13. Atze Dijkstra and Doaitse Swierstra. Typing Haskell with an Attribute Grammar (Part I).
Technical Report UU-CS-2004-037, Department of Computer Science, Utrecht University,
2004.
14. Karl-Filip Faxen. A Static Semantics for Haskell. Journal of Functional Programming,
12(4):295, 2002.
15. Benedict R. Gaster and Mark P. Jones. A Polymorphic Type System for Extensible Records
and Variants. Technical Report NOTTCS-TR-96-3, Languages and Programming Group,
Department of Computer Science, Nottingham, November 1996.
16. Cordelia Hall, Kevin Hammond, Simon Peyton Jones, and Philip Wadler. Type Classes in
Haskell. ACM TOPLAS, 18(2):109–138, March 1996.
17. Bastiaan Heeren, Jurriaan Hage, and S. Doaitse Swierstra. Generalizing Hindley-Milner
Type Inference Algorithms. Technical Report UU-CS-2002-031, Institute of Information
and Computing Science, University Utrecht, Netherlands, 2002.
18. J.R. Hindley. The principal type-scheme of an object in combinatory logic. Transactions of
the American Mathematical Society, 146:29–60, December 1969.
19. Thomas Johnsson. Attribute grammars as a functional programming paradigm. In Functional
Programming Languages and Computer Architecture, pages 154–173, 1987.
20. Mark P. Jones. Typing Haskell in Haskell. In Haskell Workshop, 1999.
21. Mark P. Jones. Typing Haskell in Haskell. http://www.cse.ogi.edu/ mpj/thih/, 2000.
22. Mark P. Jones and Simon Peyton Jones. Lightweight Extensible Records for Haskell. In
Haskell Workshop, number UU-CS-1999-28. Utrecht University, Institute of Information and
Computing Sciences, 1999.
23. M.F. Kuiper and S.D. Swierstra. Using Attribute Grammars to Derive Efficient Functional
Programs. In Computing Science in the Netherlands CSN’87, November 1987.
24. Ralf Lämmel and Simon Peyton Jones. Scrap your boilerplate: a practical design pattern
for generic programming. In Types In Languages Design And Implementation, pages 26–37,
2003.
72 A. Dijkstra and S.D. Swierstra
25. Konstantin Laufer and Martin Odersky. Polymorphic Type Inference and Abstract Data
Types. Technical Report LUC-001, Loyola University of Chicago, 1994.
26. R. Milner. A theory of type polymorphism in programming. Journal of Computer and System
Sciences, 17(3), 1978.
27. John C. Mitchell and Gordon D. Plotkin. Abstract Types Have Existential Type. ACM
TOPLAS, 10(3):470–502, July 1988.
28. Martin Odersky and Konstantin Laufer. Putting Type Annotations to Work. In Principles of
Programming Languages, pages 54–67, 1996.
29. Martin Odersky, Martin Sulzmann, and Martin Wehr. Type Inference with Constrained
Types. In Fourth International Workshop on Foundations of Object-Oriented Programming
(FOOL 4), 1997.
30. Nigel Perry. The Implementation of Practical Functional Programming Languages, 1991.
31. Simon Peyton Jones. Haskell 98, Language and Libraries, The Revised Report. Cambridge
Univ. Press, 2003.
32. Simon Peyton Jones and Mark Shields. Practical type inference for arbitrary-rank types.
http://research.microsoft.com/Users/simonpj/papers/putting/index.htm,
2004.
33. Simon L. Peyton Jones. The Implementation of Functional Programming Languages. Pren-
tice Hall, 1987.
34. Benjamin C. Pierce. Types and Programming Languages. MIT Press, 2002.
35. Joao Saraiva. Purely Functional Implementation of Attribute Grammars. PhD thesis, Utrecht
University, 1999.
36. Chung-chieh Shan. Sexy types in action. ACM SIGPLAN Notices, 39(5):15–22, May 2004.
37. Mark Shields and Simon Peyton Jones. First-class Modules for Haskell. In Ninth Inter-
national Conference on Foundations of Object-Oriented Languages (FOOL 9), Portland,
Oregon, December 2001.
38. Utrecht University Software Technology Group. UUST library.
http://cvs.cs.uu.nl/cgi-bin/cvsweb.cgi/uust/, 2004.
39. S.D. Swierstra, P.R. Azero Alocer, and J. Saraiava. Designing and Implementing Com-
binator Languages. In Doaitse Swierstra, Pedro Henriques, and José Oliveira, editors,
Advanced Functional Programming, Third International School, AFP’98, number 1608 in
LNCS, pages 150–206. Springer-Verlag, 1999.
40. Simon Thompson. Type Theory and Functional Programming. Addison-Wesley, 1991.
41. Phil Wadler. Theorems for free! In 4’th International Conference on Functional Program-
ming and Computer Architecture, September 1989.
42. Philip Wadler. Deforestation: transforming programs to eliminate trees. In Theoretical Com-
puter Science, (Special issue of selected papers from 2’nd European Symposium on Pro-
gramming), number 73, pages 231–248, 1990.
Programming with Arrows
John Hughes
1 Introduction
1.1 Point-Free Programming
Consider this simple Haskell definition, of a function which counts the number
of occurrences of a given word w in a string:
count w = length . filter (==w) . words
This is an example of “point-free” programming style, where we build a function
by composing others, and make heavy use of higher-order functions such as
filter. Point-free programming is rightly popular: used appropriately, it makes
for concise and readable definitions, which are well suited to equational reasoning
in the style of Bird and Meertens [2]. It’s also a natural way to assemble programs
from components, and closely related to connecting programs via pipes in the
UNIX shell.
Now suppose we want to modify count so that it counts the number of
occurrences of a word in a file, rather than in a string, and moreover prints the
result. Following the point-free style, we might try to rewrite it as
count w = print . length . filter (==w) . words . readFile
But this is rejected by the Haskell type-checker! The problem is that readFile
and print have side-effects, and thus their types involve the IO monad:
readFile :: String -> IO String
print :: Show a => a -> IO ()
Of course, it is one of the advantages of Haskell that the type-checker can distin-
guish expressions with side effects from those without, but in this case we pay a
price. These functions simply have the wrong types to compose with the others
in a point-free style.
Now, we can write a point-free definition of this function using combinators
from the standard Monad library. It becomes:
count w = (>>=print) .
liftM (length . filter (==w) . words) .
readFile
But this is no longer really perspicuous. Let us see if we can do better.
V. Vene and T. Uustalu (Eds.): AFP 2004, LNCS 3622, pp. 73–129, 2005.
c Springer-Verlag Berlin Heidelberg 2005
74 J. Hughes
In Haskell, functions with side-effects have types of the form a -> IO b. Let
us introduce a type synonym for this:
We parameterise Kleisli over the IO monad because the same idea can be used
with any other one, and we call this type Kleisli because functions with this
type are arrows in the Kleisli category of the monad m.
Now, given two such functions, one from a to b, and one from b to c, we can
“compose” them into a Kleisli arrow from a to c, combining their side effects in
sequence. Let us define a variant composition operator to do so. We choose to
define “reverse composition”, which takes its arguments in the opposite order to
(.), so that the order in which we write the arrows in a composition corresponds
to the order in which their side effects occur.
Using this combinator, we can now combine side-effecting and pure functions in
the same point-free definition, and solve our original problem in the following
rather clear way:
– We can write overloaded code that works with many different libraries —
the functions in the standard Monad library are good examples. Such code
provides free functionality that the library author need neither design nor
implement.
– When a shared interface is sufficiently widely used, it can even be worthwhile
to add specific language support for using it. Haskell’s do syntax does just
this for monads.
– Library users need learn less to use a new library, if a part of its interface is
already familiar.
These are compelling advantages — and yet, the monadic interface suffers a
rather severe restriction. While a monadic program can produce its output in
many different ways — perhaps not at all (the Maybe monad), perhaps many
times (the list monad), perhaps by passing it to a continuation — it takes its
input in just one way: via the parameters of a function.
We can think of arrows as computations, too. The Arrow class we have de-
fined is clearly analogous to the usual Monad class — we have a way of creating a
pure computation without effects (arr/return), and a way of sequencing com-
putations ((>>>)/(>>=)). But whereas monadic computations are parameterised
over the type of their output, but not their input, arrow computations are pa-
rameterised over both. The way monadic programs take input cannot be varied
by varying the monad, but arrow programs, in contrast, can take their input in
many different ways depending on the particular arrow used. The stream func-
tion example above illustrates an arrow which takes its input in a different way,
as a stream of values rather than a single value, so this is an example of a kind
of computation which cannot be represented as a monad.
Arrows thus offer a competing way to represent computations in Haskell.
But their purpose is not to replace monads, it is to bring the benefits of a
shared interface, discussed above, to a wider class of computations than monads
can accomodate. And in practice, this often means computations that represent
processes.
One aspect of monads we have not touched on so far, is that they satisfy the
so-called monad laws [26]. These laws play a rather unobtrusive rôle in practice
— since they do not appear explicitly in the code, many programmers hardly
think about them, much less prove that they hold for the monads they define.
Yet they are important: it is the monad laws that allow us to write a sequence
of operations in a do block, without worrying about how the sequence will
be bracketed when it is translated into binary applications of the monadic bind
operator. Compare with the associative law for addition, which is virtually never
explicitly used in a proof, yet underlies our notation every time we write a+ b + c
without asking ourselves what it means.
Arrows satisfy similar laws, and indeed, we have already implicitly assumed
the associativity of (>>>), by writing arrow compositions without brackets!
78 J. Hughes
Other laws tell us, for example, that arr distributes over (>>>), and so the
definition of count we saw above,
is equivalent to
Now, it would be very surprising if this were not the case, and that illustrates
another purpose of such laws: they help us avoid “surprises”, where a slight
modification of a definition, that a programmer would reasonably expect to be
equivalent to the original, leads to a different behaviour. In this way laws provide
a touchstone for the implementor of an arrow or monad, helping to avoid the
creation of a design with subtle traps for the user. An example of such a design
would be a “monad” which measures the cost of a computation, by counting
the number of times bind is used. It is better to define a separate operation for
consuming a unit of resource, and let bind just combine these costs, because
then the monad laws are satisfied, and cosmetic changes to a monadic program
will not change its cost.
Nevertheless, programmers do sometimes use monads which do not satisfy
the stated laws. Wadler’s original paper [26] introduced the “strictness monad”
whose only effect is to force sequencing to be strict, but (as Wadler himself
points out), the laws are not satisfied. Another example is the random generation
“monad” used in our QuickCheck [4] testing tool, with which terms equivalent
by the monad laws may generate different random values — but with the same
distribution. There is a sense in which both these examples “morally” satisfy the
laws, so that programmers are not unpleasantly surprised by using them, but
strictly speaking the laws do not hold.
In the same way, some useful arrow instances may fail to satisfy the arrow
laws. In fact, the stream functions we are using as our main example fail to do
so, without restrictions that we shall introduce below. In this case, if we drop
the restrictions then we may well get unpleasant surprises when we use stream
function operations later.
Despite the importance of the arrow laws, in these notes I have chosen to
de-emphasize them. The reason is simple: while monads can be characterised by
a set of three laws, the original arrows paper states twenty [10], and Paterson’s
tutorial adds at least seven more [18]. It is simply harder to characterise the
expected behaviour of arrows equationally. I have therefore chosen to focus on
understanding, using, and implementing the arrow interface, leaving a study of
the laws for further reading. Either of the papers cited in this paragraph is a
good source.
Programming with Arrows 79
In the case of monads, the second argument of (>>=) is a Haskell function, which
permits the user of this interface to use all of Haskell to map the result of the first
computation to the computation to be performed next. Every time we sequence
two monadic computations, we have an opportunity to run arbitrary Haskell code
in between them. But in the case of arrows, in contrast, the second argument of
(>>>) is just an arrow, an element of an abstract datatype, and the only things
we can do in that arrow are things that the abstract data type interface provides.
Certainly, the arr combinator enables us to have the output of the first arrow
passed to a Haskell function — but this function is a pure function, with the
type b -> c, which thus has no opportunity to perform further effects. If we
want the effects of the second arrow to depend on the output of the first, then
we must construct it using operations other than arr and (>>>).
Thus the simple Arrow class that we have already seen is not sufficiently
powerful to allow much in the way of useful overloaded code to be written.
Indeed, we will need to add a plethora of other operations to the arrow interface,
divided into a number of different classes, because not all useful arrow types can
support all of them. Implementing all of these operations makes defining a new
arrow type considerably more laborious than defining a new monad — but there
is another side to this coin, as we shall see later. In the remainder of this section,
we will gradually extend the arrow interface until it is as powerful as the monadic
one.
addM a b = do x <- a
y <- b
return (x+y)
80 J. Hughes
But the arrow interface we have seen so far is not even powerful enough to do
this!
Suppose we are given two arrows f and g, which output integers from the
same input. If we could make a pair of their outputs, then we could supply that
to arr (uncurry (+)) to sum the components, and define
addA :: Arrow arr => arr a Int -> arr a Int -> arr a Int
addA f g = f_and_g >>> arr (uncurry (+))
(The composition operator binds less tightly than the other arrow operators).
The new operator is simple to implement for functions and Kleisli arrows:
For stream functions, we just zip the output streams of f and g together. We can
conveniently use the arrow operators on functions to give a concise point-free
definition!
do better than this: note that the input type of f ||| g here, (Bool,a), carries
the same information as Either a a, where (True,a) corresponds to Left a,
and (False,a) to Right a. If we use an Either type as the input to the choice
operator, rather than a pair, then the Left and Right values can carry different
types of data, which is usefully more general. We therefore define
class Arrow arr => ArrowChoice arr where
(|||) :: arr a c -> arr b c -> arr (Either a b) c
Note the duality between (|||) and (&&&) — if we reverse the order of the
parameters of arr in the type above, and replace Either a b by the pair type
(a,b), then we obtain the type of (&&&)! This duality between choice and pairs
recurs throughout this section. As we will see later, not all useful arrow types
can support the choice operator; we therefore place it in a new subclass of Arrow,
so that we can distinguish between arrows with and without a choice operator.
As an example of using conditionals, let us see how to define a map function
for arrows:
mapA :: ArrowChoice arr => arr a b -> arr [a] [b]
The definition of mapA requires choice, because we must choose between the base
and recursive cases on the basis of the input list. We shall express mapA as base-
case ||| recursive-case, but first we must convert the input into an Either type.
We do so using
listcase [] = Left ()
listcase (x:xs) = Right (x,xs)
and define mapA by
mapA f = arr listcase >>>
arr (const []) ||| (f *** mapA f >>> arr (uncurry (:)))
where we choose between immediately returning [], and processing the head
and tail, then consing them together. We will see examples of using mapA once
we have shown how to implement (|||).
Notice first that f ||| g requires that f and g have the same output type,
which is a little restrictive. Another possibility is to allow for different output
types, and combine them into an Either type by tagging f’s output with Left,
and g’s output with Right. We call the operator that does this (+++):
class Arrow arr => ArrowChoice arr where
...
(+++) :: arr a b -> arr c d -> arr (Either a c) (Either b d)
Now observe that (+++) is to (|||) as (***) is to (&&&): in other words, we
can easily define the latter in terms of the former, and the former is (marginally)
simpler to implement. Moreover, it is dual to (***) — just replace Either types
by pairs again, and swap the parameters of arr. In this case the definition of
(|||) becomes
84 J. Hughes
Now, just as (***) combined two arrows into an arrow on pairs, and could be
defined in terms of a simpler combinator which lifted one arrow to the first
components of pairs, so (+++) can be defined in terms of a simpler operator
which just lifts an arrow to the left summand of an Either type. Therefore we
introduce
The idea is that left f passes inputs tagged Left to f, passes inputs tagged
Right straight through, and tags outputs from f with Left. Given left, we can
then define an analogous combinator
With these definitions, mapA behaves like map for functions, and mapM for
Kleisli arrows1:
Moreover, all the elements of xs tagged Right should be copied to the out-
put. But how should the Left and Right values be merged into the final output
stream?
There is no single “right answer” to this question. We shall choose to restrict
our attention to synchronous stream functions, which produce exactly one output
per input2 . With this assumption, we can implement left by including one
element of f’s output in the combined output stream every time an element
tagged Left appears in the input. Thus:
The only stream function arrow we have seen so far which does not preserve
the length of its argument is delay — the delayed stream has one more element
than the input stream. Recall the definition we saw earlier:
delay x = SF (x:)
In order to meet our new restriction, we redefine delay as
delay x = SF (init . (x:))
This does not change the behaviour of the examples we saw above.
As an example of using choice for stream functions, let us explore how mapA
behaves for this arrow type. It is interesting to map the delay arrow over a
stream of lists:
StreamFns> runSF (mapA (delay 0)) [[1,2,3],[4,5,6],[7,8,9]]
[[0,0,0],[1,2,3],[4,5,6]]
_ _ _ _ _ _
| |_| |_| |_| |_| |_| |_
_ _ _ _
__| |_| |_________| |_|
Here the top two signals are the input, and the bottom one the output. As we
would expect, the output is high only when both inputs are low. (The ASCII
graphics are ugly, but easily produced by portable Haskell code: a function which
does so is included in the appendix).
Synchronous circuits contain delays, which we can simulate with the delay
arrow. For example, a rising edge detector can be modelled by comparing the
input with the same signal delayed one step.
edge :: SF Bool Bool
edge = arr id &&& delay False >>> arr detect
where detect (a,b) = a && not b
Testing this arrow might produce
_______ _______
________| |_______|
_ _
________| |_____________| |_____
where a pulse appears in the output at each rising edge of the input.
88 J. Hughes
Now, by connecting two NOR-gates together, one can build a flip-flop (see
Figure 1). A flip-flop takes two inputs, SET and RESET, and produces two
outputs, one of which is the negation of the other. As long as both inputs remain
low, the outputs remain stable, but when the SET input goes high, then the first
output does also, and when the RESET input goes high, then the first output
goes low. If SET and RESET are high simultaneously, then the flip-flop becomes
unstable. A flip-flop is made by connecting the output of each NOR-gate to one
input of the other; the remaining two inputs of the NOR-gates are the inputs of
the flip-flop, and their outputs are the outputs of the flip-flop.
RESET
OR
SET OR
which closely resembles the definition for functions, making a recursive definition
of the feedback stream cs. However, this is just a little too strict. We would of
course expect loop (arr id) to behave as arr id (with an undefined feedback
stream), and the same is true of loop (arr swap), which feeds its input through
the feedback stream to its output. But with the definition above, both these loops
are undefined. The problem is the recursive definition of (bs,cs) above: the
functions unzip and zip are both strict — they must evaluate their arguments
before they can return a result — and so are arr id and arr swap, the two
functions we are considering passing as the parameter f, with the result that the
value to be bound to the pattern (bs,cs) cannot be computed until the value
of cs is known! Another way to see this is to remember that the semantics of
a recursive definition is the limit of a sequence of approximations starting with
the undefined value, ⊥, and with each subsequent approximation constructed by
evaluating the right hand side of the definition, with the left hand side bound to
the previous one. In this case, when we initially bind (bs,cs) to ⊥ then both
bs and cs are bound to ⊥, but now because zip is strict then zip as ⊥=⊥,
because f is strict then f ⊥=⊥, and because unzip is strict then unzip ⊥=⊥.
So the second approximation, unzip (f (zip as ⊥ )), is also ⊥, and by the
same argument so are all of the others. Thus the limit of the approximations is
undefined, and the definition creates a “black hole”.
To avoid this, we must ensure that cs is not undefined, although it may be
a stream of undefined elements. We modify the definition as follows:
instance ArrowLoop SF where
loop (SF f) = SF $ \as ->
let (bs,cs) = unzip (f (zip as (stream cs))) in bs
where stream ~(x:xs) = x:stream xs
The ~ in the definition of stream indicates Haskell’s lazy pattern matching — it
delays matching the argument of stream against the pattern (x:xs) until the
bound variables x and xs are actually used. Thus stream returns an infinite list
without evaluating its argument — it is only when the elements of the result
are needed that the argument is evaluated. Semantically, stream ⊥=⊥:⊥:⊥:
. . .. As a result, provided as is defined, then so is zip as (stream ⊥) —
it is a list of pairs with undefined second components. Since neither f nor
unzip needs these components to deliver a defined result, we now obtain de-
fined values for bs and cs in the second approximation, and indeed the limit
of the approximations is the result we expect. The reader who finds this ar-
gument difficult should work out the sequence of approximations in the call
runSF (loop (arr swap)) [1,2,3] — it is quite instructive to do so.
Note that stream itself is not a length-preserving stream function: its result is
always infinite, no matter what its argument is. But loop respects our restriction
to synchronous stream functions, because zip always returns a list as long as
its shorter argument, which in this case is as, so the lists bound to bs and cs
always have the same length as as.
Returning to the flip-flop, we must pair two NOR-gates, take their out-
puts and duplicate them, feeding back one copy, and supplying each NOR-gate
90 J. Hughes
with one input and the output of the other NOR-gate as inputs. Here is a first
attempt:
flipflop =
loop (arr (\((reset,set),(c,d)) -> ((set,d),(reset,c))) >>>
nor *** nor >>>
arr id &&& arr id)
The first line takes the external inputs and fed-back outputs and constructs the
inputs for each NOR-gate. The second line invokes the two NOR-gates, and the
third line duplicates their outputs.
Unfortunately, this definition is circular: the ith output depends on itself. To
make a working model of a flip-flop, we must add a delay. We do so as follows:
flipflop =
loop (arr (\((reset,set),~(c,d)) -> ((set,d),(reset,c))) >>>
nor *** nor >>>
delay (False,True) >>>
arr id &&& arr id)
which initialises the flip-flop with the first output low. We must also ensure that
the loop body is not strict in the loop state, which explains the lazy pattern
matching on the first line. Note that the delay in the code delays a pair of bits,
and so corresponds to two single-bit delays in the hardware, and the feedback
path in this example passes through both of them (refer to Figure 1). This makes
the behaviour a little less responsive, and we must now trigger the flip-flop with
pulses lasting at least two cycles. For example, one test of the flip-flop produces
this output:
___ ___
________________________| |_________| |___________
___ ___
__________| |_______________________| |___________
___________ _ _ _
______________| |_________________| |_| |_|
___________ ___________ _ _ _
| |_______________| |___| |_| |_|
Here the first two traces are the RESET and SET inputs, and the bottom two
are the outputs from the flip-flop. Initially the first output is low, but when the
SET input goes high then so does the output. It goes low again when the RESET
input goes high, then when both inputs go high, the flip-flop becomes unstable.
The ArrowLoop class, together with instances for functions and Kleisli arrows,
is included in Control.Arrow. Ross Paterson has also suggested overloading
Programming with Arrows 91
delay, and placing it in an ArrowCircuit class, but this has not (yet) found its
way into the standard hierarchical libraries.
2.5 Exercises
1. Filtering. Define
filterA :: ArrowChoice arr => arr a Bool -> arr [a] [a]
to behave as filter on functions, and like filterM on Kleisli arrows.
Experiment with running
filterA (arr even >>> delay True)
on streams of lists of varying lengths, and understand its behaviour.
2. Stream processors. Another way to represent stream processors is using
the datatype
data SP a b = Put b (SP a b) | Get (a -> SP a b)
where Put b f represents a stream processor that is ready to output b and
continue with f, and Get k represents a stream processor waiting for an
input a, which will continue by passing it to k. Stream processors can be
interpreted as stream functions by the function
runSP (Put b s) as = b:runSP s as
runSP (Get k) (a:as) = runSP (k a) as
runSP (Get k) [] = []
Construct instances of the classes Arrow, ArrowChoice, ArrowLoop, and
ArrowCircuit for the type SP.
– You are provided with the module Circuits, which defines the class
ArrowCircuit.
– You should find that you can drop the restriction we imposed on stream
functions, that one output is produced per input — so SP arrows can
represent asynchronous processes.
– On the other hand, you will encounter a tricky point in defining first.
How will you resolve it?
Programming with Arrows 93
– Check that your implementation of loop has the property that the arrows
loop (arr id) and loop (arr swap) behave as arr id:
SP> runSP (loop (arr id)) [1..10]
[1,2,3,4,5,6,7,8,9,10]
SP> runSP (loop (arr swap)) [1..10]
[1,2,3,4,5,6,7,8,9,10]
Module Circuits also exports the definition flipflop above, together
with sample input flipflopInput and a function showSignal which
visualises tuples of lists of booleans as the “oscilloscope traces” we saw
above.
SP> putStr$ showSignal$ flipflopInput
___ ___
________________________| |_________| |___________
___ ___
__________| |_______________________| |___________
Use these functions to test a flipflop using your stream processors as the
underlying arrow type. The behaviour should be the same as we saw
above.
λ-expressions: they denote an arrow, and bind a name (or, in general, a pattern)
to the arrow input, which may then be used in the body of the abstraction.
However, the body of an arrow abstraction is not an expression: it is of a
new syntactic category called a command. Commands are designed to help us
construct just those arrows that can be built using the arrow combinators —
but in a sometimes more convenient notation.
The simplest form of command, f -< exp, can be thought of as a form of
“arrow application” — it feeds the value of the expression exp to the arrow f.
(The choice of notation will make more sense shortly). Thus, for example, an
AND-gate with a delay of one step could be expressed by
proc (x,y) -> delay False -< x && y
This is equivalent to arr (\(x,y) -> x&&y) >>> delay False. Arrow abstrac-
tions with a simple command as their body are translated as follows,
Note that the scope of x in the example now has two holes: the arrow-valued
expressions before each arrow application.
Even in the simple example above, the pointed notation is more concise than
the point-free. When we extend this idea to case commands, its advantage is
even greater. Recall the definition of mapA from section 2.2:
mapA f = arr listcase >>>
arr (const []) ||| (f *** mapA f >>> arr (uncurry (:)))
where listcase [] = Left ()
listcase (x:xs) = Right (x,xs)
We were obliged to introduce an encoding function listcase to convert the
case analysis into a choice between Left and Right. Clearly, a case analysis
with more cases would require an encoding into nested Either types, which
would be tedious in the extreme to program. But all these encodings are gener-
ated automatically from case-commands in the pointed arrow notation. We can
reexpress mapA as
mapA f = proc xs ->
case xs of
[] -> returnA -< []
x:xs’ -> (f *** mapA f >>> uncurry (:)) -< (x,xs’)
which is certainly more readable.
Just as in the monadic notation, we need a way to express just delivering
a result without any effects: this is the rôle of returnA -< [] above, which
corresponds to arr (const []) in the point-free code. In fact, returnA is just
an arrow, but a trivial arrow with no effects: it is defined to be arr id. We
could have written this case branch as arr (const []) -< xs, which would
correspond exactly to the point-free code, but it is clearer to introduce returnA
instead.
in which we name the string read by readFile as s. And now at last the choice
of -< as the arrow application operator makes sense — it is the tail feathers of
an arrow! A binding of the form x <- f -< e looks suggestively as though e is
being fed through an arrow labelled with f to be bound to x!
As another example, recall the rising edge detector from section 2.3:
Notice that both a and b are in scope after the binding of b, although they are
bound at different places. Thus a binding is translated into an arrow that extends
the environment, by pairing the bound value with the environment received as
input. The translation rule is
proc pat -> do x <- c1 (arr id &&& proc pat -> c1) >>>
−→
c2 proc (pat,x) -> c2
where we see clearly which variables are in scope in each command. Applying
the rule to this example, the translation of the pointed definition of edge is
which can be simplified by the arrow laws to the point-free definition we started
with, bearing in mind that arr (\a->a) and returnA are both the identity
arrow, and thus can be dropped from compositions. In practice, GHC can and
does optimise these translations, discarding unused variables from environments,
for example. But the principle is the one illustrated here.
Note that the same variable occupies different positions in the environment in
different commands, and so different occurrences must be translated differently.
The arrow notation lets us use the same name for the same value, no matter
where it occurs, which is a major advantage.
We can use the do notation to rewrite mapA in an even more pointed form.
Recall that in the last section we redefined it as
Programming with Arrows 97
x c1
Half
OR
y Adder s1 c2
Half
c Adder s2
Here the second branch of the case is still expressed in a point-free form. Let us
use do to name the intermediate results:
We are left with a definition in a style which closely resembles ordinary monadic
programming.
When used with the stream functions arrow, the pointed notation can be
used to express circuit diagrams with named signals very directly. For example,
suppose that a half-adder block is available, simulated by
A full adder can be constructed from a half adder using the circuit diagram in
Figure 2. From the diagram, we can read off how each component maps input
signals to output signals, and simply write this down in a do block.
The arrow code is essentially a net-list for the circuit. Without the pointed arrow
notation, we would have needed to pass c past the first half adder, and c1 past
the second, explicitly, which would have made the dataflow much less obvious.
98 J. Hughes
RESET
OR
SET OR
appears at first that we cannot define combinators that take commands as ar-
guments. However, commands do denote arrows from environments to outputs,
and these are first-class values. The pointed arrow notation therefore provides a
mechanism for using arrow combinators as command combinators.
However, we cannot use just any arrow combinator as a command combina-
tor. The commands that appear in pointed arrow notation denote arrows with
types of the form arr env a, where env is the type of the environment that the
command appears in, and a is the type of its output. Now, when we apply a
command combinator, then all of the commands we pass to it naturally occur in
the same environment, which is moreover the same environment that the com-
binator itself appears in. Thus the type of a command combinator should have
the form
arr env a -> arr env b -> ... -> arr env c
That is, all the arrows passed to it as arguments should have the same input
type, which moreover should be the same input type as the arrow produced.
For example, the pairing combinator
(&&&) :: Arrow arr => arr a b -> arr a c -> arr a (b,c)
has just such a type (where a is the type of the environment), while in contrast
(|||) :: Arrow arr => arr a c -> arr b c -> arr (Either a b) c
(One trap for the unwary is that this syntax is ambiguous: in the example above,
&&& could be misinterpreted as a part of the expression following returnA -<.
The dos in the example are there to prevent this. Because of the layout rule, it
is clear in the example above that &&& is not a part of the preceding command.)
When a command combinator is not an infix operator, then applications are
enclosed in banana brackets to distinguish them from ordinary function calls.
Thus (since (&&&) is not an infix operator), we could also write the example
above as
3.5 Exercises
1. Adder. An n-bit adder can be built out of full adders using the design shown
in Figure 4, which adds two numbers represented by lists of booleans, and
delivers a sum represented in the same way, and a carry bit. Simulate this
design by defining
adder :: Arrow arr => Int ->
arr ([Bool],[Bool]) ([Bool],Bool)
Represent the inputs and the sum with the most significant bit first in the list.
2. Bit serial adder. A bit-serial adder can be constructed from a full adder
using feedback, by the circuit in Figure 5. The inputs are supplied to such
an adder one bit at a time, starting with the least significant bit, and the
sum is output in the same way. Model this circuit with an arrow
bsadd :: ArrowCircuit arr => arr (Bool,Bool) Bool
Use the rec arrow notation to obtain feedback. The showSignal function from
module Circuits may be useful again for visualising the inputs and outputs.
3. Filter.
(a) Define
filterA :: ArrowChoice arr => arr a Bool -> arr [a] [a]
again (as in exercise 1 in section 2.5), but this time use the pointed arrow
notation.
(b) Now define a command combinator filterC:
filterC :: ArrowChoice arr =>
arr (env,a) Bool -> arr (env,[a]) [a]
and test it using the following example:
test :: Show a => Kleisli IO [a] [a]
test = proc xs -> (|filterC (\x->Kleisli keep-<x)|) xs
where keep x = do putStr (show x++"? ")
s <- getLine
return (take 1 s == "y")
Running this example might yield:
Main> runKleisli (test3 >>> Kleisli print) [1..3]
1? y
2? n
3? y
[1,3]
102 J. Hughes
x3
Full sum3
y3
Adder
carry
x2
Full sum2
y2
Adder
carry
x1
Full sum1
y1
Adder
carry
x sum
y Full
carry
Adder
delay
4. Counters.
(a) One of the useful circuit combinators used in the Lava hardware descrip-
tion environment [15] is row, illustrated in Figure 6. Let us represent f
by an arrow from pairs to pairs,
f :: arr (a,b) (c,d)
with the components representing inputs and outputs as shown:
Programming with Arrows 103
f f f f
Fig. 6. A row of f
b
a d
f
c
Define a command combinator
delay
sum
Half
Adder
carry
Fig. 7. A 1-bit counter
4 Implementing Arrows
As we observed then, the fact that the second argument of (>>=) is a Haskell
function gives the user a great deal of expressivity “for free” — to obtain similar
expressivity with the arrows interface, we have to introduce a large number of
further operations on the arrow types. However, there is another side to this
coin. When we implement a monad, the (>>=) can do nothing with its second
argument except apply it — the (>>=) operator is forced to treat its second
operand as abstract. When we implement an arrow, on the other hand, the
(>>>) can inspect the representation of its second operand and make choices
based on what it finds. Arrows can carry static information, which is available
to the arrow combinators, and can be used to implement them more efficiently.
This was, in fact, the original motivation for developing arrows. Swierstra
and Duponcheel had developed a parsing library, in which the representation
of parsers included a list of starting symbols, and the choice combinator made
use of this information to avoid backtracking (which can incur a heavy space
penalty by saving previous states in case it is necessary to backtrack to them)
[24]. Their parsing library, in contrast to many others, did not — indeed, could
not — support a monadic interface, precisely because the starting symbols of
f >>= g cannot be determined in general without knowing the starting symbols
of both f and g, and the latter are not available to (>>=). It was the search for an
interface with similar generality to the monadic one, which could be supported
by Swierstra and Duponcheel’s library, which led to my proposal to use arrows.
Programming with Arrows 105
However, since parsing arrows have been discussed in several papers already, we
will take other examples in these notes.
(where ALA stands for arr-lift-arr: a value of the form ALA f g h represents
arr f >>> lift g >>> arr h). We lift underlying arrows to this type by pre-
and post-composing with the identity arrow:
The implementations of the arrow operations just take advantage of the known
pure parts to construct compositions of functions, rather than the underlying
arrow type, wherever possible.
Programming with Arrows 107
input : (False,False)@4.0
input : (True,True)@5.0
output: (False,False)@5.1
input : (False,False)@6.0
output: (False,True)@6.1
output: (True,True)@6.109999999999999
output: (False,True)@6.209999999999999
output: (False,False)@6.209999999999999
output: (False,True)@6.309999999999999
output: (True,True)@6.3199999999999985
output: (False,True)@6.419999999999998
output: (False,False)@6.419999999999998
We can see that when the set or reset input goes high, the flip-flop responds by
quickly setting the appropriate output high, after a brief moment in which both
outputs are low. When both inputs go low, the output does not change, and no
event appears in the trace above. If both inputs are high simultaneously, and
then drop, then the flip-flop becomes unstable and begins to oscillate, generating
many output events although no further events appear in the input.
One application for this more accurate kind of simulation is power estimation
for integrated circuits. Although the behaviour of such a circuit can be simulated
by a synchronous simulation which just computes the final value of each signal on
each clock cycle, the power consumption depends also on how many times each
signal changes its value during a clock cycle. This is because much of the power in
such a circuit is consumed charging and discharging wires, and if this happens
several times during one clock cycle, the power consumed is correspondingly
higher.
Like the stream functions arrow, our new simulation arrows represent a kind
of process, transforming input events to output events. They are thus a rather
typical kind of application for arrows, and useful to study for that reason. More-
over, this library (although much smaller) is closely related to the Yampa sim-
ulation library [8] — many design decisions are resolved in the same way, and
the reader who also studies the internals of Yampa will find much that is famil-
iar. However, there is also much that is different, since Yampa is not a discrete
event simulator: Yampa simulations proceed in steps in which every signal is
calculated, although the “sampling interval” between steps can vary.
increasing sequence. That is, each signal must have a first event before which it
has a constant value no matter how far back in time we go, and after each event
there must be a next event. We can thus represent a signal by its initial value,
and the events at which its value changes3 . We shall represent events by the type
The value of a signal at time t will be the initial value, if t is before the time of
the first event, or the value of the last event at a time less than or equal to t.
This abstract view of signals fits our intended application domain well, but
it is worth noting that we are ruling out at least one useful kind of signal: those
which take a different value at a single point only. Consider an edge detector for
example: when the input signal changes from False to True, we might expect
the output signal to be True at the time of the change, but False immediately
before and after. We cannot represent this: if the output signal takes the value
True at all, then it must remain True throughout some non-zero interval — it
would be non-sensical to say that the output signal has both a rising and a falling
event at the same time.
Of course, a hardware edge detector must also keep its output high for a
non-zero period, so our abstraction is realistic, but nevertheless in some types of
simulation we might find it useful to allow instantaneously different signal values.
For example, if we were simulating a car wash, we might want to represent the
arrival of a car as something that takes place at a particular instant, rather
than something that extends over a short period. What Yampa calls “events”
are in fact precisely such signals: they are signals of a Maybe type whose value is
Nothing except at the single instant where an event occurs. We could incorporate
such signals into our model by associating two values with each event, the value
at the instant of the event itself, and the value at later times, but for simplicity
we have not pursued this approach.
one of them may lie far in the simulated future. In the presence of feedback, it
is disastrous if we cannot compute present simulated events without knowing
future ones, since then events may depend on themselves and simulation may
be impossible. Hence this approach fails. We will use an approach related to the
stream processors of Exercise 2 instead.
Abstractly, though, we will think of a simulation arrow as a function from
an input signal to an output signal. (In practice, we shall parameterise sim-
ulation arrows on a monad, but we ignore that for the time being). But we
place two restrictions on these functions: they should respect causality, and be
time-invariant.
Causality means that the output of an arrow at time t should not be affected
by the value of its input at later times: the future may not affect the past.
Causality is a vital property: it makes it possible to run a simulation from earlier
times to later ones, without the need to return to earlier times and revise the
values of prescient signals when later events are discovered.
Time-invariance means that shifting the input signal of an arrow backwards
or forwards in time should shift the output signal in the same way: the behaviour
should not depend on the absolute time at which events occur. One important
consequence is that the output of an arrow cannot change from its initial value
until the input has (since any such event can depend only on the constant part
of the input by causality, and so can by shifted arbitrarily later in time by time
invariance). This relieves the simulator of the need to simulate all of history,
from the beginning of time until the first input event, since we know that all
signals must retain their initial values until that point.
We shall represent simulation arrows by functions from the initial input value,
to the initial output value and a simulation state:
This state will then evolve as the simulation proceeds, and (in general) it depends
on the initial input value. We parameterise simulation arrows on a monad m (in
our examples, the IO monad), so that it is possible to define probes such as
printA, which we saw in the introduction to this section.
A running simulation can be in one of three states, which we represent using
the following type:
data State m a b =
Ready (Event b) (State m a b)
| Lift (m (State m a b))
| Wait Time (State m a b) (Event a -> State m a b)
Here
which fails if the constructed state is ready to output before the first input, and
we ensure the other invariants hold by constructing simulation states using
ready e r = Ready e (causal (time e) r)
lift m = Lift m
wait t f k = Wait t (causal t f)
(\e -> causal (time e) (k e))
Note, however, that these smart constructors do not ensure time invariance in
the arrows we write, because we write code which manipulates absolute times. It
would be possible to build a further layer of abstraction on top of them, in which
we would program with relative times only (except in monadic actions which do
not affect future output, such as printing the simulated time in printA). For
this prototype library, however, this degree of safety seems like overkill: we rely
on writing correct code instead.
In this section, we shall see how to implement the arrow operations for this type.
Of course, we must require the underlying monad to be a monad:
The arr operation is rather simple to define: arr f just applies f to the initial
input value, and to the value in every subsequent input event. Computing a new
output takes no simulated time.
Note, however, that the output events generated may well be redundant. For
example, if we simulate an AND-gate as follows:
then we see that the output signal carries three events, even though the value
only changes once.
Redundant events are undesirable, even if they seem semantically harmless,
because they cause unnecessary computation as later parts of the simulation
react to the “change” of value. Our approach to avoiding the problem is to
define an arrow nubA which represents the identity function on signals, but drops
redundant events. Inserting nubA into the example above, we now see the output
we would expect:
Main> runSim (arr (uncurry (&&)) >>> nubA >>> printA "out")
(False,False)
[Event 1 (False,True),
Programming with Arrows 115
Event 2 (True,False),
Event 3 (True,True)]
out: False@init
out: [email protected]
Defining nubA just involves some simple programming with our smart
constructors:
nubA :: (Eq a, Monad m) => Sim m a a
nubA = sim $ \a -> return (a,loop a)
where loop a = waitInput $ \(Event t a’) ->
if a==a’ then loop a
else ready (Event t a’) (loop a’)
Why not just include this in the definition of arr? Or why not rename the old
definition of arr to protoArr and redefine arr as follows?
arr f = protoArr f >>> nubA
The reason is types: we are constrained by the type stated for arr in the Arrow
class. The definition above does not have that type — it has the type
arr :: (Arrow arr, Eq b) => (a->b) -> arr a b
with the extra constraint that the output type must support equality, so that
nubA can discard events with values equal to previous one. Of course, we could
just define arr’ as above, and use that instead, but any overloaded arrow func-
tions which we use with simulation arrows, and the code generated from the
pointed arrow notation, will still use the class method arr. For this reason we
make nubA explicit as an arrow, and simply expect to use it often4 .
Composing simulation arrows is easy — we just have to compute the initial
output — but the real work is in composing simulation states.
Sim f >>> Sim g = sim $ \a -> do
(b,sf) <- f a
(c,sg) <- g b
return (c,sf ‘stateComp‘ sg)
When we compose simulation states, we must be careful to respect our invariants.
Since all the outputs of a composition are generated by a single arrow (the second
operand), which should itself fulfill the invariants, we can expect them to be
correctly ordered at least. However, we must see to it that no output is delayed
until after a later input is received by the first operand, which would violate
4
It is interesting to note that the same problem arose in a different context in the
first arrow paper [10]. I have proposed a language extension that would solve the
problem, by allowing parameterised types to restrict their parameters to instances
of certain classes only [9]. With this extension, simulation arrows could require that
their output type be in class Eq, making equality usable on the output even though
no such constraint appears in the Arrow class itself.
116 J. Hughes
When does the output of first f change? Well, clearly, if the output of f
changes, then so does the output of first f. But the output of first f may also
change if its input changes, since the change may affect the second component of
the pair, which is fed directly through to the output. Moreover, when the output
of f changes, then we need to know the current value of the second component
of the input, so we can construct the new pair of output values. Likewise, when
the input to first f changes, and we have a new second component for the
output pair, then we need to know the current value of the first component of
the output to construct the new output pair. For this reason, the stateFirst
function is parameterised not only on the state of f, but also on the current
values of the components of the output.
In the light of this discussion, we can see that if f is waiting, then first f
should also wait: no change to the output can occur until either the deadline
is reached, or an input is received. If an input is received before the deadline,
then first f is immediately ready to output a new pair containing an updated
second component.
stateFirst b c (Wait t s k) =
wait t (stateFirst b c s) $ \(Event t’ (a,c’)) ->
ready (Event t’ (b,c’)) (stateFirst b c’ (k (Event t’ a)))
The trickiest case is when f is ready to output an event. Before we can actually
output the corresponding event from first f, we must ensure that there are
no remaining inputs at earlier times, which would cause changes to the output
of first f that should precede the event we are about to generate. The only
way to ensure this is to wait until the simulated time at which the event should
occur, and see whether we timeout or receive an input. Thus we define:
stateFirst b c (Ready b’ s) =
wait (time b’)
(ready (Event (time b’) (value b’,c))
(stateFirst (value b’) c s))
(\(Event t’ (a,c’)) ->
ready (Event t’ (b,c’))
(stateFirst b c’
(ready b’ (s ‘after‘ (Event t’ a)))))
After waiting without seeing an input until the time of the new event, we can
generate a new output immediately. If an input is received in the meantime, we
generate a corresponding output event and continue waiting, but in the state
118 J. Hughes
we reach by feeding the first component of the input just received into the
state of f.
This definition seems very natural, but it does exhibit a surprising behaviour
which we can illustrate with the following example:
Although there is only one input event, first generates two output events, one
generated by the change of the input, and the other by the change in the output
of arr (+1). Moreover, both output events occur at the same simulated time —
and how should we interpret that? Our solution is to declare that when several
events appear at the same simulated time, then the last one to be generated
gives the true value of the output signal at that time. The previous events we
call “glitches”, and they represent steps towards the correct value. Glitches arise,
as in this example, when two parts of the simulation produce output events at
exactly the same time, and they are then combined into the same signal. A glitch
results because we make no attempt to identify such simultaneous events and
combine them into a single one.
We put up with glitches, because of our design decision to produce output
events as soon as possible. To eliminate them, we would, among other things,
need to change the semantics of Wait. At present, Wait times out if no input
event arrives before the deadline; we would need to change this to time out only
if the next input event is after the deadline, thus allowing inputs that arrive
exactly on the deadline to be received, possibly affecting the output at the same
simulated time. The danger would be that some arrows would then be unable
to produce their output at time t, without seeing a later input event — which
would make feedback impossible to implement. However, it is not obvious that
this would be inevitable, and a glitch-free version of this library would be worth
investigating. For the purpose of these notes, though, we stick with the glitches.
We will, however, introduce an arrow to filter them out: an arrow which
copies its input to its output, but whose output is glitch-free even when its
input contains glitches. To filter out glitches in the input at time t, we must
wait until we can be certain that all inputs at time t have been received —
which we can only be after time t has passed! It follows that a glitch-remover
must introduce a delay: it must wait until some time t’ later than t, before it
can output a copy of the input at time t. We therefore build glitch removal into
our delay arrow, which we call delay1, because there is at most one event at
each simulated time in its output.
In contrast to the delay arrow we saw earlier, in this case we do not need
to supply an initial value for the output. The stream function delay, for use in
synchronous simulation, delays its output by one step with respect to its input,
and needs an initial value to insert in the output at the first simulation step. In
Programming with Arrows 119
contrast, our signals all have an initial value “since time immemorial”, and the
output of delay1 just has the same initial value as its input.
We define delay1 as follows:
delay1 d = sim (\a -> return (a,r))
where r = waitInput loop
loop (Event t a) =
wait (t+d) (ready (Event (t+d) a) r) $
\(Event t’ a’) ->
if t==t’
then loop (Event t’ a’)
else ready (Event (t+d) a) (loop (Event t’ a’))
It delays its input signal by time d, which permits it to wait until time t+d to
be sure that the input at time t is stable.
also track the current values of the loop input and the loop state, so as to be
able to construct a new input pair for the loop body when either one changes.
After this discussion, we can present the code of stateLoop. If the loop body
is ready to output, the loop as a whole does so, and the change to the loop state
is added to the pending queue.
If the loop body is waiting, and there are no pending state changes, then the
loop as a whole waits. If a new input is received, then it is passed together with
the current loop state to the loop body, and the current input value is updated.
stateLoop a c [] (Wait t s k) =
wait t (stateLoop a c [] s) $ \(Event t’ a’) ->
stateLoop a’ c [] (k (Event t’ (a’,c)))
Finally, if the loop body is waiting and there are pending state changes, then
we must wait and see whether any input event arrives before the first of them.
If so, we process the input event, and if not, we make the state change.
As a simple example, let us simulate a loop which simply copies its input to its
output — a loop in which the feedback is irrelevant.
This state change is fed back to the loop body, and generates another new (and
also unchanged) state, and so on. To avoid this, we must discard state “changes”
which do not actually change the state, by inserting a nubA arrow into the loop
body.
Sim> runSim (loop nubA >>> printA "out")
0
[Event 1 1, Event 2 2]
out: 0@init
out: [email protected]
*** Exception: <<loop>>
This does not work either! In this case, the exception “<<loop>>” is generated
when a value depends on itself, and it is fairly clear which value that is — it
is, of course, the loop state, whose initial value is undefined5 . Indeed, since the
initial value of the loop state cannot be determined from the input, there is
no alternative to specifying it explicitly. We therefore define a new combinator,
which provides the initial output value for an arrow explicitly:
initially x (Sim f) = Sim $ \a -> do (_,s) <- f a
return (x,s)
Now we can revisit our example and initialise the loop state as follows:
Sim> runSim (loop (initially (0,0) nubA) >>> printA "out") 0
[Event 1 1, Event 2 2]
out: 0@init
out: [email protected]
out: [email protected]
At last, it works as expected.
Now, although this example was very trivial, the difficulties that arose will
be with us every time we use loop: the loop state must (almost) always be
initialised, and we must always discard redundant state changes, to avoid gen-
erating an infinite number of events. This means that we will always need to
include nubA in a loop body. This is not an artefact of our particular library,
but a fundamental property of simulation with feedback: after an input change,
a number of state changes may result, but eventually (we hope) the state will
stabilise. A simulator must continue simulating until the state reaches a fixed
point, and that is exactly what loop with nubA does.
It would be natural, now, to include nubA in the definition of loop, since
it will always be required, but once again the type stated for loop in the class
ArrowLoop prevents us from doing so. Instead, we define a new looping combi-
nator just for simulations, which combines loop with nubA. We give it a type
analogous to mfix:
5
The generation of loop exceptions is somewhat variable between one GHC version
and another. The output shown here was generated using version 6.2.1, but other
versions might actually loop infinitely rather than raise an exception.
122 J. Hughes
5.5 Examples
In this section we will give just a few simple examples to show how the simulation
arrows can be used to simulate small circuits. First let us revisit the nor gate:
we can now make our simulation more realistic, by including a gate delay.
nor = proc (a,b) -> do
(a’,b’) <- delay1 0.1 -< (a,b)
returnA -< not (a’||b’)
A nor gate can be used to construct an oscillator, which generates an oscillating
signal as long as its input is low:
oscillator = proc disable ->
(|afix (\x -> nor -< (disable,x))|)
Here the output of the nor gate is fed back, using afix, to one of its inputs.
While disable is low, the nor gate simply inverts its other input, and so the
circuit acts as an inverter with its output coupled to its input, and oscillates.
When disable is high, then the output of the nor gate is always held low.
Running a simulation, we see that the oscillator behaves as expected:
Sim> runSim (oscillator >>> printA "out") True
[Event 1 False, Event 2 True]
out: False@init
out: [email protected]
out: [email protected]
out: [email protected]
out: [email protected]
out: [email protected]
out: [email protected]
out: [email protected]
out: [email protected]
out: [email protected]
out: [email protected]
Of course, it is important to initialise the input signal to True, since otherwise
the oscillator should oscillate “since time immemorial”, and we cannot represent
that. If we try, we find that the output of the oscillator is undefined.
It is interesting that in this example, we did not need to initialise the oscillator
state. This is because the initial state is the solution to the equation
Programming with Arrows 123
x = not (True || x)
and this is equal to False, because Haskell’s “or” operator (||) is not strict in
its second input, when the first one is True.
Finally, let us see how we can use afix to define a flip-flop:
flipflop = proc (reset,set) ->
(|afix (\ ~(x,y)->do
x’ <- initially False nor -< (reset,y)
y’ <- initially True nor -< (set,x)
returnA -< (x’,y’))|)
Although this is not quite as notationally elegant as the rec syntax, we can see
that afix does let us emulate recursive definitions rather nicely, and that we are
able to describe the flip-flop in a natural way. As usual, the argument of a fix-
point combinator cannot be strict, so we must match on the argument pair lazily.
Simulating this flip-flop, we obtain the results presented in the introduction to
this section.
5.6 Exercises
input, and when should it be taken from f? It seems natural to take the
Right input when it is present, and the output from f when it is not, with
the result that the multiplexing of the output channel is the same as the
multiplexing of the input.
Implement left according to the ideas in this discussion, and experiment
with the if-then-else pointed arrow notation (which uses it) to investigate
its usefulness.
6 Arrows in Perspective
Arrows in Haskell are directly inspired by category theory, which in turn is just
the theory of arrows — a category is no more than a collection of arrows with
certain properties. Thus every time we define a new instance of class Arrow,
we construct a category! However, categorical arrows are more general than
their Haskell namesakes, in two important ways. Firstly, the “source” and “tar-
get” of Haskell arrows, that is the types of their inputs and outputs, are just
Haskell types. The source and target of a categorical arrow can be anything at
all — natural numbers, for example, or pet poodles. In the general case, most
of the operations we have considered make no sense — what would the target
of f &&& g be, in a category where the targets of arrows are natural numbers?
Secondly, Haskell arrows support many more operations than categorical arrows
do. In fact, categorical arrows need only support composition and identity ar-
rows (which we constructed as arr id). In general, categories have no equivalent
even of our arr operator, let alone all the others we have considered. Thus even
Haskell arrows which are only instances of class Arrow have much more structure
than categorical arrows do in general.
However, as we saw in the introduction, there is little interesting that can
be done without more operations on arrows than just composition. The same
is true in category theory, and mathematicians have explored an extensive flora
of additional operations that some categories have. Of particular interest to
programming language semanticists are cartesian closed categories, which have
just the right structure to model λ-calculus. In such models, the meaning of
a λ-expression is an arrow of the category, from the types of its free vari-
ables (the context), to the type of its value — compare with the translations
of Paterson’s arrow notation. The advantage of working categorically is that
one can study the properties of all semantic models at once, without cluttering
one’s work with the details of any particular one. Pierce’s book is an excel-
lent introduction to category theory from the programming language semantics
perspective [21].
One may wonder, then, whether the structure provided by Haskell arrows
has been studied by theoreticians? The answer turns out to be “yes”. Monads,
which were invented by category theorists for quite different purposes, were first
connected with computational effects by Moggi [14], who used them to structure
denotational semantic descriptions, and his work was the direct inspiration for
Wadler to introduce monads in Haskell [26]. But Power and Robinson were
Programming with Arrows 125
achieved elegantly using a monad, and this insight lies behind his Wash/CGI
library [25].
Ross Paterson developed the pointed arrow notation presented in these notes
[17], collaborated with Peyton-Jones on its implementation in GHC, and is one of
the people behind the arrows web page [6]. Paterson introduced the ArrowLoop
class, and has developed an extensive experimental arrow library containing
many arrow transformers, and classes to make arrows constructed using many
transformers easy to use. Paterson applied arrows to circuit simulation, and made
an arrowized version of Launchbury et als. architecture description language
Hawk [12]. A good description of this work can be found in Paterson’s excellent
tutoral on arrows [18].
Patrik Jansson and Johan Jeuring used arrows to develop polytypic data
conversion algorithms [11]. Their development is by equational reasoning, and
the advantage of arrows in this setting is just the point-free notation — Jansson
and Jeuring could have worked with monads instead, but their proofs would
have been much clumsier.
Joe English uses arrows in his library for parsing and manipulating XML
[7]. Inspired by Wallace and Runciman’s HaxML [27], XML is manipulated by
composing filters, which are almost, but not quite, functions from an a to a list
of bs. Filters are defined as an arrow type, and the advantage of using arrows
rather than a monad in this case is that the composition operator can be very
slightly stricter, which improves memory use when the filters are run.
Courney and Elliott developed an arrow-based GUI library called Fruit [5],
based on functional reactive programming. Fruit considers a GUI to be a map-
ping between the entire history of the user’s input (mouse motion, button presses,
etc), and the history of the appearance of the screen — from a user input signal
to a screen output signal. The GUIs are implemented as arrows, which leads to
a very attractive programming style.
Indeed, arrows have been adopted comprehensively in recent work on func-
tional reactive programming, now using a system called Yampa [8]. Yampa pro-
grams define arrows from input signals to output signals, where a signal, just as
in these notes, is a function from time to a value. Functional reactive program-
ming is older than arrows, of course, and in its original version programmers
wrote real functions from input signals to output signals. The disadvantage of
doing so is that signals become real Haskell values, and are passed around in
FRP programs. Since a signal represents the entire history of a value, and in
principle a program might ask for the signal’s value at any time, it is difficult
for the garbage collector to recover any memory. In the arrowized version, in
contrast, signals are not first-class values, and the arrow combinators can be
implemented to make garbage collection possible. As a result, Yampa has much
better memory behaviour than the original versions of FRP.
Finally, arrows have been used recently by the Clean group to develop graph-
ical editor components [19]. Here a GUI is seen as a kind of editor for an underly-
ing data structure — but the data structure is subject to constraints. Whenever
the user interacts with the interface, thus editing the underlying data, the editor
Programming with Arrows 127
reacts by modifying other parts, and possibly performing actions on the real
world, to reestablish the constraints. Editors are constructed as arrows from the
underlying datatype to itself: invoking the arrow maps the data modified by
the user to data in which the constraints are reestablished. This work will be
presented at this very summer school.
If these applications have something in common, it is perhaps that arrows
are used to combine an attractive programming style with optimisations that
would be hard or impossible to implement under a monadic interface. Arrows
have certainly proved to be very useful, in applications I never suspected. I hope
that these notes will help you, the reader, to use them too.
References
1. Nick Benton and Martin Hyland. Traced premonoidal categories. ITA, 37(4):273–
299, 2003.
2. R. S. Bird. A calculus of functions for program derivation. In D. Turner, editor,
Research Topics in Functional Programming. Addison-Wesley, 1990.
3. M. Carlsson and T. Hallgren. FUDGETS - A graphical user interface in a lazy
functional language. In Proceedings of the ACM Conference on Functional Pro-
gramming and Computer Architecture, Copenhagen, 1993. ACM.
4. Koen Claessen and John Hughes. Quickcheck: A lightweight tool for random test-
ing of Haskell programs. In International Conference on Functional Programming
(ICFP). ACM SIGPLAN, 2000.
5. Anthony Courtney and Conal Elliott. Genuinely functional user interfaces. In
Haskell Workshop, pages 41–69, Firenze, Italy, 2001.
6. Antony Courtney, Henrik Nilsson, and Ross Paterson. Arrows: A general interface
to computation. http://www.haskell.org/arrows/.
7. Joe English. Hxml. http://www.flightlab.com/∼joe/hxml/.
8. Paul Hudak, Antony Courtney, Henrik Nilsson, and John Peterson. Arrows, robots,
and functional reactive programming. In Summer School on Advanced Functional
Programming 2002, Oxford University, volume 2638 of Lecture Notes in Computer
Science, pages 159–187. Springer-Verlag, 2003.
9. J. Hughes. Restricted Datatypes in Haskell. In Third Haskell Workshop. Utrecht
University technical report, 1999.
10. John Hughes. Generalising monads to arrows. Science of Computer Programming,
37(1–3):67–111, 2000.
11. Patrik Jansson and Johan Jeuring. Polytypic data conversion programs. Science
of Computer Programming, 43(1):35–75, 2002.
12. John Launchbury, Jeffrey R. Lewis, and Byron Cook. On embedding a microarchi-
tectural design language within Haskell. In ICFP, pages 60–69, Paris, 1999. ACM
Press.
13. Sheng Liang, Paul Hudak, and Mark P. Jones. Monad transformers and mod-
ular interpreters. In Symposium on Principles of Programming Languages, San
Francisco, January 1995. ACM SIGPLAN-SIGACT.
14. Eugenio Moggi. Computational lambda-calculus and monads. In Proceedings 4th
Annual IEEE Symp. on Logic in Computer Science, LICS’89, Pacific Grove, CA, USA,
5–8 June 1989, pages 14–23. IEEE Computer Society Press, Washington, DC, 1989.
15. M. Sheeran P. Bjesse, K. Claessen and S. Singh. Lava: Hardware design in Haskell.
In ICFP. ACM Press, 1998.
128 J. Hughes
import Control.Arrow
import List
flipflopInput = sig
[(5,(False,False)),(2,(False,True)),(5,(False,False)),
(2,(True,False)),(5,(False,False)),(2,(True,True)),
(6,(False,False))]
Epigram: Practical Programming
with Dependent Types
Conor McBride
1 Motivation
You can’t, of course: this program is obviously nonsense unless you’re a type-
checker. The trouble is that only certain computations make sense if the null xs
test is True, whilst others make sense if it is False. However, as far as the type
system is concerned, the type of the then branch is the type of the else branch is
the type of the entire conditional. Statically, the test is irrelevant. Which is odd,
because if the test really were irrelevant, we wouldn’t do it. Of course, tail []
doesn’t go wrong—well-typed programs don’t go wrong—so we’d better pick a
different word for the way they do go.
Abstraction and application, tupling and projection: these provide the ‘soft-
ware engineering’ superstructure for programs, and our familiar type systems
ensure that these operations are used compatibly. However, sooner or later, most
programs inspect data and make a choice—at that point our familiar type sys-
tems fall silent. They simply can’t talk about specific data. All this time, we
thought our programming was strongly typed, when it was just our software en-
gineering. In order to do better, we need a static language capable of expressing
the significance of particular values in legitimizing some computations rather
than others. We should not give up on programming.
James McKinna and I designed Epigram [27,26] to support a way of program-
ming which builds more of the intended meaning of functions and data into their
types. Its style draws heavily from the Alf system [13,21]; its substance from my
to Randy Pollack’s Lego system [20,23] Epigram is in its infancy and its imple-
mentation is somewhat primitive. We certainly haven’t got everything right, nor
have we yet implemented the whole design. We hope we’ve got something right.
In these notes, I hope to demonstrate that such nonsense as we have seen above
is not inevitable in real life, and that the extra articulacy which dependent types
offer is both useful and usable. In doing so, I seek to stretch your imaginations
towards what programming can be if we choose to make it so.
V. Vene and T. Uustalu (Eds.): AFP 2004, LNCS 3622, pp. 130–170, 2005.
c Springer-Verlag Berlin Heidelberg 2005
Epigram: Practical Programming with Dependent Types 131
Datatypes like Matrix i j may depend on values fixing some particular prop-
erty of their elements—a natural number indicating size is but one example. A
function can specialize its return type to suit each argument. The typing rules
for abstraction and application show how:
x :S t : T f : ∀x : S ⇒ T s : S
λx ⇒ t : ∀x : S ⇒ T f s : [s/x ]T
but we’d have to stop sooner or later, and we’d have difficulty abstracting over
either the whole collection, or specific subcollections like lists of even length. In
Epigram, we can express the lot in one go, giving us the family of vector types
with indices from Nat representing length. Nat is just an ordinary datatype.
data where ; n : Nat
Nat : zero : Nat suc n : Nat
n : Nat ; X :
data where
Vec n X : vnil : Vec zero X
x : X ; xs : Vec n X
vcons x xs : Vec (suc n) X
Inductive families [15], like Vec, are collections of datatypes, defined mutually
and systematically, indexed by other data. Now we can use the dependent func-
tion space to give the ‘tail’ function a type which prevents criminal behaviour:
For no n is vtail n X vnil well typed. Indexed types state properties of their
data which functions can rely on. They are the building blocks of Epigram pro-
gramming. Our Matrix i j can just be defined as a vector of columns, say:
rows, cols : Nat
let Matrix rows cols ⇒ Vec cols (Vec rows Nat)
Matrix rows cols :
Already in Haskell, there are hooks available to crooks who want more control
over data. One can exploit non-uniform polymorphism to enforce some kinds of
structural invariant [31], like this
although this type merely enforces rectangularity, rather than a specific size.
One can also collect into a Vec type class those functors which generate vector
structures [24]. Matrix multiplication then acquires a type like
mult :: (Vec f,Vec g,Vec h)=> f (g Int) -> g (h Int) -> f (h Int)
It may surprise (if not comfort) functional programmers to learn that de-
pendently typed programming seems odd to type theorists too. Type theory is
usually seen either as the integration of ‘ordinary’ programming with a logical
superstructure, or as a constructive logic which permits programs to be quietly
extracted from proofs. Neither of these approaches really exploits dependent
types in the programs and data themselves. At time of writing, neither Agda
nor Coq offers substantial support for the kind of data structures and programs
we shall develop in these notes, even though Alf and ‘Oleg’ did!
There is a tendency to see programming as a fixed notion, essentially untyped.
In this view, we make sense of and organise programs by assigning types to them,
the way a biologist classifies species, and in order to classify more the exotic
creatures, like printf or the zipWith family, one requires more exotic types.
This conception fails to engage with the full potential of types to make a positive
contribution to program construction. Given what types can now express, let us
open our minds afresh to the design of programming language constructs, and
of programming tools and of the programs we choose to write anyway.
I’ve dropped from these notes a more formal introduction to type theory:
which introductory functional programming text explains how the typechecker
works within the first forty pages? A precise understanding of type theory isn’t
necessary to engage with the ideas, get hold of the basics and start programming.
I’ll deal with technicalities as and when we encounter them. If you do feel the
need to delve deeper into the background, there’s plenty of useful literature out
there—the next subsection gives a small selection.
Moreover, to save space here, I adopt an end-of-line style with braces {},
where the system puts them at the beginning.
At the top level, the document is a vertical sequence of declarations delin-
eated by rules. A rule is a sequence of at least three ---. The initial document
has just one declaration, consisting of a shed, waiting for you to start work.
Let’s examine the new technology in the context of a simple and familiar prob-
lem: adding natural numbers. We have seen this Epigram definition:4
data where ; n : Nat
Nat : zero : Nat suc n : Nat
This signals our intention to define a function called plus which takes two
natural numbers and returns a natural number. The machine responds
plus x y [ ]
by way of asking ‘So plus has two arguments, x and y. What should it do with
them?’. Our type signature has become a programming problem to be solved
interactively. The machine supplies a left-hand side, consisting of a function
4
Unary numbers are not an essential design feature; rest assured that primitive binary
numbers will be provided eventually [10].
5
Isn’t this a big step backwards for brevity? Or does Haskell’s brevity depend on
some implicit presumptions about what datatypes can possibly be?
138 C. McBride
plus x y ⇐ rec x {
plus x y [ ] }
plus x y ⇐ rec x {
plus x y ⇐ case x {
plus zero y [ ]
plus (suc x ) y [ ] }}
The two subproblems are precisely those corresponding to the two ways x
could have been made by constructors of Nat. We can certainly finish the first
one off, entering ⇒ y. (The ‘ ⇒ ’ is ‘return’.) We can make some progress on
the second by deciding to return the successor of something, ⇒ suc [ ] .
plus x y ⇐ rec x {
plus x y ⇐ case x {
plus zero y ⇒ y
plus (suc x ) y ⇒ suc [ ] }}
Select the remaining shed and you’ll see that we have to fill in an element
of Nat, given x , y and a subtly different memo-structure. Case analysis has
instantiated the original argument, so we now have the ‘ability to make recursive
Epigram: Practical Programming with Dependent Types 139
plus x y ⇐ rec x {
plus x y ⇐ case x {
plus zero y ⇒ y
plus (suc x ) y ⇒ suc (plus x y) }}
We wrote the type signature: we might have done that for virtue’s sake,
but virtue doesn’t pay the rent. Here, we were repaid—we exchanged the usual
hand-written left-hand sides for machine-generated patterns resulting from the
conceptual step (usually present, seldom written) ⇐ case x . This was only
possible because the machine already knew the type of x . We also wrote the
⇐ rec x , a real departure from conventional practice, but we got repaid for that
too—we (humans and machines) know that plus is total.
Perhaps it’s odd that the program’s text is not entirely the programmer’s
work. It’s the record of a partnership where we say what the plan is and the
machine helps us carry it out. Contrast this with the ‘type inference’ model of
programming, where we write down the details of the execution and the machine
tries to guess the plan. In its pure form, this necessitates the restriction of plans
to those which are blatant enough to be guessed. As we move beyond the Hindley-
Milner system, we find ourselves writing down type information anyway.
‘Type inference’ thus has two aspects: ‘top-level inference’—inferring type
schemes, as with Hindley-Milner ‘let’—and ‘program inference given types’—
inferring details when a scheme is instantiated, as with Hindley-Milner variables.
Epigram rejects the former, but takes the latter further than ever. As types
represent a higher-level design statement than programs, we should prefer to
write types if they make programs cheaper.
Despite its interactive mode of construction, Epigram is fully compliant with
the convention that a file of source code, however manufactured, contains all
that’s required for its recognition as a program. The bare text, without colour
or other markup, is what gets elaborated. The elaboration process for a large
code fragment just reconstructs a suitable interactive development offstage, cued
140 C. McBride
by the program text—this is how we reload programs. You are free to negotiate
your own compromise between incremental and batch-mode programming.
−p−
primRec{ } :: Nat -> p -> (Nat -> p -> p) -> p
−p−
primRec{ } Zero mz ms = mz
−p−
primRec{ } (Suc n) mz ms = ms n (primRec{− p −
} n mz ms)
I’ve made primRec’s type parameter explicit in a comment so we can follow what
happens as we put it to work. How might we write plus? Try applying primRec
to the first argument, then checking what’s left to do:
Now we must fill in the methods mz and ms, but do their types show what
rôle they play? There are seven occurrences6 of Nat in their types—which is
which? Perhaps you can tell, because you understand primRec, but how would
a machine guess? And if we were defining a more complex function this way, we
might easily get lost—try defining equality for lists using foldr.
However, recall our NatInd principle with operational behaviour just like
primRec but a type which makes clear the relationship between the methods
and the patterns for which they apply. If we’re careful, we can use that extra
information to light our way. Where primRec takes a constant type parameter,
NatInd takes a function P : Nat → . If we take P x ; Nat → Nat, we
get the primRec situation. How might we use P ’s argument to our advantage?
Internally, Epigram doesn’t build plus : Nat → Nat → Nat but rather a proof
such that
call plus x y (return plus x y n) ; n
Given ♦plus, we may readily extract plus—apply and run!
plus ; λx , y ⇒ call plus x y (♦plus x y) : Nat → Nat → Nat
Now, let’s build ♦plus as a proof by NatInd:
♦plus ; NatInd (λx ⇒ ∀y : Nat ⇒ plus x y : Nat ) mz ms
where mz : ∀y : Nat ⇒ plus zero y : Nat
ms : ∀x : Nat ⇒ (∀y : Nat ⇒ plus x y : Nat )
→ ∀y : Nat ⇒ plus (suc x ) y : Nat
It’s not hard to see how to generate the left-hand sides of the subproblems
for each case—just read them off from their types! Likewise, it’s not hard to see
how to translate the right-hand sides we supplied into the proofs—pack them
up with return · · · translate recursive calls via call · · · :
mz ; λy ⇒ return plus zero y y
ms ; λx ⇒ λxhyp ⇒ λy ⇒
return plus (suc x ) y (suc (call plus x y (xhyp y)))
From this proof, you can read off both the high-level program and the its low-level
operational behaviour in terms of primitive recursion. And that’s basically how
Epigram works! Dependent types aren’t just the basis of the Epigram language—
the system uses them to organise even simply typed programming.
then the types of mz and ms give us the split we saw when we wrote the program.
What about rec x ?
rec x : ∀P : Nat → ⇒
(∀x : Nat ⇒ (memo x ) P → P x ) →
Px
This says ‘if you want to do P x , show how to do it given access to P for ev-
erything structurally smaller than x ’. This (memox ) is another gadget generated
by Epigram from the structure of x ’s type—it uses the power of computation in
types to capture the notion of ‘structurally smaller’:
(memo zero) P ; One
(memo (suc n)) P ; (memo n) P ∧ P n
That is (memox )P is the type of a big tuple which memoizes P for everything
smaller than x . If we analyse x , the memo-structure computes,7 giving us the
trivial tuple for the zero case, but for (suc n), we gain access to P n. Let’s watch
the memo-structure unfolding in the inevitable Fibonacci example.
let n : Nat ; fib n ⇐ rec n {
fib n : Nat
fib n ⇐ case n {
fib zero ⇒ zero
fib (suc n) ⇐ case n {
fib (suc zero) ⇒ suc zero
fib (suc (suc n)) ⇒ [ ] }}}
If you select the remaining shed, you will see that the memo structure in the
context has unfolded (modulo trivial algebra) to:
(memo n) (λx ⇒ fib x : Nat ) ∧ fib n : Nat ∧ fib (suc n) : Nat
which is just as well, as we want to fill in plus (fib n) (fib (suc n)).
At this stage, the approach is more important than the details. The point is
that programming with ⇐ imposes no fixed notion of case analysis or recursion.
Epigram does not have ‘pattern matching’. Instead, ⇐ admits whatever notion of
problem decomposition is specified by the type of the expression (the eliminator )
which follows it. The value of the eliminator gives the operational semantics to
the program built from the solutions to the subproblems.
Of course, Epigram equips every datatype with case and rec, giving us the
usual notions of constructor case analysis structural recursion. But we are free
to make our own eliminators, capturing more sophisticated analyses or more
powerful forms of recursion. By talking about patterns, dependent types give us
the opportunity to specify and implement new ways of programming.
7
Following a suggestion by Thierry Coquand, Eduardo Giménez shows how to sep-
arate induction into case and rec in [17]. He presents memo-structures inductively,
to justify the syntactic check employed by Coq’s Fix construct. The computational
version is mine [23]; its unfolding memo-structures give you a menu of recursive calls.
Epigram: Practical Programming with Dependent Types 143
∀P : ⇒ (P → P) → P
to which only the run time system gives a computational behaviour; a less drastic
way is to treat general recursion as an impure monadic effect.
But in any case, you might be surprised how little you need general recursion.
Dependent types make more programs structurally recursive, because dependent
types have more structure. Inductive families with inductive indices support
recursion on the data itself and recursion on the indices. For example, first-
order unification [36] becomes structurally recursive when you index terms by
144 C. McBride
the number of variables over which they are constructed—solving a variable may
blow up the terms, but it decreases this index [25].
Note that I didn’t declare X in the rules for nothing and just. The hypotheses
of a rule scope only over its conclusion, so it’s not coming from the Maybe rule.
Rather, in each rule Epigram can tell from the way X is used that it must be a
type, and it silently generalizes the constructors, just the way the Hindley-Milner
system generalizes definitions.
It’s the natural deduction notation which triggers this generalization. We
were able to define Bool without it because there was nothing to generalize.
Without the rule to ‘catch’ the X , plain
nothing : Maybe X
wouldn’t exactly be an error. The out-of-scope X is waiting to be explained by
some prior definition: the nothing constructor would then be specific to that X.
Rule-induced generalization is also happening here, for polymorphic lists:
X : x : X ; xs : List X
data where ;
List X : nil : List X cons x xs : List X
We also need the binary trees with N -labelled nodes and L-labelled leaves.
N,L : l : L n : N ; s, t : Tree N L
data where ;
Tree N L : leaf l : Tree N L node n s t : Tree N L
Exercise 3 (merge). Use the above to define the function which merges two
lists, presumed already sorted into increasing order, into one sorted list contain-
ing the elements from both.
let xs, ys : List Nat
merge xs ys : List Nat
Is this function structurally recursive on just one of its arguments? Nested recs
combine lexicographically.
Maintain this balancing invariant throughout: in (node true s t), s and t contain
equally many numbers, whilst in (node false s t), s contains exactly one more
number than t.
Moving on to dependent data structures now, let’s take a closer look at Vec:
n : Nat ; X :
data where
Vec n X : vnil : Vec zero X
x : X ; xs : Vec n X
vcons x xs : Vec (suc n) X
What happens when we elaborate? Well, consider which constructors can pos-
sibly have made ys. Certainly not vnil, unless zero = suc n. We just get a vcons
case—the one case we want, for ‘head’ and ‘tail’:
ys : Vec (suc m) Y
let ; vhead ys ⇐ case ys {
vhead ys : Y
vhead (vcons y ys) ⇒ y }
ys : Vec (suc m) Y
let ; vtail ys ⇐ case ys {
vtail ys : Vec m Y
vtail (vcons y ys) ⇒ ys }
In the latter, not only do we get that it’s vcons as opposed to vnil: it’s the
particular vcons which extends vectors of the length we need. What’s going on?
Epigram: Practical Programming with Dependent Types 147
Only the vcons case survives—Epigram then tries to choose names for the pat-
tern variables which maintain a ‘family resemblance’ to the scrutinee, hence the
(vcons y ys) in the patterns.
This unification doesn’t just rule cases in or out: it can also feed information
to type-level computations. Here’s how to append vectors:
xs : Vec m X ; ys : Vec n X
let
vappendm xs ys : Vec (plus m n) X
vappendm xs ys ⇐ rec xs {
vappendm xs ys ⇐ case xs {
vappendzero vnil ys ⇒ ys
vappend(suc m) (vcons x xs) ys ⇒ vcons x (vappendm xs ys) }}
Let’s examine the consequences of dependent case analysis for a different family:
data n : Nat where ; i : Fin n
Fin n : fz : Fin (suc n) fs i : Fin (suc n)
Fin zero is empty, and each Fin (suc n) is made by embedding the n ‘old’
elements of Fin n, using fsn , and adding a ‘new’ element fzn . Fin n provides a
representation of numbers bounded by n, which can be used as ‘array subscripts’:
xs : Vec n X ; i : Fin n
let ; vproj xs i ⇐ rec xs {
vproj xs i : X
vproj xs i ⇐ case xs {
vproj vnil i ⇐ case i
vproj (vcons x xs) i ⇐ case i {
vproj (vcons x xs) fz ⇒ x
vproj (vcons x xs) (fs i)
⇒ vproj xs i }}}
We need not fear projection from vnil, for we can dismiss i : Fin zero, as a
harmless fiction. Of course, we could have analysed the arguments the other way
around:
vproj xs i ⇐ rec xs {
vproj xs i ⇐ case i {
vproj xs fz ⇐ case xs {
vproj (vcons x xs) fz ⇒ x }
vproj xs (fs i ) ⇐ case xs {
vproj (vcons x xs) (fs i) ⇒ vproj xs i }}}
Here, inspecting i forces n to be non-zero in each case, so xs can only be a vcons.
The same result is achieved either way, but in both definitions, we rely on the
impact the first case analysis has on the possibilities for the second. It may seem
a tautology that dependent case analyses are not independent, but its impact is
profound. We should certainly ask whether the traditional case expression, only
expressing the patterns of its scrutinee, is as appropriate as it was in the past.
Our unification tables give some intuition to what is happening with case anal-
ysis. In Thierry Coquand’s presentation of dependent pattern matching [13],
Epigram: Practical Programming with Dependent Types 149
constructor case analysis is hard-wired and unification is built into the typing
rules. In Epigram, we have the more generic notion of refining a programming
problem by an eliminator, ⇐ e. If we take a closer look at the elaboration of this
construct, we’ll see how unification arises and is handled inside the type theory.
I’ll maintain both the general case and the vtail example side by side.
As we saw with plus, when we say8
Γ ys : Vec (suc m) Y
let let
fΓ : R vtail ys : Vec m Y
p⇐e
f vtail ys ⇐ case ys
We call P the motive—it says what we gain from the elimination. In particular,
we’ll have a proof of P for the t. The mi are the methods by which the motive
is to be achieved for each si . James McKinna taught me to choose this motive:
P ; λΘ ⇒ P ; λn; X ; xs ⇒
∀Δ ⇒ Θ=t → ∀m : Nat; Y : ; ys : Vec (suc m) Y
f p : T ⇒ n=(suc m) → X =Y → xs=ys →
vtailm Y ys : Vec m Y
This is just Henry Ford’s old joke. Our motive is to produce a proof of
∀Δ ⇒ f p : T , for ‘any Θ we like as long as it’s t ’—the t are the only Θ we
keep in stock. For our example, that means ‘any vector you like as long as it’s
8
I write Greek capitals for sequences of variables with type assignments in binders
and also for their unannotated counterparts as argument sequences.
150 C. McBride
s : S ; t : T
s=t : refl : t =t
Above, the types of xs and ys are different, but they will unify if we can solve
the prior equations. Hypothetical equations don’t change the internal rules by
which the typechecker compares types—this is lucky, as hypotheses can lie.
If we can construct the methods, mi , then we’re done:
♦fsub ; λΔ ⇒ e P m1 . . . mn ♦vtail ; λm; Y ; ys ⇒ (case ys) P m1 m2
Δ refl . . . refl m Y ys refl refl refl
: ∀Δ ⇒ f p : T : ∀m : Nat; Y : ; ys : Vec (suc m) Y
⇒ vtailm Y ys : Vec m Y
m1 : ∀ X ; m; Y ; ys : Vec (suc m) Y ⇒
zero=(suc m) → X =Y → vnil=ys →
vtailm Y ys : Vec m Y
m2 : ∀ X ; n; x ; xs : Vec n X ; m; Y ; xs : Vec (suc m) Y ⇒
(suc n)=(suc m) → X =Y → (vcons x xs)=ys →
vtailm Y ys : Vec m Y
Look at the equations! They express exactly the unification problems for case
analysis which we tabulated informally. Now to solve them: the rules of first-order
unification for data constructors—see figure 1—are derivable in UTT. Each rule
(read backwards) simplifies a problem with an equational hypothesis. We apply
these simplifications to the method types. The conflict and cycle rules dispose
of ‘impossible case’ subproblems. Meanwhile, the substitution rule instantiates
pattern variables. In general, the equations si =t will be reduced as far as possible
by first-order unification, and either the subproblem will be dismissed, or it will
yield some substitution, instantiating the patterns p.
In our example, the vnil case goes by conflict, and the vcons case becomes:
After ‘cosmetic renaming’ gives x and xs names more like the original ys, we get
9
Also known as ‘John Major’ equality [23].
Epigram: Practical Programming with Dependent Types 151
deletion P →
x =x → P
conflict chalk s =cheese t → P
injectivity (s =t → P) →
chalk s =chalk t → P
substitution Pt → x , t : T ; x ∈ FV (t )
x =t → P x
cycle x =t → P x constructor-guarded in t
I’m not quite sure what to write as the length of the returned vector, so I’ve left
a shed: perhaps it needs some kind of predecessor function with a precondition.
If we have ys : Vec (suc m) Y , then preVtail ys refl will be well typed. We could
even use this function with a more informative conditional expression:
Now that we’ve seen how dependent case analysis is elaborated, let’s do some
more work with it. The next example shows a key difference between Epigram’s
implicit syntax and parametric polymorphism. The operation
let x : X
vec x : Vec n X
makes a vector of copies of its argument. For any given usage of vec, the intended
type determined the length, but how are we to define vec? We shall need to work
by recursion on the intended length, hence we shall need to make this explicit
at definition time. The following declaration achieves this:
n : Nat ; x : X
let ; vecn x ⇐ rec n {
vecn x : Vec n X
vecn x ⇐ case n {
veczero x ⇒ vnil
vec(suc n) x ⇒ vcons x (vecn x ) }}
Note that in vec’s type signature, I explicitly declare n first, thus making it the
first implicit argument: otherwise, X might happen to come first. By the way,
we don’t have to override the argument in the recursive call vecn x —it’s got
to be a Vec n X —but it would perhaps be a little disconcerting to omit the n,
especially as it’s the key to vec’s structural recursion.
Epigram: Practical Programming with Dependent Types 153
va fs ss ⇐ rec fs {
va fs ss ⇐ case fs {
va vnil ss ⇐ case ss {
va vnil vnil ⇒ vnil }
va (vcons f fs) ss ⇐ case ss {
va (vcons f fs) (vcons s ss) ⇒ vcons (f s) (va fs ss) }}}
Exercise 9 (zero, identity). How would you compute the zero matrix of a
given size? Also implement a function to compute any identity matrix.
Exercise 10 (matrix by vector). Implement matrix-times-vector multiplica-
tion. (ie, interpret a Matrix m n as a linear map Vec n Nat → Vec m Nat.)
Exercise 11 (matrix by matrix). Implement matrix-times-matrix multipli-
cation. (ie, implement composition of linear maps.)
Exercise 12 (monad). (Mainly for Haskellers.) It turns out that for each n,
Vec n is a monad, with vec playing the part of return. What should the corre-
sponding notion of join do? What plays the part of ap?
154 C. McBride
You should find that fmax and fweak partition the finite sets, just as fz and fs
do. Imagine how we might pretend they’re an alternative set of constructors. . .
Note that vtab and vproj offer alternative definitions of matrix operations.
4 Representing Syntax
The Fin family can represent de Bruijn indices in nameless expressions [14]. As
Françoise Bellegarde and James Hook observed in [6], and Richard Bird and
Ross Paterson were able to implement in [9], you can do this in Haskell, up to a
point—here are the λ-terms with free variables given by v:
Under a Lda, we use (Maybe v) as the variable set for the body, with Nothing
being the new free variable and Just embedding the old free variables. Renaming
is just fmap, and substitution is just the monadic ‘bind’ operator >>=.
Epigram: Practical Programming with Dependent Types 155
However, Term is a bit too polymorphic. We can’t see the finiteness of the
variable context over which a term is constructed. In Epigram, we can take the
number of free variables to be a number n, and choose variables from Fin n.
data n : Nat
Tm n :
i : Fin n f , s : Tm n t : Tm (suc n)
where ; ;
var i : Tm n app f s : Tm n lda t : Tm n
The n in Term n indicates the number of variables available for term formation:
we can explain how to λ-lift a term, by abstracting over all the available variables:
let t : Tm n ; ldaLiftn t ⇐ rec n {
ldaLiftn t : Tm zero
ldaLiftn t ⇐ case n {
ldaLiftzero t ⇒ t
ldaLift(suc n) t ⇒ ldaLiftn (lda t) }}
Not so long ago, we were quite excited about the power of non-uniform datatypes
to capture useful structural invariants. Scoped de Bruijn terms gave a good
example, but most of the others proved more awkward even than the ‘fake’
dependent types you can cook up using type classes [24].
Real dependent types achieve more with less fuss. This is mainly due to the
flexibility of inductive families. For example, if you wanted to add ‘weakening’ to
delay explicitly the shifting of a term as you push it under a binder—in Epigram,
but not Haskell or Cayenne, you could add the constructor
t : Tm n
weak t : Tm (suc n)
Exercise 17 (ren). Use wren to help you implement the renaming traversal
ρ : Fin m → Fin n ; t : Tm m
let
ren ρ t : Tm n
We’ve seen untyped λ-calculus: let’s look at how to enforce stronger invariants, by
representing a typed λ-calculus. Recall the rules of the simply typed λ-calculus:
Γ; x ∈σ t ∈ τ Γ f ∈ σ⊃τ Γ s ∈ σ
Γ ; x ∈ σ; Γ x ∈ σ Γ λx ∈ σ. t ∈ σ ⊃ τ Γ f s∈τ
Well-typed terms are defined with respect to a context and a type. Let’s just
turn the rules into data! I add a base type, to make things more concrete.
data where ; σ, τ : SType
SType : sNat : SType sFun σ τ : SType
We could use Vec for contexts, but I prefer contexts which grow on the right.
data n : Nat where
SCtxt n : empty : SCtxt zero
Γ : SCtxt n ; σ : SType
bind Γ σ : SCtxt (suc n)
This is a precise definition of the simply-typed λ-terms. But is it any good? Well,
just try writing programs with it.
How would you implement renaming? As before, we could represent a re-
naming as a function ρ : Fin m → Fin n. Can we rename a term in STm Γ τ to
get a STm Δ τ , where Γ : SCtxt m and Δ : SCtxt n? Here comes the crunch:
· · · renn Γ Δ ρ (svar i ) ⇒ svar (ρ i )
The problem is that svar i : STm Γ (sproj Γ i ), so we want a STm Δ (sproj Γ i)
on the right, but we’ve got a STm Δ (sproj Δ (ρ i )). We need to know that ρ is
type-preserving! Our choice of variable representation prevents us from building
this into the type of ρ. We are forced to state an extra condition:
∀i : Fin m ⇒ sproj Γ i =sproj Δ (ρ i )
We’ll need to repair our program by rewriting with this proof.11 But it’s worse
than that! When we move under a slda, we’ll lift the renaming, so we’ll need a
different property:
∀i : Fin (suc m) ⇒ sproj (bind Γ σ) i =sproj (bind Δ σ) (lift ρ i )
This follows from the previous property, but it takes a little effort. My program
has just filled up with ghastly theorem-proving. Don’t dependent types make life
a nightmare? Stop the world I want to get off!
If you’re not afraid of hard work, you can carry on and make this program
work. I think discretion is the better part of valour—let’s solve the problem in-
stead. We’re working with typed terms but untyped variables, and our function
which gives types to variables does not connect the variable clearly to the con-
text. For all we know, sproj always returns sNat! No wonder we need ‘logical
superstructure’ to recover the information we’ve thrown away.
This family strongly resembles Fin. Its constructors target only nonempty con-
texts; it has one constructor which references the ‘newest’ variable; the other
constructor embeds the ‘older’ variables. You may also recognize this family as
an inductive definition of context membership. Being a variable means being a
member of the context. Fin just gives a data representation for variables without
their meaning. Now we can replace our awkward svar with
i : SVar Γ τ
svar i : STm Γ τ
Bad design makes for hard work, whether you’re making can-openers, doing
mathematics or writing programs. It’s often tempting to imagine that once we’ve
made our representation of data tight enough to rule out meaningless values, our
job is done and things should just work out. This experience teaches us that more
is required—we should use types to give meaningful values their meaning. Fin
contains the right data, but SVar actually explains it.
This program does not, of itself, make sense. The best we can say is that
we can make sense of it, provided typeOf has been correctly implemented.
The machine looks at the types but does not see when they are the same,
hence the unsafeCoerce. The significance of the test is obscure, so blind obe-
dience is necessary. Of course, I trust them, but I think they could aspire for
better.
The trouble is that representing the result of a computation is not enough:
you need to know the meaning of the computation if you want to justify its
consequences. A Boolean is a bit uninformative. To see when we look, we need
a new way of looking. Take the vector indexing example. We can explain which
number is represented by a given i : Fin n by forgetting its bound:
let i : Fin n ; fFin i ⇐ rec i {
fFin i : Nat
fFin i ⇐ case i {
fFin fz ⇒ zero
fFin (fs i) ⇒ suc (fFin i ) }}
mayProjn xs m ⇐ checkBound n m {
mayProjn xs (fFin i ) ⇒ just (vproj xs i )
mayProjn xs (plus n m ) ⇒ nothing }
160 C. McBride
Constructor case analysis is the normal way of seeing things. Suppose I have
a funny way of seeing things. We know that ⇐ doesn’t care—a ‘way of seeing
things’ is expressed by a type and interpreted as a way of decomposing a pro-
gramming problem into zero or more subproblems. But how do I establish that
my funny way of seeing at things makes sense?
Given n, m : Nat, we want to see m as either (fFin i ) for some i : Fin n, or
else some (plus n m ). We can write a predicate which characterizes the n and
m for which this is possible—it’s possible for the very patterns we want.12
data n, m : Nat
BoundCheck n m :
where i : Fin n
inBound i : BoundCheck n (fFin i )
m : Nat
outOfBound m : BoundCheck n (plus n m )
There’s no trouble using the view we’re trying to establish: the recursive call
is structural, but used in an eliminator rather than a return value. This func-
tion works much the way subtraction works. The only difference is that it has a
type which establishes a connection between the output to the function and its
inputs, shown directly in the patterns! We may now take
Exercise 21. Show that fmax and fweak cover Fin by constructing a view.
data n : Nat
RTm n :
i : Fin n f , s : RTm n
where ;
rvar i : RTm n rapp f s : RTm n
σ : SType ; b : RTm (suc n)
rlda σ b : RTm n
Why is τ an explicit argument? Well, the point of writing this forgetful map
is to define a notion of pattern for finite sets which characterizes projection. We
need to see the information which the pattern throws away. Let’s establish the
view—it’s just a more informative vproj, telling us not only the projected thing,
but that it is indeed the projection we wanted.
Γ : SCtxt n ; i : Fin n i : SVar Γ τ
data where
Find Γ i : found τ i : Find Γ (fV τ i)
let
find Γ i : Find Γ i
find Γ i ⇐ rec Γ {
find Γ i ⇐ case i {
find Γ fz ⇐ case Γ {
find (bind Γ σ) fz ⇒ found σ vz }
find Γ (fs i ) ⇐ case Γ {
find (bind Γ σ) (fs i ) ⇐ view (find Γ i ) {
find (bind Γ σ) (fs (fV τ i )) ⇒ found τ (vs i) }}}}
But not every raw term is the forgetful image of a well typed term. We’ll need
data Γ : SCtxt n where · · ·
TError Γ :
Γ : SCtxt n ; e : TError Γ
let
fTError e : RTm n
Exercise 24 (TError, fTError). Fill out the definition of TError and implement
fTError. (This will be easy, once you’ve done the missing exercise. The TErrors
will jump out as we write the typechecker—they pack up the failure cases.)
Let’s start on the typechecking view. First, the checkability relation:
data Γ : SCtxt n : r : RTm n
Check Γ r :
where t : STm Γ τ ; e : TError Γ
good t : Check Γ (fTm τ t) bad e : Check Γ (fTError e)
The story so far: we used find to check variables; we used check recursively
to check the body of an rlda and packed up the successful outcome. Note that
we don’t need to write the types of the good terms—they’re implicit in STm.
164 C. McBride
We also got some way with application: checking the function; checking that
the function inhabits a function space; checking the argument. The only trouble
is that our function expects a σ and we’ve got an α. We need to see if they’re
the same: that’s the missing exercise.
let
compare σ τ : Compare σ τ :
You’ll need to define a representation of STypes which differ from a given σ and
a forgetful map fDiff which forgets this difference.
If we use your compare view, we can see directly that the types match in
one case and mismatch in the other. For the former, we can now return a well
typed application. The latter is definitely wrong.
We’ve done all the good cases, and we’re left with choosing inhabitants of
TError Γ for the bad cases. There’s no reason why you shouldn’t define TError Γ
to make this as easy as possible. Just pack up the information which is lying
around! For the case we’ve just seen, you could have:13
σ : Diff σ ; f : STm Γ (sFun σ τ ) ; s : STm Γ (fDiff σ σ )
mismatchError σ f s : TError Γ
This recipe gives one constructor for each bad case, and you don’t have any choice
about its declaration. There are two basic type errors—the above mismatch and
the application of a non-function. The remaining three bad cases just propagate
failure outwards: you get a type of located errors.
Of course, you’ll need to develop comparable first. To define Diff, just play
the same type-of-diagnostics game. Develop the equality test, much as you would
13
The ? means ‘please infer’—it’s often useful when writing forgetful maps. Why?
Epigram: Practical Programming with Dependent Types 165
with the Boolean version, but using the view recursively in order to see when
the sources and targets of two sFuns are the same. If you need a hint, see [27].
What have we achieved? We’ve written a typechecker which not only returns
some well typed term or error message, but, specifically, the well typed term
or error message which corresponds to its input by fTm or fTError. That
correspondance is directly expressed by a very high level derived form of pattern
matching: not rvar, rapp or rlda, but ‘well typed’ or ‘ill typed’.
Exercise 25. Make an environment whose entries are the constructors for Nat,
together with some kind of iterator. Add two and two.
6 Epilogue
Well, we’ve learned to add two and two. It’s true that Epigram is currently little
more than a toy, but must it necessarily remain so? There is much work to do.
I hope I have shown that precise data structures can manipulated successfully
and in a highly articulate manner. You don’t have to be content with giving
orders to the computer and keeping your ideas to yourself. What has become
practical is a notion of program as effective explanation, rather than merely an
effective procedure. Upon what does this practicality depend?
We need a library, but it’s not enough to import the standard presentation
of standard functionality. Our library must support the idioms of dependently
typed programming, which may well be different. Standardizing too early might
be a mistake: we need to explore the design space for standard equipment.
But the greatest potential for change is in the tools of program development.
Here, we have barely started. Refinement-style editing is great when you have a
plan, but often we don’t. We need to develop refactoring technology for Epigram,
so that we can sharpen our definitions as we learn from experiments. It’s seldom
straight away that we happen upon exactly the indexed data structure we need.
Moreover, we need editing facilities that reflect the idioms of programming.
Many data structures have a rationale behind them—they are intended to relate
to other data structures in particular ways and support particular operations. At
the moment we write none of this down. The well typed terms are supposed to be
a more carefully indexed version of the raw terms—we should have been able to
construct them explicitly as such. If only we could express our design principles
then we could follow them deliberately. Currently, we engineer coincidences,
dreaming up datatypes and operations as if from thin air.
But isn’t this just wishful thinking? I claim not. Dependent types, seen
through the Curry-Howard lens, can characterize types and programs in a way
which editing technology can exploit. We’ve already seen one class of logical prin-
ciple reified as a programming operation—the ⇐ construct. We’ve been applying
reasoning to the construction of programs on paper for years. We now have what
we need to do the same effectively on a computer: a high-level programming lan-
guage in which reasons and programs not merely coexist but coincide.
Acknowledgements
I’d like to thank the editors, Tarmo Uustalu and Varmo Vene, and the anonymous
referees, for their patience and guidance. I’d also like to thank my colleagues
and friends, especially James McKinna, Thorsten Altenkirch, Zhaohui Luo, Paul
Callaghan, Randy Pollack, Peter Hancock, Edwin Brady, James Chapman and
Peter Morris. Sebastian Hanowski and Wouter Swierstra deserve special credit
for the feedback they have given me. Finally, to those who were there in Tartu,
thank you for making the experience one I shall always value.
This work was supported by EPSRC grants GR/R72259 and EP/C512022.
References
1. Andreas Abel and Thorsten Altenkirch. A predicative analysis of structural recur-
sion. Journal of Functional Programming, 2000.
2. Thorsten Altenkirch and Bernhard Reus. Monadic presentations of lambda-terms
using generalized inductive types. In Computer Science Logic 1999, 1999.
3. Lennart Augustsson. Compiling Pattern Matching. In Jean-Pierre Jouannaud,
editor, Functional Programming Languages and Computer Architecture, volume
201 of LNCS, pages 368–381. Springer-Verlag, 1985.
Epigram: Practical Programming with Dependent Types 169
Alberto Pardo
1 Introduction
V. Vene and T. Uustalu (Eds.): AFP 2004, LNCS 3622, pp. 171–209, 2005.
c Springer-Verlag Berlin Heidelberg 2005
172 A. Pardo
The purpose of this paper is to study recursion schemes for programs with
effects, assuming that effects are modelled by monads [2]. Most of the standard
recursion schemes can only deal with purely-functional programs (i.e. effect-free
programs). This means that they fail when we try to use them to represent
programs with effects. Basically, the problem is with the shape of recursion that
such programs possess, which is different from that of purely-functional ones.
This raises the necessity of generalizing the existing recursion schemes to cope
with the patterns of computations of programs with effects.
The compositional style of programming still holds in the context of programs
with effects. This means that we will be interested in eliminating intermediate
data structures generated by the composition of monadic programs, but now
produced as the result of monadic computations. Our strategy will be therefore
the derivation of fusion laws associated with the program schemes for programs
with effects in order to restore deforestation in the presence of effects.
The paper is built on previous work on recursion schemes for programs with
effects [20,25,24]. In contrast to [25,24], where a more abstract style of presenta-
tion based on category theory was followed, in this paper concepts and definitions
are described in a functional programming style, using a Haskell-like notation.
The paper is organized as follows. In Section 2 we review some standard re-
cursion schemes and their associated fusion laws. Section 3 presents background
material on monads. Section 4 is devoted to the analysis of recursion schemes
for programs with effects. We also present examples which illustrate the use of
the program schemes and their laws. In Section 5, we conclude the paper with
a brief description of a program fusion tool which integrates many of the ideas
discussed in the paper.
F id = id F (f ◦ g) = F f ◦ F g
A standard example of a functor is that formed by the List type constructor and
the well-known map function, which applies a function to the elements of a list,
building a new list with the results.
map :: (a → b) → (List a → List b)
map f Nil = Nil
map f (Cons a as) = Cons (f a) (map f as)
We will use functors to capture the structure (or signature) of datatypes. In
this paper we will only consider a restricted class of datatypes, called regular
datatypes. These are datatypes whose declarations contain no function spaces
and have recursive occurrences with the same arguments from left-hand sides.
The functors corresponding to regular datatypes’ signatures will be characterised
by an inductive definition, composed by the following basic functors.
Identity Functor. The identity functor is defined as the identity type construc-
tor and the identity function (on functions):
type I a = a
I :: (a → b) → (I a → I b)
I f =f
Constant Functor. For any type t , we can construct a constant functor de-
fined as the constant type constructor and the constant function that maps any
function to the identity on t :
type t a = t
t :: (a → b) → (t a → t b)
t f = id
Product Functor. The product functor is an example of a bifunctor (a functor
on two arguments). The product type constructor gives the type of pairs as
result. The mapping function takes two functions which are applied to each
component of the input pair.
data a × b = (a, b)
(×) :: (a → c) → (b → d ) → (a × b → c × d )
(f × g) (a, b) = (f a, g b)
The elements of a product can be inspected using the projection functions.
π1 :: a × b → a
π1 (a, b) = a
π2 :: a × b → b
π2 (a, b) = b
174 A. Pardo
The assumption that τ is regular implies that each τi,j is restricted to the fol-
lowing forms: some constant type t (like Int, Char , or even a type variable); a
type constructor D (e.g. List) applied to a type τi,j ; or τ itself.
The derivation of a functor from a datatype declaration then proceeds as
follows:
– pack the arguments of the constructors in tuples; for constant constructors
(i.e. those with no arguments) we place the empty tuple ();
– regard alternatives as sums, replacing | by +; and
– substitute the occurrences of τ by a type variable a in every τi,j .
As a result, we obtain the following type constructor:
F a = σ1,1 × · · · × σ1,k1 + · · · + σn,1 × · · · × σn,kn
where σi,j = τi,j [τ := a]1 . The body of the mapping function F :: (a → b) →
(F a → F b) is similar to that of F a, with the difference that now we substitute
the occurrences of the type variable a by a function f ::a → b, and write identities
in the other positions:
F f = σ 1,1 × · · · × σ 1,k1 + · · · + σ n,1 × · · · × σ n,kn
with
⎧
⎪ f if σi,j = a
⎨
σ i,j = id if σi,j = t, for some type t
⎪
⎩
D σ i,j if σi,j = D σi,j
Example 1.
– For the datatype of natural numbers,
data Nat = Zero | Succ Nat
we can derive a functor N given by
type N a = () + a
N :: (a → b) → (N a → N b)
N f = id + f
As a functorial expression, N = () + I .
– For a datatype of arithmetic expressions:
data Exp = Num Int | Add Exp Exp
we can derive a functor E given by
type E a = Int + Exp × Exp
E :: (a → b) → (E a → E b)
E f = id + f × f
As a functorial expression, E = Int + I × I .
1
By s[t := a] we denote the replacement of every occurrence of t by a in s.
176 A. Pardo
each the inverse of the other, such that inF (outF ) packs the constructors (de-
structors) of the datatype. The type μF contains partial, finite as well as infinite
values. Further details can be found in [1,8].
Example 2.
– In the case of the datatype of natural numbers, the corresponding isomor-
phism is given by the type μN = N at and the functions inN and outN :
inN :: N Nat → Nat
inN = const Zero Succ
outN :: Nat → N Nat
outN Zero = Left ()
outN (Succ n) = Right n
const :: a → b → a
const a b =a
– In the case of the datatype of lists, the corresponding isomorphism is given
by the type μLa = List a and the functions inLa and outLa :
inLa :: La (List a) → List a
inLa = const Nil uncurry Cons
Combining Datatypes and Effects 177
2.2 Fold
fold h -
μF a
6 6
inF h
F μF - F a
F (fold h)
Remark 1. When writing the instances of the program schemes, we will adopt the
following notational convention for algebras: We will write (h1 , . . . , hn ) instead
of h1 · · · hn :: F a → a, such that, hi = v when hi = const v :: () → a, or
hi :: τ1 → · · · → τk → a is the curried version of hi :: τ1 × · · · × τk → a. For
example, given an algebra const v f ::La b → b we will write (e, curry f )::(b, a →
b → b). 2
178 A. Pardo
Example 5.
Lists The list type functor corresponds to the standard map function [3]:
List :: (a → b) → (List a → List b)
List f = fold L (Nil , λa bs → Cons (f a) bs)
that is,
List f Nil = Nil
List f (Cons a as) = Cons (f a) (List f as)
180 A. Pardo
2.4 Unfold
Let us now analyze the dual case. The corresponding pattern of recursion, called
unfold [9,12], captures function definitions by structural corecursion. By corecur-
sion we understand functions whose structure is dictated by that of the values
produced as result. Unfold has a pattern of recursion given by the following
scheme:
unfold g -
a μF
g outF
? ?
F a - F μF
F (unfold g)
Proceeding as with fold, since inF is the inverse of outF , we can write:
unfold :: (a → F a) → a → μF
unfold g = inF ◦ F (unfold g) ◦ g
Combining Datatypes and Effects 181
2.5 Hylomorphism
Now we look at functions given by the composition of a fold with an unfold.
They capture the idea of general recursive functions whose structure is dictated
by that of a virtual data structure.
Given an algebra h :: F b → b and a coalgebra g :: a → F a, a hylomorphism
[18,19,27,23] is a function hylo h g :: a → b defined by
182 A. Pardo
unfold g - fold h -
hylo h g = a μF b (1)
From this definition it is easy to see that fold and unfold are special cases of
hylomorphism.
fold h = hylo h outF unfold g = hylo inF g
Unfold-Hylo Fusion.
σ :: ∀ a . (a → F a) → (a → G a)
⇒
hylo h (σ outF ) ◦ unfold g = hylo h (σ g)
3 Monads
It is well-known that computational effects, such as exceptions, side-effects, or
input/output, can be uniformly modelled in terms of algebraic structures called
monads [21,2]. In functional programming, monads are a powerful mechanism
to structure functional programs that produce effects [31].
A monad is usually presented as a Kleisli triple (m, return, >
>=) composed by
a type constructor m, a polymorphic function return and a polymorphic operator
(>>=) often pronounced bind. The natural way to define a monad in Haskell is
by means of a class.
class Monad m where
return :: a → m a
(>>=) :: m a → (a → m b) → m b
Computations delivering values of type a are regarded as objects of type m a,
and can be understood as terms with remaining computation steps. The bind
operator describes how computations are combined. An expression of the form
m> >= λx → m is read as follows: evaluate computation m, bind the variable
x to the resulting value of this computation, and then continue with the evalu-
ation of computation m . How the effect is passed around is a matter for each
monad. In some cases, we may not be interested in binding the result of the first
computation to be used in the second. This can be performed by an operator
pronounced then,
(>
>) :: Monad m ⇒ m a → m b → m b
m> > m = m >>= λ → m
Formally, to be a monad, the components of the triple must satisfy the following
equations:
m> >= return = m (2)
>= λx → m = m [x := a ]
return a > (3)
>= λx → m ) >
(m > >= λy → m = m >
>= λx → (m >
>= λy → m ) (4)
In (4) x cannot appear free in m . The expression m [x := a ] means the substi-
tution of all free occurrences of x by a in m.
With the introduction of monads the focus of attention is now on functions
of type a → m b, often referred to as monadic functions, which produce an effect
when applied to an argument. Given a monadic function f :: a → m b, we define
f :: m a → m b as f m = m > >= f . Using the same idea it is possible to define
the Kleisli composition of two monadic functions,
Combining Datatypes and Effects 185
(•) :: Monad m ⇒ (b → m c) → (a → m b) → (a → m c)
(f • g) a = g a >
>= f
Now we can assign a meaning to the laws of Kleisli triples. The first two laws
amount to say that return is a left and right identity with respect to Kleisli
composition, whereas the last one expresses that composition is associative. Note
that f • g = f ◦ g.
Associated with every monad we can define also a map function, which applies
a function to the result yielded by a computation, and a lifting operator, which
turns an arbitrary function into a monadic function.
mmap :: Monad m ⇒ (a → b) → (m a → m b)
mmap f m = m >>= λa → return (f a)
() :: (a → b) → (a → m b)
f = return ◦ f
Using the Kleisli triple’s laws it can be easily verified that both mmap and ()
happen to be functorial on functions:
mmap id = id = return
id
mmap (f ◦ g) = mmap f ◦ mmap g ◦ g = f •
f g
Example 10. State-based computations are modelled by the state monad. These
are computations that take an initial state and return a value and a possibly
modified state.
newtype State s a = State (s → (a, s))
instance Monad (State s) where
return x = State (λs → (x , s))
State c >>= f = State (λs → let (a, s ) = c s
State c = f a
in c s )
The bind operator combines two computations in sequence so that the state and
value resulting from the first computation are supplied to the second one.
The state monad has been used as an effective tool for encapsulating ac-
tual imperative features, such as, mutable variables, destructive data structures,
and input/output, while retaining fundamental properties of the language (see
[26,14,13]). The idea is to hide the real state in an abstract data type (based on
the monad) which is equipped with primitive operations that internally access
the real state [31,5,13]. 2
Example 11. The list monad enables us to describe computations that produce
a list of results, which can be used to model a form of nondeterminism.
instance Monad List where
return = wrap
Nil >
>= f = Nil
(Cons a as) >
>= f = f a +
+ (as >
>= f )
This monad can be seen as a generalization of the maybe monad: a computation
of type List a may succeed with several outcomes, or fail by returning no result
at all. 2
out to be essential for this integration, since it permits to focus on the relevant
structure of recursive programs disregarding the specific details of the effects
they produce.
The fusion laws associated with the monadic program schemes are particu-
larly interesting because they encapsulate new cases of deforestation. However,
as we will see later, some of the fusion laws require very strong conditions for
their application, reducing dramatically their possibilities to be considered in
practice. To overcome this problem we will introduce alternative fusion laws,
which, though not so powerful, turn out to be useful in practice.
Two alternative approaches can be adopted to the definition of monadic
program schemes. One of them, to be presented first, is a strictly structural
approach based on a lifting construction. This means to translate to the monadic
universe the constructions that characterize the recursion schemes, as well as
the concepts that take part in them. The other approach, to be presented in
Subsection 4.8, is more pragmatical and turns out to be more useful in practice.
4.1 Lifting
Let us start explaining the notion of lifting. Our goal is to define program schemes
that capture the recursion structure of functions with effects. Consider the pat-
tern of recursion captured by hylomorphism:
hylo -
a b
6
g h
?
F a - F b
F hylo
By lifting we mean that we view each arrow of this diagram as an effect-
producing function (a somehow imperative view). By thinking functionally, we
make the effects explicit, giving rise to the following recursion scheme:
mhylo - mb
a
6
g h (5)
?
m (F a) - m (F b)
mhylo)
(F
f = F a F f- dist F-
F F (m b) m (F b) (6)
combining a pair of computations by first evaluating the first one and then the
second. The other alternative is to evaluate the computations from right-to-left,
dist × (m, m ) = do {b ← m ; a ← m; return (a, b)}
A monad is said to be commutative if both alternatives produce the same result
on the same input. Monads like identity or state reader [31] are commutative.
Examples of noncommutative monads are state and list.
Example 12. Assuming that dist × proceeds from left-to-right, the following are
examples of distributive laws:
dist N :: Monad m ⇒ N (m a) → m (N a)
dist N = λx → case x of
Left () → return (Left ())
Right ma → do a ← ma
return (Right a)
dist La :: Monad m ⇒ L a (m b) → m (L a b)
dist La = λx → case x of
Left () → return (Left ())
Right (a, mb) → do b ← mb
return (Right (a, b))
dist Ba :: Monad m ⇒ B a (m b) → m (B a b)
dist Ba = λx → case x of
Left a → return (Left a)
Right (mb, mb ) → do b ← mb
b ← mb
return (Right (b, b ))
dist Ra :: Monad m ⇒ R a (m b) → m (R a b)
dist Ra = λ(a, mbs) → do bs ← sequence mbs
return (a, bs)
where sequence is a distributive law corresponding to the list type functor:
sequence :: Monad m ⇒ List (m a) → m (List a)
sequence Nil = return Nil
sequence (Cons m ms) = do a ← m
as ← sequence ms
return (Cons a as)
2
can be derived from the definition of dist F :
An inductive definition of F
I f = f (F+ G) f = dist + ◦ (F f + G
f)
t f = return (F× G) f = dist × ◦ (F f × Gf)
FG f = F G
f
Df = fold (mmap in F a ◦ F (f , id ))
Example 13. Assuming that dist × proceeds from left to right, the following are
examples of monadic extensions:
N :: Monad m ⇒ (a → m b) → (N a → m (N b))
N f = λx → case x of
Left () → return (Left ())
Right a → do b ← f a
return (Right b)
a :: Monad m ⇒ (b → m c) → (L a b → m (L a c))
L
a f = λx → case x of
L
Left () → return (Left ())
Right (a, b) → do c ← f b
return (Right (a, c))
a :: Monad m ⇒ (b → m c) → (B a b → m (B a c))
B
a f = λx → case x of
B
Left a → return (Left a)
Right (b, b ) → do c ← f b
c ← f b
return (Right (c, c ))
a :: Monad m ⇒ (b → m c) → (R a b → m (R a c))
R
a f = λ(a, bs) → do cs ← mapM f bs
R
return (a, cs)
of the list type functor:
where mapM is a monadic extension List
mapM :: Monad m ⇒ (a → m b) → (List a → m (List b))
mapM f Nil = return Nil
mapM f (Cons a as) = do b ← f a
bs ← mapM f as
return (Cons b bs)
2
F return = return F (f • g) = F f • F g
As established by Mulry [22], a monadic extension is a lifting iff its associated
distributive law satisfies the following conditions:
where
join :: Monad m ⇒ m (m a) → m a
join m = do {m ← m; m }
Equation (7) ensures the preservation of identities, while (8) makes F distribute
over Kleisli composition.
An interesting case to analyze is that of the product functor.2 It is easy to
verify that (7) is valid for every monad:
dist × ◦ (return × return) = return
For example, assuming that dist × proceeds from left to right, we have that:
(dist × ◦ (return × return)) (a, b)
= { definition dist × }
do {x ← return a; y ← return b; return (x , y)}
= { (3) }
do {y ← return b; return (a, y)}
= { (3) }
return (a, b)
The same holds if dist × is right to left. However, equation (8),
join × join
- (m a, m b)
(m2 a, m2 b)
dist × dist ×
? ?
m (m a, m b) - m (a, b)
dist ×
does not always hold, since it requires the monad to be commutative. To see the
problem, let us calculate the expressions corresponding to each side of the equa-
tion. Again, assume that dist × is left to right. We start with the left-hand side:
(dist × ◦ (join × join)) (m2 , m2 )
= { definition of dist × }
do {a ← join m2 ; b ← join m2 ; return (a, b)}
= { definition of join }
do {a ← do {m ← m2 ; m }; b ← do {m ← m2 ; m }; return (a, b)}
= { (4) }
do {m ← m2 ; a ← m; m ← m2 ; b ← m ; return (a, b)}
Now, the right-hand side:
(dist × • dist × ) (m2 , m2 )
= { definition of dist × and Kleisli composition }
do {(n, n ) ← do {m ← m2 ; m ← m2 ; return (m, m )};
a ← n; b ← n ; return (a, b)}
= { (3) and (4) }
do {m ← m2 ; m ← m2 ; a ← m; b ← m ; return (a, b)}
2
A detailed analysis for all regular functors can be found in [25,24].
192 A. Pardo
Both expressions involve exactly the same computations, but they are executed
in different order. If we were working with the state monad, for example, the
order in which computations are performed is completely relevant both for the
side-effects produced and for the values delivered by the computations.
The failure of (8) for functors containing products makes it necessary to
add the hypothesis of preservation of Kleisli composition in those fusion laws
in which that condition is required. There are some functors involving products
for which (8) holds. These are functors containing product expressions of the
form F = t × I (or symmetric). For example, for that F , the distributive law
dist F :: (t , m a) → m (t , a), given by,
dist F (t , m)
= { inductive definition }
(dist × ◦ (return × id )) (t , m)
= { dist × left-to-right }
do {x ← return t ; a ← m; return (x , a)}
= { (3) }
do {a ← m; return (t , a)}
Monadic fold [7] is a pattern of recursion that captures structural recursive func-
tions with monadic effects. A definition of monadic fold is obtained by instanti-
ating (5) with g = outF :
mfold h - ma
μF
6
o
utF h
?
m (F μF ) - m (F a)
(F (mfold h))
Combining Datatypes and Effects 193
mfold h - ma
μF
6
outF h
?
F μF - m (F a)
F (mfold h))
Therefore,
mfold :: Monad m ⇒ (F a → m a) → μF → m a
mfold h = h • F (mfold h) ◦ outF
Example 14. The following are instances of monadic fold for different datatypes.
We assume a left-to-right product distribution dist × .
Lists
mfold L :: Monad m ⇒ (m b, a → b → m b) → List a → m b
mfold L (h1 , h2 ) = mf L
where
mf L Nil = h1
mf L (Cons a as) = do y ← mf L as
h2 a y
For instance, the function that sums the numbers produced by a list of compu-
tations (performed from right to left),
msum L :: Monad m ⇒ List (m Int) → m Int
msum L Nil = return 0
msum L (Cons m ms) = do {y ← msum L ms; x ← m; return (x + y)}
can be defined as:
msum L = mfold L (return 0, λm y → do {x ← m; return (x + y)})
Leaf-labelled binary trees
mfold B :: Monad m ⇒ (a → m b, b → b → m b) → Btree a → m b
mfold B (h1 , h2 ) = mf B
where
mf B (Leaf a) = h1 a
mf B (Join t t ) = do y ← mf B t
y ← mf B t
h2 y y
For instance, the function that sums the numbers produced by a tree of compu-
tations (performed from left to right),
194 A. Pardo
The following are fusion laws for monadic fold. In all of them it is necessary to
assume that function mmap :: (a → b) → (m a → m b) is strictness-preserving,
in the sense that it maps strict functions to strict functions.
MFold Fusion. If F preserves Kleisli compositions,
f strict ∧ f • h = k • F f ⇒ f • mfold h = mfold k
MFold Pure Fusion.
f strict ∧ mmap f ◦ h = k ◦ F f ⇒ mmap f ◦ mfold h = mfold k
MFold-Fold Fusion.
τ :: ∀ a . (F a → a) → (G a → m a)
⇒
mmap (fold h) ◦ mfold (τ inF ) = mfold (τ h)
We will adopt a similar notational convention as for the case of algebras to
write this kind of functions τ in instances of the program schemes.
Example 15. In Example 14, we showed that msum L can be defined as a monadic
fold. Assuming that mmap is strictness-preserving, we use fusion to show that:
msum L = mmap sum L ◦ lsequence
being lsequence the function that performs a list of computations from right to
left:
lsequence :: Monad m ⇒ List (m a) → m (List a)
lsequence Nil = return Nil
lsequence (Cons m ms) = do as ← lsequence ms
a ←m
return (Cons a as)
We can express lsequence as a monadic fold,
lsequence = mfold L (return Nil ,
λm as → do {a ← m; return (Cons a as)})
such that it is possible to write its monadic algebra as τ (Nil , Cons), where
τ :: (b, a → b → b) → (b, m a → b → m b)
τ (h1 , h2 ) = (return h1 ,
λm b → do {a ← m; return (h2 a b)})
Finally, we calculate
mmap sum L ◦ lsequence
= { definition of sum L and lsequence }
mmap (fold L (0, (+))) ◦ mfold L (τ (Nil , Cons))
= { mfold-fold fusion }
mfold L (τ (0, (+)))
= { definition of τ and msum L }
msum L 2
196 A. Pardo
Now we turn to the analysis of corecursive functions with monadic effects. Like
monadic fold, the definition of monadic unfold can be obtained from (5), now
taking h = in F.
munfold g - m μF
a
6
g
in F
?
m (F a) - m (F μF )
(F (munfold g))
that is,
munfold :: Monad m ⇒ (a → m (F a)) → (a → m μF )
munfold g a = (return ◦ inF ) • F (unfold g) • g
Example 16. We show the definition of monadic unfold for different datatypes.
Again, we assume a left to right product distribution dist × .
Lists
munfold L :: Monad m ⇒ (b → m (L a b)) → (b → m (List a))
munfold L g b = do x ← g b
case x of
Left () → return Nil
Right (a, b ) → do as ← munfold L g b
return (Cons a as)
Leaf-labelled binary trees
munfold B :: Monad m ⇒ (b → m (B a b)) → (b → m (Btree a))
munfold B g b = do x ← g b
case x of
Left a → return (Leaf a)
Right (b1 , b2 ) → do t1 ← munfold B g b1
t2 ← munfold B g b2
return (Join t1 t2 )
Rose trees
munfold R :: Monad m ⇒ (b → m (R a b)) → (b → m (Rose a))
munfold R g b = do (a, bs) ← g b
rs ← mapM (munfold R g) bs
return (Fork a rs) 2
fusion laws for monadic unfold. A F -homomorphism between two monadic coal-
gebras g :: a → m (F a) and g :: b → m (F b) is a function f :: a → m b such
that g • f = F f • g. Homomorphisms between monadic coalgebras are closed
under composition provided F preserves Kleisli compositions.
Like with monadic algebras, we can define a weaker notion of structure-
preserving mapping. A pure homomorphism between two coalgebras g :: a →
m (F a) and g :: b → m (F b) is a function f :: a → b between their carriers such
that g ◦ f = mmap (F f ) ◦ g. Again, a pure homomorphism may be regarded as
a representation changer.
The following are fusion laws for monadic unfold.
MUnfold Fusion. If F preserves Kleisli compositions,
g • f = F f • g ⇒ munfold g • f = munfold g
MUnfold Pure Fusion.
g ◦ f = mmap (F f ) ◦ g ⇒ munfold g ◦ f = munfold g
Unfold-MUnfold Fusion.
σ :: ∀ a . (a → F a) → (a → m (G a))
⇒
munfold (σ outF ) ◦ unfold g = munfold (σ g)
A graph traversal is a function that takes a list of roots (entry points to a graph)
and returns a list containing the vertices met along the way. In this subsection
we show that classical graph traversals, such as DFS or BFS, can be formulated
as a monadic unfold.
We assume a representation of graphs that provides a function adj which
returns the adjacency list for each vertex.
type Graph v = ...
adj :: Eq v ⇒ Graph v → v → List v
In a graph traversal vertices are visited at most once. Hence, it is necessary to
maintain a set where to keep track of vertices already visited in order to avoid
repeats. Let us assume an abstract data type of finite sets over a, with operations
emptyS :: Set a
insS :: Eq a ⇒ a → Set a → Set a
memS :: Eq a ⇒ a → Set a → Bool
where emptyS denotes the empty set, insS is set insertion and memS is a mem-
bership predicate.
We handle the set of visited nodes in a state monad. A standard technique
to do so is to encapsulate the set operations in an abstract data type based on
the monad [31]:
198 A. Pardo
runMS :: M a b → b
runMS (State f ) = π1 (f emptyS )
insMS :: Eq a ⇒ a → M a ()
insMS a = State (λs → ((), insS a s))
memMS :: Eq a ⇒ a → M a Bool
memMS a = State (λs → (memS a s, s))
Such a technique makes it possible to consider, if desired, an imperative repre-
sentation of sets, like e.g. a characteristic vector of boolean values, which allows
O(1) time insertions and lookups when implemented by a mutable array. In that
case the monadic abstract data type has to be implemented in terms of the ST
monad [14].
Now, we define graph traversal:
type Policy v = Graph v → v → List v → List v
graphtrav :: Eq v ⇒ Policy v → Graph v → List v → List v
graphtrav pol g = runMS ◦ gtrav pol g
gtrav :: Eq v ⇒ Policy v → Graph v → List v → M v (List v )
gtrav pol g vs = do xs ← mdropS vs
case xs of
Nil → return Nil
Cons v vs → do insMS v
zs ← gtrav pol g (pol g v vs)
return (Cons v zs)
mdropS :: Eq v ⇒ List v → M v (List v )
mdropS Nil = return Nil
mdropS (Cons v vs) = do b ← memMS a
if b then mdropS vs
else return (Cons v vs)
Given an initial list of roots, graphtrav first creates an empty set, then executes
gtrav , obtaining a list of vertices and a set, and finally discards the set and
returns the resulting list. In each iteration, the function gtrav starts with an
exploration of the current list of roots in order to find a vertex that has not been
visited yet. To this end, it removes from the front of that list every vertex u that
is marked as visited until, either an unvisited vertex is met, or the end of the
list is reached. This task is performed by the function mdropS .
After the application of mdropS , we visit the vertex at the head of the input
list, if still there is any, and mark it (by inserting it in the set). A new ‘state’ of the
list of roots is also computed. This is performed by an auxiliary function, called
pol , which encapsulates the administration policy used for the list of pending
Combining Datatypes and Effects 199
Example 17. The following are instances of monadic hylomorphism for specific
datatypes. Again, we assume a left to right product distribution dist × .
Lists
mhylo L :: Monad m ⇒
(m c, a → c → m c) → (b → m (L a b)) → (b → m c)
mhylo L (h1 , h2 ) g = mh L
where
mh L b = do x ← g b
200 A. Pardo
case x of
Left () → h1
Right (a, b ) → do c ← mh L b
h2 a c
Leaf-labelled binary trees
mhylo B :: Monad m ⇒
(a → m c, c → c → m c) → (b → m (B a b)) → (b → c)
mhylo B (h1 , h2 ) g = mh B
where
mh B b = do x ← g b
case x of
Left a → h1 a
Right (b1 , b2 ) → do c1 ← mh B b1
c2 ← mh B b2
h2 c1 c2
Rose trees
mhylo R :: Monad m ⇒
(a → [c ] → m c) → (b → m (R a b)) → (b → m c)
mhyloh h g b = do (a, bs) ← g b
cs ← mapM (mhylo R h g) bs
h a cs 2
The fusion laws for monadic hylomorphism are a consequence of those for
monadic fold and monadic unfold.
MHylo Fusion. If F preserves Kleisli compositions,
τ :: ∀ a . (F a → a) → (G a → m a)
⇒
mmap (fold h) ◦ mhylo (τ inF ) g = mhylo (τ h) g
Combining Datatypes and Effects 201
Unfold-MHylo Fusion.
σ :: ∀ a . (a → F a) → (a → m (G a))
⇒
mhylo h (σ outF ) ◦ unfold g = mhylo h (σ g)
Pruning. Pruning traverses the forest in depth-first order, discarding all sub-
trees whose roots have occurred previously. Analogous to graph traversals, prun-
ing needs to maintain a set (of marks) to keep track of the already visited nodes.
This suggest the use of the same monadic abstract data type.
In the pruning process we will use a datatype of rose trees extended with an
empty tree constructor.
data ERose a = ENull | EFork a (List (ERose a))
Clearly, both clean and collect are folds, clean = fold ER cl and collect =
fold L coll , for suitable algebras cl and coll = (coll1 , coll2 ), respectively.
Finally, we define the function that prunes a forest of rose trees, returning
the rose trees that remain:
prune :: Eq v ⇒ List (Rose a) → M a (List (Rose a))
prune = mmap fclean ◦ fpruneR
We call gp (for generate then prune) the resulting fold. Inlining, we get the
following recursive definition:
gp :: Eq v ⇒ Graph v → List v → M v (List (Rose v ))
gp g Nil = return Nil
gp g (Cons v vs) = do x ← gpc g v
rs ← gp g vs
return (case x of
Left () → rs
Right r → Cons r rs)
Now, let us analyze function gpc, which expresses how individual trees are gen-
erated, pruned and cleaned in a shot.
204 A. Pardo
= { functor mmap }
runMS ◦ mmap (concat ◦ List preorder ◦ collect ) ◦ mapM (gpc g)
= { map-fold fusion, define pjoin = uncurry (+ +) ◦ (preorder × id ) }
runMS ◦ mmap (fold L (Nil , pjoin) ◦ collect ) ◦ mapM (gpc g)
= { define: τ (see below) }
runMS ◦ mmap (fold L (Nil , pjoin) ◦ fold L (τ (Nil , Cons))) ◦ mapM (gpc g)
= { fold-fold fusion }
runMS ◦ mmap (fold L (τ (Nil , pjoin))) ◦ mapM (gpc g)
= { property: mmap (fold h) ◦ D f = fold (M h ◦ F f id ) }
(gpc g) id )
runMS ◦ fold L (mmap (τ (Nil , pjoin)) ◦ L
The monadic program schemes shown so far were all derived from the lifting
construction presented in Subsection 4.1.
mhylo - mb
a
6
g h
?
m (F a) - m (F b)
mhylo)
(F
206 A. Pardo
However, despite its theoretical elegance, this construction suffers from an im-
portant drawback that hinders the practical use of the program schemes derived
from it. The origin of the problem is the compulsory use of the distributive
law dist F associated with F as unique way of joining the effects produced by
the recursive calls. It is not hard to see that this structural requirement intro-
duces a restriction in the kind of functions that can be formulated in terms of
the monadic program schemes. To see a simple example, consider the following
function that prints the values contained in a leaf-labelled binary tree, with a
’+’ symbol in between.
printTree :: Show a ⇒ Btree a → IO ()
printTree (Leaf a) = putStr (show a)
printTree (Join t t ) = do {printTree t ; putStr "+"; printTree t }
For instance, when applied to the tree Join (Join (Leaf 1) (Leaf 2)) (Leaf 3),
printTree returns an I/O action that, when performed, prints the string "1+2+3"
on the standard output. Since it is a monadic function defined by structural
recursion on the input tree, one could expect that it can be written as a monadic
fold. However, this is impossible. To see why, recall that the definition of monadic
fold for binary trees follows a pattern of recursion of this form:
mf B (Leaf a) = h1 a
mf B (Join t t ) = do {y ← mf B t ; y ← mf B t ; h2 y y }
when a left to right product distribution dist × is assumed. According to this
pattern, in every recursive step the computations returned by the recursive calls
must be performed in sequence, one immediately after the other. This means
that there is no way of interleaving additional computations between the re-
cursive calls, precisely the contrary of what printTree does. This limitation is
a consequence of having fixed the use of a monadic extension F as unique al-
ternative to structure the recursive calls in monadic program schemes. In other
words, the fault is in the lifting construction itself.
This problem can be overcome by introducing a more flexible construction
for the definition of monadic hylomorphism:
mhylo h g - mb
a
6
g h
?
m (F a) - m (F (m b))
mmap (F (mhylo h g))
There are two differences between this definition and the one shown previously.
First, this definition avoids the use of a monadic extension F , and second, the
type of h has changed with respect to the type it had previously. Now, its type is
F (m b) → m b. Therefore, strictly speaking, h is not more a monadic F -algebra,
but an F -algebra with monadic carrier. As a consequence of these modifications,
in the new scheme the computations returned by the recursive calls are not
Combining Datatypes and Effects 207
performed apart in a separate unit any more. Instead, they are provided to the
algebra h, which will specify the order in that these computations are performed,
as well as their possible interleaving with other computations.
It is easy to see that this new version of monadic hylomorphism subsumes
the previous one. In fact, a previous version of monadic hylomorphism (with
monadic algebra h :: F b → m b) can be represented in terms of the new one by
taking h • dist F as algebra, that is, mhylo old h g = mhylo (h • dist F ) g. This
means that the definitions, examples and laws based on the lifting construction
can all be regarded as special cases of the new construction.
Of course, we can derive new definitions of monadic fold and unfold from the
new construction. For monadic unfold, the algebra of the monadic hylomorphism
should only join the effects of the computations returned by the recursive calls,
and build the values of the data structure using the constructors. Therefore,
munfold :: Monad m ⇒ (a → m (F a)) → a → m μF
munfold g = mhylo (in F • dist F ) g
Unfold-MHylo Fusion.
σ :: ∀ a . (a → F a) → (a → m (G a))
⇒
mhylo h (σ outF ) ◦ unfold g = mhylo h (σ g)
data structures from both purely-functional programas and programs with ef-
fects. The system accepts as input standard functional programs written in a
subset of Haskell and translates them into an internal representation in terms of
(monadic) hylomorphism. The tool is based on ideas and algorithms used in the
design of the HYLO system [23]. In addition to the manipulation of programs
with effects, our system extends HYLO with the treatment of some other shapes
of recursion for purely-functional programs.
The following web page contains documentation and versions of the tool:
http://www.fing.edu.uy/inco/proyectos/fusion
References
1 Introduction
In the last decade, Graphical User Interfaces (GUIs) have become the standard
for user interaction. Programming these interfaces can be done without much ef-
fort when the interface is rather static, and for many of these situations excellent
tools are available. However, when there is more dynamic interaction between
interface and application logic, such applications require tedious manual pro-
gramming in any programming language. Programmers need to be skilled in the
use of a large programming toolkit.
The goal of the Graphical Editor project is to obtain a concise programming
toolkit that is abstract, compositional, and type-directed. Abstraction is required
to reduce the size of the toolkit, compositionality reduces the effort of putting
together (or altering) GUI code, and type-directed automatic creation of GUIs
allows the programmer to focus on the data model. In contrast to visual pro-
gramming environments, programming toolkits can provide ultimate flexibility,
V. Vene and T. Uustalu (Eds.): AFP 2004, LNCS 3622, pp. 210–244, 2005.
c Springer-Verlag Berlin Heidelberg 2005
GEC: A Toolkit for Generic Rapid Prototyping 211
type safety, and dynamic behavior within a single framework. We use a pure
functional programming language (Clean [22]) because functional programming
languages have proven to be very suitable for creating abstraction layers on top
of each other. Additionally, they have strong support for type definitions and
type safety.
Our programming toolkit utilizes the Graphical Editor Component (GEC) [6]
as universal building block for constructing GUIs. A GECt is a graphical editor
for values of any monomorphic first-order type t. This type-directed creation
of GECs has been obtained by generic programming techniques [9,16,15]. With
generic programming one defines a family of functions that depend on the struc-
ture of types. The GEC toolkit project is to our knowledge the first project in
which generic programming techniques are used for the creation of GUI applica-
tions. It is not the purpose of these notes to explain the inner workings of the
GEC building blocks. The reader is referred to [6] for that. Instead we focus on
the use of these building blocks and on how the toolkit is built using the basic
blocks.
The basic first order GEC building blocks from [6] have been extended in
two ways, such that we can construct higher-order value editors [8]. The first
extension uses run-time dynamic typing [1,21], which allows us to include them
in the GEC toolkit, but this does not allow type-directed GUI creation. It does,
however, enable the toolkit to use polymorphic higher-order functions and data
types. The second extension uses compile-time static typing, in order to gain
monomorphic higher-order type-directed GUI creation of abstract types. It uses
the abstraction mechanism of the GEC toolkit [7].
Apart from putting all the earlier published work together in a single context,
focusing on the use of the toolkit and explaining the extensions using the basic
building blocks, these notes also introduce a library for composing GECs which
is based on the arrows [17] concept. Furthermore, these notes contain exercises
at the end of each section to encourage the reader to get familiar with the treated
GEC concepts.
These notes are structured as follows. Section 2 contains an overview of the
basic first-order GEC toolkit. In Sect. 3 it is explained how GECs can be com-
posed to form larger applications both using GECs directly as well as using a new
arrows library. The GEC-abstraction for model-view programming is treated in
Sect. 4. Extensions for working with higher order types, dynamically and stati-
cally are covered in Sect. 5. Related work is discussed in Sect. 6 and conclusions
are summarized in Sect. 7.
A note on the implementation and the examples in this paper. The project
has been realized in Clean. Familiarity with Haskell is assumed, relevant dif-
ferences between Haskell and Clean are explained in footnotes. The GUI code is
mapped to Object I/O [4], which is Clean’s library for GUIs. Given sufficient sup-
port for dynamic types, the results of this project can be transferred to Generic
Haskell [19], using the Haskell [20] port of Object I/O [3]. The complete code
212 P. Achten et al.
1
This function is equivalent with Haskell main::IO ().
2
MDI selects Object I/O’s Multiple Document Interface.
3
Void is equivalent with Haskell ().
4
Clean separates the types of function arguments by whitespace, instead of →.
5
In a function type, | introduces all overloading class restrictions.
6
Use the generic instance of kind of gGEC.
GEC: A Toolkit for Generic Rapid Prototyping 213
Fig. 1. Generated editor for the standard list type, initially with value [1]
:: GECFunction t env
:==8 (GECDef t env) env → (GECInterface t env,env)
The (GECDef t env) consists of three elements. The first is a string that
identifies the top-level Object I/O element (window or dialog) in which the
editor must be created. The second is the initial value of type t of the editor.
The third is a callback function of type t → env → env. This callback function
tells the editor which parts of the program need to be informed of user actions.
The editor uses this function to respond to changes to the value of the editor by
the application user.
:: GECDef t env :== (String, t,CallBackFunction t env)
:: CallBackFunction t env :== t env → env
The (GECInterface t env) is a record that contains all methods of the newly
created GECt .
:: GECInterface t env = { gecGetValue :: GecGet t env
, gecSetValue :: GecSet t env
}9
:: GecGet t env :== env → (t,env)
:: GecSet t env :== Update t env → env
:: Update = YesUpdate | NoUpdate
The gecGetValue method returns the current value, and gecSetValue sets
the current value of the associated GECt object. The gecSetValue method has
an argument of type Update indicating whether or not the call-back function has
to be called propagating the change of the value through the system.
In Fig. 2 the basic use of the function gGEC is illustrated by showing the corre-
sponding GUI for several alternative definitions of myEditor (as in the example
7
generic f t :: T (t) introduces a generic function f with type scheme T (t).
8
:== introduces a type synonym.
9
A record type with fields fi of types ti is denoted as {fi :: ti }.
214 P. Achten et al.
myEditor2
= generateEditor (”Integer” ,0)
myEditor3
= generateEditor (”String” ,”Hello World!”)
myEditor4
= generateEditor (”Tuple of Integer and String” ,(0 ,”Hello World!”))
myEditor5
= generateEditor
(”Tree” ,Node Leaf 1 Leaf)
:: Tree a
= Node (Tree a) a (Tree a) | Leaf
derive gGEC Tree
above). This generates an editor for the argument data type. All you have to
specify is the name of the window and an initial value. On the right the editor
is shown.
For standard types a version of gGEC is derived automatically. For user-defined
types it is required that a version of gGEC is explicitly derived for the given type.
For the type Tree this is explicitly done in the example. In the rest of these notes
these derives are not shown.
Programs can consist of several editors. Editors can communicate with each
other by tying together the various gecSetValue and gecGetValue methods. In
Sect. 3.2 it is shown how an arrow combinator library [5] can be used for the
necessary plumbing. In this section we use the function selfGEC (explained in
Sect. 3.1) to create ‘self-correcting’ editors:
selfGEC ::
String (t → t) t (PSt ps) → (PSt ps) | gGEC{||} t & bimap{||}10 ps
Given function f of type t → t on the data model of type t and an initial
value v of type t, selfGEC gui f v creates the associated GECt using gGEC
(hence the context restriction). selfGEC creates a feedback loop that sends every
edited output value back as input to the same editor, after applying f.
An example of the use of selfGEC is given by the following program that
creates an editor for a self-balancing binary tree:
10
The generic gGEC function requires an instantiation of the predefined generic function
bimap.
GEC: A Toolkit for Generic Rapid Prototyping 215
fromTreeToList Leaf = [ ]
fromTreeToList (Node l x r)
= fromTreeToList l ++12 [x:fromTreeToList r] 13
Note that the only things that need to be specified by the programmer are
the initial value of the desired type, and the feedback function.
Fig. 4. The default (left) and customized (right) editor of the counter example
of type Counter to the screenshot shown at the right in Fig. 4. Because it has
been explained in detail in [6], we will not repeat the code, but point out the
important points:
For the creation of GUI applications, we need to model both specific GUI
elements (such as buttons) and layout control (such as horizontal, vertical lay-
out). In a way similar to the one shown above for the spin button, this has also
been done by specializing gGEC for a number of other types that either represent
GUI elements or layout. Below the predefined specialized editors are shown for a
number of types. The specialized editor for Display creates a non-editable GUI;
for Button a button is created; for <|> and <-> two editors are created below
each other, respectively next to each other; and finally Hide creates no GUI at
all which is useful for remembering state.
For large data structures it may be infeasible to display the complete data
structure. Customization can be used to define a GECt that creates a view on a
finite subset of such a large data structure with buttons to browse through the
rest of the data structure. This same technique can also be used to create GECs
for lazy infinite data structures. For these infinite data structures customization
is a must since clearly they can never be fully displayed.
Exercise 1. A single address GEC. Write a GEC for editing a single record
containing standard data base data such as name, address and location.
:: Display a = Display a
:: <|> a b = a <|> b
:: <-> a b = a <-> b
:: Hide a = Hide a
3 Composition of GECs
In this section we present a number of examples to show how GECs can be
combined using the callback mechanism and method invocation (Sect. 3.1). In
Sect. 3.2 we show how these examples can be expressed using arrow combinators.
15
The #-notation of Clean has a special scope rule such that the same variable name can
be used for subsequent non-recursive #-definitions. For mutually recursive definitions
(as in apply2GECs) a standard where-definition has to be used with a different name
for each variable.
218 P. Achten et al.
Of course, the same can be done for binary functions with slightly more effort:
apply2GECs :: (String,String,String) (a → b → c) a b (PSt ps)
→ (PSt ps)
| gGEC{||} a & gGEC{||} b & gGEC{||} c & bimap{||} ps
apply2GECs (sa,sb,sc) f va vb env = env3
where
(gec_c,env1) = gGEC{||} (sc,Display (f va vb) ,const id) env
(gec_b,env2) = gGEC{||} (sb,vb,combine gec_a gec_c (flip f)) env1
(gec_a,env3) = gGEC{||} (sa,va,combine gec_b gec_c f) env2
myEditor
= apply2GECs (”List1” ,”List2” ,”Balanced Tree”) makeBalTree [ 1 ] [ 1 ]
where
makeBalTree l1 l2 = fromListToBalTree (l1 ++ l2)
Mutually Dependent GECs. In a similar way one can define mutually de-
pendent GECs. Take the following definition of mutualGEC.
mutualGEC :: (String,String) a (a → b) (b → a) (PSt ps) → (PSt ps)
| gGEC{||} a & gGEC{||} b & bimap{||} ps
mutualGEC (gui1,gui2) va a2b b2a env = env2
where (gec_b,env1) = gGEC{||} (gui1, a2b va, set gec_a b2a) env
(gec_a,env2) = gGEC{||} (gui2, va, set gec_b a2b) env1
This function displays two GECs. It is given an initial value va of type a,
a function a2b :: a → b, and a function b2a :: b → a. The gec_a initially dis-
plays va, while gec_b initially displays a2b va. Each time one of the GECs is
changed, the other is updated automatically. The order in which changes are
made is irrelevant. For example, the application mutualGEC (“Euros”,“Pounds”)
220 P. Achten et al.
exchangerate = 1.4
{euros = 3.5} toPounds toEuros results in an editor that calculates the ex-
change between pounds and euros (see Fig. 8) and vice versa.
The example of Fig. 8 may look a bit like a tiny spreadsheet, but it is es-
sentially different since standard spreadsheets do not allow mutual dependencies
between cells. Notice also the separation of concerns: the way GECs are coupled
is defined completely separate from the actual functionality.
When circuits are combined this will yield a double connection (one forward
set and one backward get for each circuit). It is essential to realize that usage of
the set method is restricted to the circuit that produces that input, and, likewise,
usage of the get method is restricted to the circuit that needs that output.
Moreover, a GEC-to-be-combined of type GecCircuit a b needs to know
where to send its output to, and where to obtain its input from. More precisely,
it is only completely defined if it is provided with a corresponding set method (of
type GecSet b env) and a get method (of type GecGet a env). These methods
correspond exactly with the ‘missing’ methods in Fig. 9. Put in other words, a
GecCircuit a b behaves as a function. Indeed, the way we obtain the restricted
communication is by passing continuation functions. Through these continua-
tions values are passed and set throughout the circuit. Each GecCircuit a b
is a function that takes two continuations as arguments (one for the input and
one for the output) and produces two continuations. The way a circuit takes its
continuation arguments, creates a circuit and produces new continuations, can
be visualized with the internal view of a circuit (see Fig. 10).
A GecCircuit is not only a continuation pair transformation function but it
also transforms an Object I/O environment since it has to be able to incorporate
the environment functions for the creation of graphical editor components. These
environment functions are of type (PSt ps) → (PSt ps).
The global idea sketched above motivates the following full definition of the
GecCircuit a b type:
:: GecCircuit a b
= GecCircuit (∀ ps:
(GecSet b (PSt ps) ,GecGet a (PSt ps) ,PSt ps)
→ (GecSet a (PSt ps) ,GecGet b (PSt ps) ,PSt ps))
The circuits do not depend on the program state ps. This is expressed ele-
gantly using a rank-2 polymorphic function type.
A GecCircuit a b generalizes GECs by accepting input values of type a
and produces output values of type b. Clearly, for every GECa there exists a
GecCircuit a a. This relation is expressed concisely with the function edit:
edit :: String → GecCircuit a a | gGEC{||} a
222 P. Achten et al.
We will provide an instantiation of the standard arrow class for our GEC
arrows of type GecCircuit. This standard arrow class definition is given below.
It describes the basic combinators >>> (serial composition), arr (function lifting),
and first (saving values across computations). The other definitions below can
all be derived in the standard way from these basic arrow combinators. They
are repeated here because we use them in our examples.
class Arrow arr where
arr :: (a → b) → arr a b
(>>>) :: (arr a b) → (arr b c) → arr a c
first :: (arr a b) → arr (a,c) (b,c)
returnA :: arr a a
returnA = arr id
Functionally Dependent GECs. The first arrow example (of which the ex-
ternal view is given in Fig. 11) implements applyGECs of Sect. 3.1.
GEC: A Toolkit for Generic Rapid Prototyping 223
Fig. 12. apply2GECs using arrows creating a balanced tree from two lists, external view
myEditor
= startCircuit (applyGECs (”List” ,”Balanced Tree”)
(Display o fromListToBalTree)) [1 ,5 ,2]
Fig. 13. selfGEC using arrows, self balancing a tree, external view
Self-correcting GECs. The example below shows the arrow combinator ver-
sion of the selfGEC example (see its external view in Fig. 13).
myEditor = startCircuit selfGEC Leaf
designEditor = feedback (
toDesignEditor >>>
edit ”design” >>>
arr (updateDesign o fromDesignEditor))
:: MyDataModel
= { d_value1 :: Int , d_value2 :: Int , d_sum :: Int }
Fig. 14. The code that is carved in stone for the running sum-example of this section
:: MyViewModel
= { v_value1 :: Int , v_value2 :: Int , v_sum :: Int }
counter to see variations and all editable fields as counter fields with one of the
fields itself calculated as a sum of other fields.
In the following sections we show step by step how this can be accomplished.
First, in Sect. 4.1 we show how to construct self-contained editors that take
care of their own update and conversion behavior. In Sect. 4.2 we turn these
self-contained editors into abstract reusable editors, thus encapsulating all infor-
mation about their implementation and behaviour. We show how this is possible
GEC: A Toolkit for Generic Rapid Prototyping 227
The concrete behavior of the generated ViewGEC editor now not only depends
on the type, but also on the concrete information stored in a value of type
ViewGEC. A self-contained reusable editor, such as counterGEC below, is now
quickly constructed. The corresponding editor takes care of the conversions and
the update. The displayGEC does a trivial update (identity) and also takes care
of the required conversions.
counterGEC :: Int → ViewGEC Int Counter
counterGEC i = mkViewGEC i toCounter updCntr fromCounter
the type of the editor v that is being used. Put in other words, the type still
reveals information about the implementation of editor v. This is undesirable for
two reasons: one can not exchange views without changing types, and the type
of composite views reflects their composite structure. For these reasons, a type
is required that abstracts from the concrete editor type v.
However, if we manage to hide these types, how can the generic mechanism
generate the editor for it? The generic mechanism can only generate an editor for
a given concrete type, not for an abstract type of which the content is unknown.
The solution is as follows. When the abstraction is being made, we do know
the contents and its type. Hence, we can store the generic editor function (of
type GECFunction, see Sect. 2) in the abstract data structure itself where the
abstraction is being made. The stored editor function can be applied later when
we really need to construct the editor. Therefore, it is possible to define an
abstract data structure (AGEC d) in which the ViewGEC d v is stored with its
corresponding generic gGEC function for v. Technically this requires a type system
that supports existentially quantified types as well as rank-2 polymorphism.
:: AGEC d
= ∃v: AGEC (ViewGEC d v) (∀ps: GECFunction (ViewGEC d v) (PSt ps))
still requires a d-editor ( | gGEC{||} d). This is caused by the fact that in this
particular case type d is used in the definition of type Display.
counterAGEC :: Int → AGEC Int
counterAGEC i = mkAGEC (counterGEC i)
toMyViewModel1 d
= { v_value1 = idAGEC d.d_value1
, v_value2 = idAGEC d.d_value2
, v_sum = displayAGEC d.d_sum }
toMyViewModel2 d
= { v_value1 = idAGEC d.d_value1
, v_value2 = counterAGEC d.d_value2
, v_sum = displayAGEC d.d_sum }
toMyViewModel3 d
= { v_value1 = counterAGEC d.d_value1
, v_value2 = sumAGEC d.d_value2
, v_sum = displayAGEC d.d_sum }
Fig. 17. Plug-and-play your favorite abstract editors. The only code that changes is
the function toMyViewModel. The values have been edited by the user.
5 Higher-Order GECs
there is one further argument, an Int, and of course a field for the result. How-
ever, if the first argument would also be the twice function then an extra argu-
ment would be required!
The number of argument fields depend on the type of the functional value
that is typed in by the user (or of the actual arguments that are given to it by
the user). These examples of Fig. 18 are created in the sections below.
Because functions are opaque, the solution requires a means of interpreting
functional expressions as functional values. Instead of writing our own parser/in-
terpreter/type inference system we use the Esther shell [24]. Esther enables the
user to enter expressions (using a subset of Clean) that are dynamically typed,
and transformed to values and functions using compiled code. It is also possible
to reuse earlier created functions, which are stored on disk. Its implementation
relies on the dynamic type system [1,21,25] of Clean.
The shell uses a text-based interface, and hence it makes sense to create a
special string-editor (Sect. 5.1), which converts any string into the corresponding
dynamically typed value. This special editor has the same power as the Esther
command interpreter and can deliver any dynamic value, including higher-order
polymorphic functions. In addition we show that the actual content of a dy-
namic value can be influenced by the very same generic mechanism, using type
dependent functions (Sect. 5.2). With this mechanism, dynamics can be used in
a type-directed way, but only for monomorphic types in dynamics.
a value of the type that is bound to the type variable b by unification, wrapped
in a dynamic. If the match fails, it yields a string in a dynamic.
Type variables in type patterns can also relate to type variables in the static
type of a function. A ^ behind a variable in a pattern associates it with the same
type variable in the static type of the function.
matchDynamic :: Dynamic → t | TC t
matchDynamic (x :: t^) = x
The static type variable t, in the example above, is determined by the static
context in which it is used, and imposes a restriction on the actual type that is
accepted at run-time by matchDynamic. The function becomes overloaded in the
predefined TC (type code) class. This makes it a type dependent function [21].
The dynamic run-time system of Clean supports writing dynamics to disk and
reading them back again, possibly in another program or during another execu-
tion of the same program. This provides a means of type safe communication,
the ability to use compiled plug-ins in a type safe way, and a rudimentary basis
for mobile code. The dynamic is read in lazily after a successful run-time unifi-
cation. The amount of data and code that the dynamic linker links is, therefore,
determined by the evaluation of the value inside the dynamic.
writeDynamic :: String Dynamic env → (Bool,env) | FileSystem env
readDynamic :: String env → (Bool,Dynamic,env) | FileSystem env
Programs, stored as dynamics, have Clean types and can be regarded as a
typed file system. We have shown that dynamicApply can be used to type check
any function application at run-time using the static types stored in dynamics.
Combining both in an interactive ‘read expression – apply dynamics – evaluate
and show result’ loop, already gives a simple shell that supports the type checked
run-time application of programs to documents. The composeDynamic function
below, taken from the Esther shell, applies dynamics and infers the type of an
expression.
composeDynamic :: String env → (Dynamic,env) | FileSystem env
showValueDynamic :: Dynamic → String
composeDynamic expr env parses expr. Unbound identifiers in expr are re-
solved by reading them from the file system. In addition, overloading is resolved.
Using the parse tree of expr and the resolved identifiers, the dynamicApply func-
tion is used to construct the (functional) value v and its type τ . These are packed
in a dynamic v :: τ and returned by composeDynamic. In other words, if env
expr :: τ and [[expr]]env = v then composeDynamic expr env = (v :: τ , env). The
showValueDynamic function yields a string representation of the value inside a
dynamic.
Creating a GEC for the Type Dynamic. With the composeDynamic func-
tion, an editor for dynamics can be constructed. This function needs an appro-
priate environment to access the dynamic values and functions (plug-ins) that
are stored on disk. The standard (PSt ps) environment used by the generic
236 P. Achten et al.
gGEC function (Sect. 2) is such an environment. This means that we can use
composeDynamic in a specialized editor to offer the same functionality as the
command line interpreter. Instead of Esther’s console we use a String editor as
interface to the application user. In addition we need to convert the provided
string into the corresponding dynamic. We therefore define a composite data
type DynString and a specialized gGEC-editor for this type (a GECDynString ) that
performs the required conversions.
:: DynString = DynStr Dynamic String
The choice of the composite data type is motivated mainly by simplicity
and convenience: the string can be used by the application user for typing in
the expression. It also stores the original user input, which cannot be extracted
from the dynamic when it contains a function.
Now we specialize gGEC for this type DynString. The complete definition of
gGEC{|DynString|} is given below.
gGEC{|DynString|} (gui,DynStr _ expr,dynStringUpd) env
# (stringGEC,env) = gGEC{||} (gui,expr,stringUpd dynStringUpd) env
= ({ gecSetValue = dynSetValue stringGEC.gecSetValue
, gecGetValue = dynGetValue stringGEC.gecGetValue } ,env)
where dynSetValue stringSetValue (DynStr _ expr) env
= stringSetValue expr env
dynGetValue stringGetValue env
# (nexpr,env) = stringGetValue env
# (ndyn, env) = composeDynamic nexpr env
= (DynStr ndyn nexpr,env)
stringUpd dynStringUpd nexpr env
# (ndyn,env) = composeDynamic nexpr env
= dynStringUpd (DynStr ndyn nexpr) env
The created GECDynString displays a box for entering a string by calling the
standard generic gGEC{||} function for the value expr of type String, yield-
ing a stringGEC. The DynString-editor is completely defined in terms of this
String-editor. It only has to take care of the conversions between a String
and a DynString. This means that its gecSetValue method dynSetValue sets
the string component of a new DynString in the underlying String-editor. Its
gecGetValue method dynGetValue retrieves the string from the String-editor,
converts it to the corresponding Dynamic by applying composeDynamic, and com-
bines these two values in a DynString-value. When a new string is created by
the application user, this will call the callback function stringUpd. It invokes
the callback function dynStringUpd (provided as an argument upon creation of
the DynString-editor), after converting the String to a DynString.
It is convenient to define a constructor function mkDynStr that converts any
input expr, that has value v of type τ , into a value of type DynString guaran-
teeing that if v :: τ and [[expr]] = v, then (DynStr (v::τ ) expr) :: DynString.
mkDynStr :: a → DynString | TC a
mkDynStr x = let dx = dynamic x in DynStr dx (showValueDynamic dx)
GEC: A Toolkit for Generic Rapid Prototyping 237
18
x =:e binds x to e.
238 P. Achten et al.
the programmer a second solution for higher-order types that is statically typed,
which allows, therefore, type-directed generic GUI creation.
The dynamicAGEC is typically used when expression editors are preferred over
value editors of a type, and when application users need to be able to enter
functions of a statically fixed monomorphic type.
One can create an editor for any higher-order type t, even if it contains poly-
morphic functions. It is required that all higher-order parts of t are abstracted,
by wrapping them with an AGEC type. Basically, this means that each part of t
of the form a → b must be changed into AGEC (a → b). For the resulting type t’
an edit dialog can be automatically created, e.g., by applying selfGEC. However,
the initial value that is passed to selfGEC must be monomorphic, as usual for
any instantiation of a generic function. Therefore, editors for polymorphic types
cannot be created automatically using this statically typed generic technique.
As explained in Sect. 5.1 polymorphic types can be handled with dynamic type
checking.
GEC: A Toolkit for Generic Rapid Prototyping 241
Summarizing, we have shown two ways to create editors that can deal with
higher order types. Firstly, one can create dynamically typed higher-order edi-
tors, which have the advantages that we can deal with polymorphic higher order
types and overloading. This has the disadvantage that the programmer has to
check type safety in the editor. Secondly, we have treated a method in which the
compiler can ensure type correctness of higher-order types in statically typed
editors, but then the resulting editors can only edit monomorphic types.
6 Related Work
GUI Programming Toolkits: From the abstract nature of the GEC toolkit it
is clear that we need to look at GUI toolkits that also offer a high level of abstrac-
tion. Most GUI toolkits are concerned with the low level management of widgets
in an imperative style. One well-known example of an abstract, compositional
GUI toolkit based on a combinator library is Fudgets [12]. These combinators
are required for plumbing when building complex GUI structures from simpler
ones. In our system far less plumbing is needed. Most work is done automat-
ically by the generic function gGEC. The only plumbing needed in our system
is for combining the GEC-editors themselves. Any application will only have a
very limited number of GEC-editors. Furthermore, the Fudget system does not
provide support for editing function values or expressions.
A GECt is a t-stateful object, hence it makes sense to look at object oriented
approaches. The power of abstraction and composition in our functional frame-
work is similar to mixins [13] in object oriented languages. One can imagine an
OO GUI library based on compositional and abstract mixins in order to obtain
a similar toolkit. Still, such a system lacks higher-order data structures.
242 P. Achten et al.
7 Conclusions
We have presented the GEC toolkit for rapid prototyping of type safe interactive
applications. The toolkit
1. produces type-safe interactive applications composed from Graphical E ditor
C omponents;
2. is highly automatic due to generic generative programming techniques;
3. can be used for first order and higher order types;
4. can be customized to create any kind of user interface;
5. allows abstraction using model-view programming to hide details and allow
type-safe view changes;
6. is compositional on various levels:
Types standard composition of types lead to composition of corresponding
graphical editor components;
Expressions the user can enter expressions in which values and functions
can be defined/used compositionally; these functions can even be com-
piled functions (possibly taken from complete applications) that are read
from disk, linked in dynamically and applied in a compositional way;
GECs GECs can be composed in an ad-hoc way by standard functional
programming or in a structured way using arrow combinators;
AGECs AGECs can be composed in a statically type safe way.
7. enables the programmer to focus on a data type representing the interaction
with the user instead of on the many nasty details of a graphical toolkit;
8. can be downloaded from http://www.cs.ru.nl/∼clean/gec.
Acknowledgements
The authors would like to thank the referees for their detailed comments.
References
1. M. Abadi, L. Cardelli, B. Pierce, G. Plotkin, and D. Rèmy. Dynamic typing in
polymorphic languages. In Proc. of the ACM SIGPLAN Workshop on ML and its
Applications, San Francisco, June 1992.
GEC: A Toolkit for Generic Rapid Prototyping 243
1 Introduction
Functional programming languages like Haskell [1] and Clean [2,3] offer a very
flexible and powerful static type system. Compact, reusable, and readable pro-
grams can be written in these languages while the static type system is able
to detect many programming errors at compile time. However, this works only
within a single application.
Independently developed applications often need to communicate with each
other. One would like the communication of objects to take place in a type
safe manner as well. And not only simple objects, but objects of any type,
including functions. In practice, this is not easy to realize: the compile time type
information is generally not available to the compiled executable at run-time. In
V. Vene and T. Uustalu (Eds.): AFP 2004, LNCS 3622, pp. 245–272, 2005.
c Springer-Verlag Berlin Heidelberg 2005
246 R. Plasmeijer and A. van Weelden
real life therefore, applications often only communicate simple data types like
streams of characters, ASCII text, or use some ad-hoc defined (binary) format.
Programming languages, especially pure and lazy functional languages like
Clean and Haskell, provide good support for abstraction (e.g., subroutines, over-
loading, polymorphic functions), composition (e.g., application, higher-order
functions, module system), and verification (e.g., strong type checking and in-
ference). In contrast, command line languages used by operating system shells
usually have little support for abstraction, composition, and especially verifica-
tion. They do not provide higher-order subroutines, complex data structures,
type inference, or type checking before evaluation. Given their limited set of
types and their specific area of application (in which they have been, and still
are, very successfull), this is not experienced as a serious problem.
Nonetheless, we think that command line languages can benefit from some
of the programming language facilities, as this will increase their flexibility,
reusability, and security. We have previously done research on reducing run-
time errors (e.g., memory access violations, type errors) in operating systems by
implementing a micro kernel in Clean that provides type safe communication of
any value of any type between functional processes, called Famke (F unctionAl
M icro K ernel E xperiment) [4]. This has shown that (moderate) use of dynamic
typing [5], in combination with Clean’s dynamic run-time system and dynamic
linker [6,7], enables processes to communicate any data (and even code) of any
type in a type safe way.
During the development of a shell/command line interface for our prototype
functional operating system it became clear that a normal shell cannot really
make use (at run-time) of the type information derived by the compiler (at
compile-time). To reduce the possibility of run-time errors during execution of
scripts or command lines, we need a shell that supports abstraction and verifi-
cation (i.e., type checking) in the same way as the Clean compiler does. In order
to do this, we need a better integration of compile-time (i.e., static typing) and
run-time (i.e., interactivity) concepts.
Both the shell and micro kernel are built on top of Clean’s hybrid static/dy-
namic type system and its dynamic I/O run-time support. It allows programmers
to save any Clean expression, i.e., a graph that can contain data, references to
functions, and closures, to disk. Clean expressions can be written to disk as
a dynamic, which contains a representation of their (polymorphic) static type,
while preserving sharing. Clean programs can load dynamics from disk and use
run-time type pattern matching to reintegrate it into the statically typed pro-
gram. In this way, new functionality (e.g., plug-ins) can be added to a running
program in a type safe way. This paper stresses type safety and assumes that
we can trust the compiler.
The shell is called Esther (E xtensible S hell with T ype cH ecking
E xpeRiment), and is capable of:
– reading an expression from the console, using Clean’s syntax for a basic,
but complete, functional language. It offers application, lambda abstraction,
recursive let, pattern matching, function definitions, and even overloading;
A Functional Shell That Operates on Typed and Compiled Applications 247
1.2 Overview
First, we introduce the static/dynamic hybrid type system and dynamic I/O
of Clean in Sect. 2. The type checking and combining of compile code features
of Esther are directly derived from Clean’s dynamic implementation. In Sect.
3 we give an overview of the expressive power of the shell command language
using tiny examples of commands that can be given. In Sect. 4 we show how
to construct a dynamic for each kind of subexpression such that it has the
correct semantics and type, and how to compose them in a type checked way.
Related work is discussed in Sect. 5 and we conclude and mention future re-
search in Sect. 6. We assume the reader to be familiar with Haskell, and will
indicate syntactic difference with Clean in footnotes. The implementation has
been done in Clean because it has more support for (de)serializing dynamics
than Haskell. Unfortunately, Clean’s dynamic linker, which is required for Es-
ther, has only been implemented for Microsoft Windows. The implementation,
which is reasonably stable but always under development, can be downloaded
from: http://www.cs.ru.nl/∼arjenw.
2 Dynamics in Clean
Clean offers a hybrid type system: in addition to its static type system it also has
a (polymorphic) dynamic type system [5,6,7]. A dynamic in Clean is a value of
static type Dynamic, which contains an expression as well as a representation of
the (static) type of that expression. Dynamics can be formed (i.e., lifted from the
static to the dynamic type system) using the keyworddynamic in combination with
the value and an optional type. The compiler will infer the type if it is omitted1 .
dynamic 42 :: Int2
dynamic map fst :: A3 .a b: [ ( a , b ) ] → [a ]
Function alternatives and case patterns can pattern match on values of type
Dynamic, i.e., bring them from the dynamic back into the static type system.
Such a pattern match consist of a value pattern and a type pattern. In the
example below, matchInt returns Just the value contained inside the dynamic if
it has type Int; and Nothing if it has any other type. The compiler translates a
pattern match on a type into run-time type unification. If the unification fails,
the next alternative is tried, as in a common (value) pattern match.
1
Types containing universally quantified variables are currently not inferred by the
compiler. We will sometimes omit these types for ease of presentation.
2
Numerical denotations are not overloaded in Clean.
3
Clean’s syntax for Haskell’s forall.
A Functional Shell That Operates on Typed and Compiled Applications 249
8
This is a uniqueness attribute, indicating that the world environment is passed
around in a single threaded way. Unique values allow safe destructive updates and
are used for I/O in Clean. The value of type World corresponds to the hidden state
of the IO monad in Haskell.
A Functional Shell That Operates on Typed and Compiled Applications 251
The run-time system has to be able to find both kinds of information when a
dynamic is read in.
contains the result of ‘applying’ (using the dynamicApply function) the dynamic
with the name “function” to the dynamic with the name “value”. The closure
40 + 2 will not be evaluated until the * operator needs it. In this case, because
the ‘dynamic application’ of df to dx is lazy, the closure will not be evaluated
until the value of the dynamic on disk named “result” is needed. Running prog4
tries to match the dynamic dr, from the file named “result”, with the type Int.
After this succeeds, it displays the value by evaluating the expression, which is
semantically equal to let x = 40 + 2 in x * x, yielding 1764.
prog1 world = writeDynamic ”function” (dynamic (*)) world
Like any other shell, our Esther shell enables users to start pre-compiled pro-
grams, provides simple ways to combine multiple programs, e.g., pipelining and
concurrent execution, and supports execution-flow controls, e.g., if-then-else con-
structs. It provides a way to interact with the underlying operating system and
the file system, using a textual command line/console interface.
A special feature of the Esther shell is that it offers a complete typed func-
tional programming language with which programs can be constructed. The
shell type checks a command line before performing any actions. Traditional
shells provide very limited error checking before executing the given command
A Functional Shell That Operates on Typed and Compiled Applications 253
line. This is mainly because the applications mentioned at the command line are
practically untyped because they work on, and produce, streams of characters.
The intended meaning of these streams of characters varies from one program
to the other. The choice to make our shell language typed also has consequences
for the underlying operating system and file system: they should be able to deal
with types as well.
In this section we give a brief overview of the functionality of the Esther shell
and the underlying operating system and file system it relies on.
Normal directory manipulation operations still apply, but one no longer reads
bytes from a file. Instead, one reads whole files (only conceptually, the dynamic
linker reads it lazily), and one can pattern match on the dynamic to check the
type. This removes the need for explicit (de)serialization, as data structures are
stored directly as graphs in dynamics. Serialization, parsing, and printing are
often significant parts of existing software (up to thirty percent), which may
be reduces by providing these operations in the programming language and/or
operating system.
The shell contains no built-in commands. The commands it knows are de-
termined by the files (dynamics) stored on disk. To find a command, the shell
searches its directories in a specific order as defined in its search paths, looking
for a file with that name.
The shell is therefore pretty useless unless a collection of useful dynamics
has been stored. When the system is initialized, a standard file system is created
(see Fig. 2) in a Windows folder. It contains:
– almost all functions from the Clean standard environment10 , such as +, −,
map, and foldr (stored as dynamic on disk);
– common commands to manipulated the file system (mkdir, rmdir, and the
like);
10
Similar to Haskell’s Prelude.
A Functional Shell That Operates on Typed and Compiled Applications 255
[2,3,4,5,6,7,8,9,10,11] :: [Int]
Roughly the following happens. The shell parses the expression. The ex-
pression consists of typical Clean-like syntactical constructs (such as (, ), and
[ .. ] ), constants (such as 1 and 10), and identifiers (such as map and +).
The names map and + are unbound (do not appear in the left hand side
of a let, case, lambda expression, or function definition) in this example, and
the shell therefore assumes that they are names of dynamics on disk. They are
256 R. Plasmeijer and A. van Weelden
read from disk (with help of readDynamic), practically extending its function-
ality with these functions, and inspects the types of the dynamics. It uses the
types of map (let us assume that the file map contains the type that we expect:
(a → b) [a ] → [b ] ), + (for simplicity, let us assume: Int Int → Int) and the
list comprehension (which has type: [Int] ) to type-check the command line. If
this succeeds, which it should given the types above, the shell applies the partial
application of + with the integer one to the list of integers from one to ten,
using the map function. The application of one dynamic to another is done using
the dynamicApply function from Sect. 2, extended with better error reporting.
How this is done exactly, is explained in more detail in Sect. 4. With the help
of the dynamicApply function, the shell constructs a new function that performs
the computation map ((+) 1) [1..10] . This function uses the compiled code of
map, +, and [ .. ] , which is implemented as a generator function called _from_to
in Clean.
Our shell can therefore be regarded as a hybrid interpreter/compiler, where
the command line is interpreted/compiled to a function that is almost as efficient
as the same function written directly in Clean and compiled to native code. If
functions, such as map and +, are used in other commands later on, the dynamic
linker will notice that they are already have been used and linked in, and it will
reuse their code. As a consequence, the shell will react even quicker, because no
dynamic linking is required anymore in such a case. For more details on Clean’s
dynamic linker we refer to Vervoort and Plasmeijer [7].
Expressions. Here are some more examples of expressions that speak for them-
selves. Application:
> map
map :: (a -> b) [a] -> [b]
Expressions that contain type errors:
> 40 + "5"
*** cannot apply + 40 :: Int -> Int
to "5" :: {#Char} ***
> one
one :: a | one a
Function Definitions. One can define new functions at the command line:
> dec x = x - 1
dec :: Int -> Int
This defines a new function with the name dec. This function is written to
disk in a file with the same name (dec) such that from now on it can be used in
other expressions.
Esther parses the example above as: λx → (λx → x). This is not standard,
and may change in the future.
A Functional Shell That Operates on Typed and Compiled Applications 259
Let Expressions. To introduce sharing and to construct both cyclic and infinite
data structures, one can use let expressions.
> let x = 4 * 11 in x + x
88 :: Int
> hd [1..]
1 :: Int
> hd []
*** Pattern mismatch in case ***
Start world
# (ok, world) = writeDynamic ”Node”
(dynamic Node :: A.a: (Tree a) (Tree a) → Tree a) world
# (ok, world) = writeDynamic ”Leaf”
(dynamic Leaf :: A.a: a → Tree a) world
# (ok, world) = writeDynamic ”myTree”
(dynamic Node (Leaf 1) (Leaf 2)) world
= world
These constructors can then be used by the shell to pattern match on a value
of that type.
260 R. Plasmeijer and A. van Weelden
> leftmost tree = case tree of Leaf x -> x; Node l r -> leftmost l
leftmost :: (Tree a) -> a
Typical Shell Commands. Esther’s search path also contains a directory with
common shell commands, such a file system operations:
> mkdir "foo"
UNIT :: UNIT
Esther displays UNIT because mkdir has type World → World, i.e., has a side
effect, but no result. Functions that operate on the Clean’s World state are
applied to the world by Esther.
More operations on the file system:
> cd "foo"
UNIT :: UNIT
> ls ""
"
bar
" :: {#Char}
4.1 Application
Suppose we have a syntax tree for constant values and function applications that
looks like:
:: Expr = (@) infixl 912 Expr Expr //13 Application
| Value Dynamic // Constant or dynamic value from disk
We introduce a function compose, which constructs the dynamic containing a
value with the correct type that, when evaluated, will yield the result of the
given expression.
compose :: Expr → Dynamic
compose (Value d) = d
compose (f @ x) = case (compose f, compose x) of
(f :: a → b , x :: a) → dynamic f x :: b
(df, dx) → raise14 (”Cannot apply ” +++ typeOf df
+++ ” to ” +++ typeOf dx)
12
This defines an infix constructor with priority 9 that is left associative.
13
This a Clean comment to end-of-line, like Haskell’s --.
14
For easier error reporting, we implemented imprecise user-defined exceptions à la
Haskell [8]. We used dynamics to make the set of exceptions extensible.
262 R. Plasmeijer and A. van Weelden
Next, we extend the syntax tree with lambda expressions and variables.
:: Expr = ··· // Previous def.
| (∼>) infixr 0 Expr Expr // Lambda abstraction: λ .. → ..
| Var String // Variable
| S | K | I // Combinators
At first sight, it looks as if we could simply replace a ∼> constructor in the syntax
tree with a dynamic containing a lambda expression in Clean:
compose (Var x ∼> e) = dynamic (λy → composeLambda x y e :: ? )
The problem with this approach is that we have to specify the type of the lambda
expression before the evaluation of composeLambda. Furthermore, composeLambda
will not be evaluated until the lambda expression is applied to an argument. This
problem is unavoidable because we cannot get ‘around’ the lambda. Fortunately,
bracket abstraction [9] solves both problems.
Applications and constant values are composed to dynamics in the usual way.
We translate each lambda expression to a sequence of combinators (S, K, and I)
and applications, with the help of the function ski.
compose ··· // Previous def.
compose (x ∼> e) = compose (ski x e)
compose I = dynamic λx → x
compose K = dynamic λx y → x
compose S = dynamic λf g x → f x (g x)
A Functional Shell That Operates on Typed and Compiled Applications 263
15
If this guard fails, we end up in the last function alternative.
264 R. Plasmeijer and A. van Weelden
Now we are ready to add irrefutable let(rec) expressions. Refutable let(rec) ex-
pressions must be written as cases, which will be introduced in next section.
:: Expr = · · · // Previous def.
| Letrec [Def] Expr // let(rec) .. in ..
| Y // Combinator
16
...until 32. Clean does not support functions or data types with arity above 32.
A Functional Shell That Operates on Typed and Compiled Applications 265
4.6 Overloading
Support for overloaded expressions within dynamics in Clean is not yet im-
plemented (e.g., one cannot write dynamic (==) :: A.a: a a → Bool | Eq a).
Even when a future dynamics implementation supports overloading, it cannot be
used in a way that suits Esther. We want to solve overloading using instances/-
dictionaries from the file system, which may change over time, and which is
something we cannot expect from Clean’s dynamic run-time system out of the
box.
Below is the Clean version of the overloaded functions == and one. We will
use these two functions as a running example.
class Eq a where (==) infix 4 :: a a → Bool
class one a where one :: a
‘original’ type of the expression t, and the type of the name of the overloaded
function o, which also contains the variable the expression is overloaded in.
Values of the type Overloaded consists of a infix constructor ||| followed by the
overloaded expression (of type d → t), and the context restrictions (of type o).
A term Class c of type Context v is used for a single context restriction of the
class c on the type variable v. Multiple context restrictions are combined in a
tree of type Contexts.
:: Overloaded d t o = (|||) infix 1 (d → t) o
:: Contexts a b = (&&&) infix 0 a b
:: Context v = Class String
5 Related Work
We have not yet seen an interpreter or shell that equals Esther’s ability to use
pre-compiled code, and to store expressions as compiled code, which can be used
in other already compiled programs, in a type safe way.
Es [14] is a shell that supports higher-order functions and allows the user
to construct new functions at the command line. A UNIX shell in Haskell [15]
by Jim Mattson is an interactive program that also launches executables, and
provides pipelining and redirections. Tcl [16] is a popular tool to combine pro-
grams, and to provide communications between them. None of these programs
provides a way to read and write typed objects, other than strings, from and to
disk. Therefore, they cannot provide our level of type safety.
A functional interpreter with a file system manipulation library can also
provide functional expressiveness and either static or dynamic type checking of
270 R. Plasmeijer and A. van Weelden
part of the command line. For example, the Scheme Shell (ScSh) [17] integrates
common shell operations with the Scheme language to enable the user to use
the full expressiveness of Scheme at the command line. Interpreters for statically
typed functional languages, such as Hugs [18], even provide static type checking
in advance. Although they do type check source code, they cannot type check
the application of binary executables to documents/data structures because they
work on untyped executables.
The BeanShell [19] is an embeddable Java source interpreter with object
scripting language features, written in Java. It is capable of inferring types for
variables and to combine shell scripts with existing Java programs. While Esther
generates compiled code via dynamics, the BeanShell interpreter is invoked each
time a script is called from a normal Java program.
Run-time code generation in order to specialize code at run-time to certain
parameters is not related to Esther. Esther only combines existing code into new
code, by adding code for function application and combinators in between, using
Clean’s dynamic I/O system.
There are concurrent versions of both Haskell and Clean. Concurrent
Haskell [20] offers lightweight threads in a single UNIX process and provides
M-Vars as the means of communication between threads. Concurrent Clean
[21] is only available on multiprocessor Transputers and on a network of
single-processor Apple Macintosh computers. Concurrent Clean provides sup-
port for native threads on Transputer systems. On a network of Apple com-
puters, it runs the same Clean program on each processor, providing a virtual
multiprocessor system. Concurrent Clean provided lazy graph copying as the
primary communication mechanism. Neither concurrent system can easily pro-
vide type safety between different programs or between multiple incarnations
of a single program.
Both Lin [22] and Cooper and Morrisett [23] have extended Standard ML
with threads (implemented as continuations using call/CC) to form a small func-
tional operating system. Both systems implement the basics needed for a stand-
alone operating system. However, none of them support the type-safe communi-
cation of any value between different computers.
Erlang [24] is a functional language specifically designed for the development
of concurrent processes. It is completely dynamically typed and primarily uses
interpreted byte-code, while Famke is mostly statically typed and executes native
code generated by the Clean compiler. A simple spelling error in a token used
during communication between two processes is often not detected by Erlang’s
dynamic type system, sometimes causing deadlock.
Back et al. [25] built two prototypes of a Java operating system. Although
they show that Java’s extensibility, portable byte code and static/dynamic type
system provides a way to build an operating system where multiple Java pro-
grams can safely run concurrently, Java does not support dynamic type uni-
fication, higher-order functions, and closures in the comfortable way that our
functional approach does.
A Functional Shell That Operates on Typed and Compiled Applications 271
6 Conclusions
We have shown how to build a shell that provides a simple, but powerful strongly
typed functional programming language. We were able to do this using only
Clean’s support for run-time type unification and dynamic linking, albeit syntax
transformations and a few low-level functions were necessary. The shell named
Esther supports type checking and type inference before evaluation. It offers
application, lambda abstraction, recursive let, pattern matching, and function
definitions: the basics of any functional language. Additionally, infix operators
and support for overloading make the shell easy to use.
By combining code from compiled functions/programs, Esther allows the use
of any pre-compiled program as a function in the shell. Because Esther stores
functions/expressions constructed at the command line as a Clean dynamic, it
supports writing compiled programs at the command line. Furthermore, these
expressions written at the command line can be used in any pre-compiled Clean
program. The evaluation of expressions using recombined compiled code is not
as fast as using the Clean compiler. Speed can be improved by introducing fewer
combinators during bracket abstraction, but it seams unfeasible to make Esther
perform the same optimizations as the Clean compiler. In practice, we find Esther
responsive enough, and more optimizations do not appear worth the effort at
this stage. One can always construct a Clean module using the same syntax and
use the compiler to generate a dynamic that contains more efficient code.
References
1. S. Peyton Jones and J. Hughes et al. Report on the programming language Haskell
98. University of Yale, 1999. http://www.haskell.org/definition/
2. M. J. Plasmeijer and M. C. J. D. van Eekelen. Functional Programming and Parallel
Graph Rewriting. Addison Wesley, 1993.
3. R. Plasmeijer and M. van Eekelen. Concurrent Clean Language Report version 2.1.
University of Nijmegen, November 2002. http://cs.kun.nl/∼clean.
4. A. van Weelden and R. Plasmeijer. Towards a Strongly Typed Functional Op-
erating System. In R. Peña and T. Arts, editors, 14th International Workshop
on the Implementation of Functional Languages, IFL’02, pages 215–231. Springer,
September 2002. LNCS 2670.
5. M. Abadi, L. Cardelli, B. Pierce, and G. Plotkin. Dynamic Typing in a Statically
Typed Language. ACM Transactions on Programming Languages and Systems,
13(2):237–268, April 1991.
6. M. Pil. Dynamic Types and Type Dependent Functions. In T. Davie, K. Hammond
and C. Clack, editors, Proceedings of the 10th International Workshop on the Im-
plementation of Functional Languages, volume 1595 of Lecture Notes in Computer
Science, pages 171–188. Springer-Verlag, 1998.
7. M. Vervoort and R. Plasmeijer. Lazy Dynamic Input/Output in the Lazy Func-
tional Language Clean. In R. Peña and T. Arts, editors, 14th International
Workshop on the Implementation of Functional Languages, IFL’02, pages 101–117.
Springer, September 2002. LNCS 2670.
272 R. Plasmeijer and A. van Weelden
Bernard Pope
1 Introduction
V. Vene and T. Uustalu (Eds.): AFP 2004, LNCS 3622, pp. 273–308, 2005.
c Springer-Verlag Berlin Heidelberg 2005
274 B. Pope
this makes the task of debugging hard, in particular it rules out the step-based
debugging style widely employed in imperative languages.
Purely functional languages, along with logic languages, are said to be declar-
ative. The uniting theme of these languages is that they emphasise what a pro-
gram computes rather than how it should do it. Or to put it another way,
declarative programs focus on logic rather than evaluation strategy. The declar-
ative style can be adopted in most languages, however the functional and logic
languages tend to encourage a declarative mode of thinking, and are usually
used most productively in that way. Proponents of declarative programming ar-
gue that the style allows programmers to focus on problem solving, and that
the resulting programs are concise, and easier to reason about than equivalent
imperative implementations. The declarative style allows more freedom in the
way that programs are executed because the logic and evaluation strategy are
decoupled. This means that declarative languages can take advantage of novel
execution mechanisms without adding to the complexity of the source code. The
non-strict semantics of Haskell and backtracking search of Prolog are examples
of this.
Despite the many advantages of declarative programming, there are situa-
tions when the programmer must reason about how a program is evaluated. That
is, the evaluation strategy is occasionally very important. For example, when per-
forming input and output (I/O) the relative order of side-effects is crucial to the
correctness of the program. The inclusion of monads and the statement-based
do notation in Haskell reflect this, and where necessary one may adopt an im-
perative style of programming. Also, efficiency considerations sometimes require
that the programmer can influence the evaluation strategy — for example strict
evaluation may lead to better memory consumption.
Debugging is another task that suffers when the evaluation strategy is un-
known to the programmer. The usual approach to debugging is to step through
the program evaluation one operation at a time. However, to make any sense,
this method requires the programmer to have an accurate mental model of the
evaluation strategy — which is the very thing that the declarative style eschews.
Forming an accurate mental model of lazy evaluation is quite a challenge. Log-
ical relationships, such as “X depends on Y”, which are evident in the source
code, may not be apparent in the reduction order.
The other main factor that complicates debugging in Haskell is the tendency
for programs to make extensive use of higher-order functions. The difficulty stems
from three things: the function type is abstract and more difficult to display than
structured data, the relationship between the static and dynamic call graphs is
more complicated, and the specification of correctness much more demanding.
A very basic facility of debugging tools is to print out values from a running
program. Functions are first-class in Haskell but they have no inherent print-
able representation. One might suppose that showing the name of a function
would suffice, however not all functions in Haskell have a name (i.e. lambda ab-
stractions). Also, there is a tendency to construct new functions dynamically by
partial application and composition. If the user of the debugger is to have any
Declarative Debugging with Buddha 275
hope of understanding what their program is doing, functions must be made ob-
servable, and the way they are shown must be easily related to the user’s mental
model of the program.
Higher-order functions make holes in the static call graph that are only filled
in when the program is executed. Curried functions can pick up their arguments
in a piecemeal fashion, and there may be a lengthy delay between when the
initial application is made and when the function has received enough arguments
for a reduction to take place. Whilst the extra flexibility in the call graph is
good for abstraction and modularity, the effect for debugging is similar to the
problem identified with non-strict evaluation — it is harder to relate the dynamic
behaviour of the program with its static description.
Lastly, fundamental questions like: “Is this function application doing what it
should? ” have less obvious answers in the higher-order context. Understanding
the meaning of a function is often hard enough, but understanding the meaning
of a function that takes another function as its argument, or returns one as its re-
sult, exacerbates the problem. A significant challenge in the design of debugging
systems for Haskell is how to reduce the cognitive load on the user, especially
when there is a large number of higher-order functions involved. This is an issue
that has seen little attention in the design of debugging systems for mainstream
imperative languages because higher-order code is much less prevalent there.
The debugger we describe in this paper, called Buddha, is based on the phi-
losophy that declarative languages deserve declarative debuggers. Or in other
words, an effective way to deal with the problem of non-strict evaluation and
higher-order functions is to aim the debugging tool at the declarative level of
reasoning. The result is a powerful debugging facility that goes far beyond the
capabilities of step-wise debuggers, extending the benefits of the declarative style
from program development to program maintenance.
Overview
In this paper we explain how Buddha is used, how it is implemented and the
overall philosophy of Declarative Debugging. You, the reader, are assumed to
be comfortable with Haskell, or a similar language, though by no means do you
have to be an expert.
A number of exercises are sprinkled throughout the text for you to ponder
over as you read along. In many cases the questions are open ended, and there
may be more than one “right” answer. Some questions might even be research
topics on their own! Of course there is no obligation to answer them all, they
are merely an aid to help you consolidate the material in between.
The rest of the paper goes as follows:
– Section 2: using Buddha on an example buggy program.
– Section 3: summary of the important parts of Buddha’s implementation.
– Section 4: deciding on the correctness of function applications.
– Section 5: controlling Buddha’s resource usage.
– Section 6: pointers to related work.
– Section 7: conclusion.
276 B. Pope
Fig. 1. A program with a bug. It is supposed to compute the digits of 341 as a list
([3,4,1]), but instead it produces [1,10,10]. Line numbers are indicated on the left
hand side.
transformed into a new Haskell program. The transformed code is compiled and
linked with a declarative debugging library, resulting in a program called debug.
When debug is executed it behaves exactly like the debuggee — it accepts the
same command line arguments and performs the same I/O. Where the debuggee
278 B. Pope
For each module X in the program, buddha transforms the code in that module
and stores the result in a file called X_B.hs. To avoid cluttering the working
directory with the new files, buddha does all of its work in a sub-directory called
Buddha, which is created during its initialisation phase. Compilation and linking
are done by the Glasgow Haskell Compiler (GHC).
Buddha introduces itself with the above banner message. Following this is
something called a derivation, and then the prompt (which is underlined):
[1] Main 5 main
result = <IO>
buddha:
A derivation records information about the evaluation of a function applica-
tion or a constant. In the above case, the derivation reports that the constant
main evaluated to an I/O action (which is abstract). Each derivation also in-
dicates in which module the entity was defined, and on what line its definition
begins. In this case main was defined in module Main on line 5. If the entity is
a function application the derivation will also show representations of its argu-
ments. The number inside square brackets is unique to the derivation, and thus
gives it an identity — we’ll see how this is useful in a moment.
The derivations are stored in a tree, one per node, called an Evaluation De-
pendence Tree (EDT).3 It looks just like a call graph. The root always contains
a derivation for main because all Haskell programs begin execution there. De-
bugging is a traversal of this tree, one derivation at a time.
The evaluation of a function application or constant will often depend on
other applications and constants. If you look back at Fig. 1, you will see that
the definition of main depends on a call to print and a call to digits. The
execution of these calls at runtime forms the children of the derivation for main,
and conversely, main is their parent.
An important concept is that of the current derivation. The current deriva-
tion is simply the one that is presently under consideration. In our example
the current derivation is the one for main. If you ever forget what the current
derivation is you can get a reminder by issuing the refresh command.
You can ask Buddha to show you the children of the current derivation by
issuing the kids command:
buddha: kids
To save typing, many of Buddha’s commands can be abbreviated to one letter,
normally the first letter of the long version. For example, kids can be abbreviated
to k. For the remainder of this example we will use the long versions of each com-
mand for clarity, but you will probably want to use the short version in practice.
Buddha responds to kids as follows:
Children of the current derivation:
Surprisingly Buddha says that the derivation for main has only one child,
namely an application of digits. What happened to the derivation for print?
Since print is defined in the Prelude it is trusted to be correct. To reduce the size
of the EDT and hopefully save time spent debugging, Buddha does not record
derivations for trusted functions.
Note that kids does not change the current derivation, it just allows you
to look ahead one level in the EDT. At this point in the example the current
derivation is still main.
Exercise 1. If print was not trusted what would its derivation look like in this
case?
Buddha can help you visualise the shape of the EDT with the draw command:
buddha: draw edt
This generates a graphical representation of the top few levels of the EDT,
using the Dot language [2], and saves it to a file called buddha.dot. You can use
a tool such as dotty 4 to view the graph:
dotty buddha.dot
Figure 2 illustrates the kind of graph produced by the draw command. It is
worth noting that the resulting graph differs from the EDT in that nodes do not
include function arguments or results (or even line numbers etcetera).
It is very difficult to say anything about the correctness of the derivation for
main because its result, an I/O value, is abstract. Therefore, it is a good idea to
consider function applications that do not give I/O results.5
The child of main is suspicious looking, and thus worthy of further scrutiny.
Here’s where the unique number of the derivation becomes useful. You can
change the current derivation using the jump command. The derivation for
digits is numbered 2, and you can jump to it as follows:
buddha: jump 2
Although not needed here, it is worth noting that jumps can be undone with
the back command, which takes you back to the derivation that you jumped
from, and allows you to resume debugging from where you left off.
After making the jump Buddha shows the new current derivation:
[2] Main 8 digits
arg 1 = 341
result = [1, 10, 10]
4
www.research.att.com/sw/tools/graphviz
5
The version of Buddha described in this paper does not support debugging of func-
tions that perform I/O, however future versions should remove this limitation.
Declarative Debugging with Buddha 281
main
digits
Fig. 2. A representation of the function call graph produced by the draw command,
which shows the relationship between calls in the EDT. Note that the dot (.) is Haskell’s
name for function composition.
This derivation says that digits was applied to 341 and it returned the
list [1, 10, 10]. This is definitely wrong — it should be [3, 4, 1]. When
a derivation is found to be wrong you can declare this information by issuing
a judgement. Function applications or constants that do not agree with your
intended interpretation of the program should be judged as erroneous:
buddha: erroneous
Buddha’s objective is to find a buggy derivation. A buggy derivation is one
that is erroneous, and which either has no children, or all of its children are
correct. Such a derivation indicates a function or constant which is improperly
defined — it is the cause of at least one bug in the program.
A nice feature of the debugging algorithm is that, if the EDT is finite (and
it always is for terminating programs), once you find an erroneous node you are
guaranteed to find a buggy node.
Exercise 2. Provide an informal argument for the above proposition. What as-
sumptions (if any) do you have to make, and will they be significant in practice?
We have established that the call to digits is erroneous, now Buddha must
determine if it is buggy. This requires an inspection of its children. In Fig. 2 we
see that the application of digits has seven children. Each child corresponds
to an instance of a function which is mentioned in the body of the definition of
282 B. Pope
digits. You might be surprised to see the EDT structured this way, because
none of the function applications in the body of digits are saturated. In a
language that allows partial application of functions, the evaluation contexts
in which a function is first mentioned and when it is fully applied can be quite
disconnected. For example, the definition of digits refers to prefixes. However,
it is not until prefixes is applied (dynamically) in the body of compose that
it becomes saturated. Therefore it might also be reasonable to make the call to
prefixes a child of a call to compose. The parent-child relationship between
nodes in the EDT is based on the idea of logical evaluation dependence. The
idea is that the correctness of the parent depends, in part, on the correctness of
the child. Higher-order programming tends to muddy the waters, and we can see
that there is some degree of flexibility as to the positioning of calls to functions
which are passed as arguments to other functions. Buddha’s approach is to base
the parent-child relationship on the dependency which is evident in the static
definition of functions. Statically, the definition of digits depends on a reference
to prefixes. Dynamically, a call to digits will give rise to a partial application
of prefixes. All the full applications of this particular instance of prefixes
will become children of the application of digits. The same logic is applied to
all the other functions which are mentioned in the body of digits.
There are two possible scenarios that can lead Buddha to a diagnosis. In the
first scenario all the children of digits are correct. The conclusion in that case
is that digits is buggy. In the second scenario one or more of the children of
digits is incorrect. In that case the erroneous children or one or more of their
descendents are buggy. Buddha could collect a set of buggy nodes as its diagnosis,
but for simplicity it stops as soon as one such node has been identified. The idea
is that you find and fix one bug at a time.
In this example Buddha will move to the first child of digits. If that child is
correct it will move to the next child, and so on. If all the children are found to
be correct the diagnosis is complete. If one of the children is erroneous, Buddha
will recursively consider the EDT under that child in the search for a bug.
Exercise 3. Will the order that the children are visited affect the diagnosis of the
bug? Can you think of any heuristics that might reduce the number of derivations
considered in a debugging session?
The first two arguments of compose are much more complicated than the
examples we have seen before. The complexity comes from two sources. First,
Declarative Debugging with Buddha 283
Exercise 4. Can you think of any other way to show the value of functions
that appear as arguments or results in derivations? What benefits/costs do the
alternatives have in comparison to the one used by Buddha?
We were in the process of checking those children for correctness. In fact we’ve
only looked at one so far, and we found it to be too complicated. Buddha treats
the children as a circular queue. Deferral simply moves the current derivation to
the end of the queue and makes the next derivation the current one. If we keep
deferring we’ll eventually get back to the start again.
buddha: defer
This leads us to two more applications of compose. Again these could be
judged correct, but for the point of demonstration we’ll defer them both:
[4] Main 40 .
arg 1 = { [341, 34, 3] -> [1, 10, 10] }
arg 2 = { [341, 34, 3, 0, ..? -> [341, 34, 3] }
arg 3 = [341, 34, 3, 0, ..?
result = [1, 10, 10]
buddha: defer
[5] Main 40 .
arg 1 = { [10, 10, 1] -> [1, 10, 10] }
arg 2 = { [341, 34, 3] -> [10, 10, 1] }
arg 3 = [341, 34, 3]
result = [1, 10, 10]
buddha: defer
Finally something which is easy to judge:
[6] Main 20 reverse
arg 1 = [10, 10, 1]
result = [1, 10, 10]
Clearly this application of reverse is correct:
buddha: correct
A correct child cannot be held responsible for an error identified in its parent.
Thus there is no need to consider the subtree under the child, so Buddha moves
on to the next of its siblings:
[8] Main 17 lastDigits
arg 1 = [341, 34, 3]
result = [10, 10, 1]
At last we find a child of digits which is erroneous (we expect that the last
digits of [341, 34, 3] to be [1, 4, 3]):
buddha: erroneous
The discovery of this error causes the focus to shift from the children of
digits to the sub-tree which is rooted at the derivation for lastDigits. The
new goal is to decide whether lastDigits or one of its descendents is buggy.
As it happens the derivation of lastDigits has only one child, which is a
call to map:
[9] Main 27 map
arg 1 = { 3 -> 1, 34 -> 10, 341 -> 10 }
arg 2 = [341, 34, 3]
result = [10, 10, 1]
Exercise 7. It would appear from the code in Fig. 1 that lastDigits calls two
functions. However Buddha only gives it one child. What is the other child, and
what happened to it?
Despite the fact that map’s first argument is a function it should be pretty
clear that this application is correct:
buddha: correct
286 B. Pope
Diagnosis. This last judgement leads us to a buggy node, which Buddha indi-
cates as follows:
Found a buggy node:
[8] Main 17 lastDigits
arg 1 = [341, 34, 3]
result = [10, 10, 1]
Here is where the debugging session ends. However we haven’t yet achieved
what we set out to do: find the bug in the program. Buddha has helped us a lot,
but we have to do a little bit of thinking on our own.
Exercise 8. Why did Buddha stop here? Trace through the steps in the diagnosis
that lead it to this point. Are you convinced it has found a bug? What about
those deferred derivations involving compose, is it okay to simply ignore them?
The diagnosis tells us that lastDigits returns the wrong result when ap-
plied to [341, 34, 3]. We also know that every application depended on by
lastDigits to produce this value is correct.
Exercise 9. What is the bug in the program in Fig. 1? Provide a definition of
lastDigits that amends the problem.
Retry. When we get to this point it is tempting to dust our hands, congratulate
ourselves, thank Buddha and move on to something else. However our celebra-
tions may be premature. Buddha only finds one buggy node at a time, but there
may be more lurking in the same tree. A diligent bug finder will re-run the pro-
gram on the same inputs that cause the previous bug, to see whether it has been
resolved, or whether there is more debugging to be done. Of course it is prudent
to test our programs on a large number and wide variety of inputs as well — the
testing suite QuickCheck can be very helpful for this task [4].
3. For each number in the above list, obtain the last digit in the desired base.
For example if the list is ‘[1976, 197, 19, 1]’, the output should be ‘[6,
7, 9, 1]’. This is the job of lastDigit.
4. Convert each (numerical) digit into a character. Following the hexadecimal
convention, numbers above 9 are mapped to a letter in the alphabet. For
example, 10 becomes ’a’, 11 becomes ’b’ and so on. This is the job of
toDigit.
5. Reverse the above list to give the digits in the desired order.
Exercise 10. Your job is to test the program to find example input values that
cause it to return the wrong result. For each set of inputs that give rise to the
Fig. 3. A program for converting numbers in base 10 notation to other bases. The
program has a number of bugs.
288 B. Pope
wrong behaviour, use Buddha to diagnose the cause of the bug. Fix the program,
and repeat the process until you are convinced that the program is bug free. To
get started, try using the program to convert 1976 to base 10. The expected
output is 1976, however program produces :0:.
3 Implementation
In this section we look at how Buddha is implemented. Space constraints neces-
sitate a fair degree of generalisation, and you should treat it as a sketch, rather
than a blueprint.
We begin with a definition of the EDT using Haskell types. Then we look
at a useful abstraction called the Oracle, which plays the part of judge in the
debugger. After the Oracle, we consider a simple bug diagnosis algorithm over
the EDT. Then we discuss the process of turning arbitrary values into textual
forms which are suitable for printing on the terminal. Of particular interest
is the way that functions are handled. Lastly, we compare two transformation
algorithms that introduce code into the debuggee for constructing the EDT.
prefixes 341 => [341, 34, 3, 0 .. ? (.) { [341, 34, 3, 0 .. ? −> [1, 10, 10] }
{ 341 −> [341, 34, 3, 0 .. ? }
341
leadingNonZeros [341, 34, 3, 0 .. ? => [341, 34, 3] => [1, 10, 10]
lastDigits [341, 34, 3] => [10, 10, 1] (.) { [341, 34, 3] −> [1, 10, 10] }
{ [341, 34, 3, 0 .. ? −> [341, 34, 3] }
[341, 34, 3, 0 .. ?
map { 3 −> 1, 34 −> 10, 341 −> 10 }
=> [1, 10, 10]
[341, 34, 3]
=> [10, 10, 1]
reverse [10, 10, 1] => [1, 10, 10] (.) { [10, 10, 1] −> [1, 10, 10] }
{ [341, 34, 3] −> [10, 10, 1] }
[341, 34, 3]
=> [1, 10, 10]
Declarative Debugging with Buddha
Fig. 4. An EDT for the program in Fig. 1. Dotted edges indicate subtrees which have been truncated for brevity.
289
290 B. Pope
data Derivation
= Derivation
{ name :: Identifier
, args :: [Value]
, result :: Value
, location :: SrcLoc }
data EDT
= EDT
{ nodeID :: Int
, derivation :: Derivation
, children :: [EDT] }
of the judge as a person sitting behind a computer terminal. However the role
of judge can be automated to a certain degree.
Buddha delegates the task of judgement to an entity called the Oracle. Cur-
rently the Oracle is a hybrid of software and human input. The diagnosis al-
gorithm passes derivations to the Oracle which returns a judgement. The goal
of the software part is to reduce the number of derivations seen by the user. It
keeps a database that records pairs of derivations and judgements, which is pop-
ulated by prior responses from the user. If a derivation has been seen before, the
corresponding judgement is retrieved from the database. Derivations never seen
before are printed on the terminal and judged by the user, and the judgement is
saved in the database. There is much room for improvement on the software side.
An obvious extension is to allow the database to be saved between debugging
sessions.
Exercise 11. Can you think of other features that might be useful in the software
part of the Oracle?
3.3 Diagnosis
Figure 6 shows a very simple Declarative Debugging algorithm in Haskell.
Exercise 12. Extend the diagnosis algorithm to collect a set of buggy nodes.
The method for obtaining the EDT depends on the underlying implementa-
tion of the debugger. In the above diagnosis algorithm that detail is left abstract.
Declarative Debugging with Buddha 291
Later, in Sec. 3.5, we’ll see two different ways that have been used in Buddha to
produce the tree.
3.4 Observation
Buddha must be able to turn values into text if it is going to print them on the
terminal. An important condition is that it must be able to print any value. In
short, we want a universal printer. Unfortunately there is no standard way of
doing this in Haskell, and GHC does not supply one, so Buddha must provide
its own.7
There are a number of requirements that make the task quite hard:
Exercise 13. In Sec. 2 we saw the partial list [341,34,3,0,..?. Recall that the
tail of the list indicated by ..? is a thunk. Provide a Graph encoding of that
list. You can assume that the numbers are of type Int. The memory addresses
of constructor applications are not important — just make them up.
Graph construction work in C. From the C perspective all values have the same
type (a heap object), so there is no limitation on the type of value that can be
passed down through reify.
Exercise 14. What have we sacrificed by observing values on the heap via the
FFI?
The result of reify has an IO type. This is necessary because multiple appli-
cations of reify to the same value may give back different Graphs. For example,
the presence or absence of thunks and cycles in a value depends on when it is
observed. Buddha ensures that values are observed in their most evaluated form
by delaying all calls to reify until the evaluation of the debuggee has run to
completion — at that point it knows their heap representations will not change.
Cycles. Cyclic values are not uncommon in Haskell. The classic example is the
infinite list of ones:
ones = 1 : ones
The non-strict semantics of Haskell allow programs to operate on this list without
necessarily causing non-termination. It is worth pointing out that the language
definition does not require this list to be implemented as a cyclic structure,
however all of the popular compilers currently do. Here is a Graph representation
of the list, assuming that it lies at address 12:
AppNode 12 ":" [IntNode 1, Cycle 12]
Buddha’s default mode of showing cyclic values is rather naive. It prints this list
as:
[1, <cycle>
This indicates the origin of the cycle, but not its destination. Buddha has another
mode of printing which uses recursive equations to show cycles. You can turn
this mode on with the set command:
buddha: set cycles True
In this mode the list is printed as:
let _x1 = [1, _x1 in _x1
You might wonder why Buddha doesn’t use the second mode by default. Our
experience is that for larger values with cycles it can actually make the output
harder to comprehend! In any case, it is often preferable to view complex struc-
tures as a diagram, which can be done with the draw command. In Sec. 2 we
saw how to use draw to produce a diagram of the EDT, using the Dot graph
language. You can also use this command for printing values that appear as
arguments or results in derivations.
294 B. Pope
Suppose the current derivation contains ones. The following command ren-
ders the result of ones and saves the output in the file buddha.dot:
buddha: draw result
As before, you can view the diagram with the dotty program, the output of
which looks like this:
If you want to draw an argument at position 3, for instance, you can issue the
command:
buddha: draw arg 3
Exercise 15. Why is it possible to record only one argument? How do you think
multi-argument functions are handled?
Functions and their unique number are “wrapped up” inside a new type
called F:
This ensures that the function and the number are always together. Wrappers are
introduced as part of the program transformation, however the unique numbers
are created dynamically.
Wrapped functions are passed to reify in the usual way, resulting in a Graph
value such as:
Obviously the printer does not treat this like an ordinary data structure. The
presence of the F constructor indicates that the Graph represents a function. In
the above example, the function is identified by the number 4. The printer scans
the global table and collects all records that correspond to this function.
For example, suppose the global table contains these records:
[ (1, V True, V False)
, (2, V 12, V 3)
, (1, V False, V True)
, (4, V "ren", V 3)
, (3, V (’a’, ’b’), V ’a’)
, (4, V "stimpy", V 6)
, (2, V 16, V 4)
]
The entries pertaining to function number 4 are underlined. The arguments and
results in the records are Values that must be converted to Graphs before they
can be printed. In this example function number 4 would be printed in the
following way:
{ "ren" -> 3, "stimpy" -> 6 }
The part of the transformation that deals with function wrapping is quite
simple. It consists of three parts. First, wrappers must be introduced where
functional values are created (lambda abstractions and partial applications).
Second, when a wrapped function is applied it must be unwrapped and the
application must be recorded in the global table. Third, function types must be
changed to reflect the wrapping.
Let’s consider a small example using code from Fig. 1. In lastDigits’s body,
map is applied to the expression (10 ‘mod‘). That expression is a function which
must be wrapped up. We introduce a family of wrappers, funn , where n indicates
the arity of the function to be wrapped. The first three of those wrappers have
the following types:
fun1 :: (a -> b) -> F a b
fun2 :: (a -> b -> c) -> F a (F b c)
fun3 :: (a -> b -> c -> d) -> F a (F b (F c d))
Exercise 16. Why do we have multiple wrapping functions? Would one suffice?
In our example, the expression (10 ‘mod‘) has an arity of one, so the defi-
nition of lastDigits is transformed like so:
lastDigits :: Int -> Int
lastDigits = map (fun1 (10 ‘mod‘))
The definition of map must also be transformed:
map :: F a b -> [a] -> [b]
map f [] = []
map f (x:xs) = apply f x : map f xs
296 B. Pope
The first parameter, called f, will be bound to a wrapped function when map
is applied. This requires two changes. First, the type is changed to indicate the
wrapping: (a->b) becomes F a b. Second, where f is applied to x, it must be
unwrapped, and the application must be recorded. The job of unwrapping and
recording is done by apply, which has this definition:
apply :: F a b -> a -> b
apply (F unique f) x
= let result = f x in updateTable unique x result
3.5 Transformation
The purpose of the transformation is to introduce code into the debuggee for
constructing the EDT. In the development of Buddha we have experimented
with two different styles of transformation. The first style builds the tree in a
purely functional way, whilst the second style builds the tree in a table by means
of impure side-effects. The current version of Buddha employs the second style
for a combination of efficiency concerns and programming convenience.
To help with the presentation we’ll consider both styles of transformation
applied to the following code for naive reverse:
rev :: [a] -> [a]
rev xs
= case xs of
[] -> []
y:ys -> append (rev ys) [y]
The Purely Functional Style. In the first style each function definition is
transformed to return a pair containing its original value and an EDT node.
Applications in the body of the function return nodes which make up its children.
Figure 7 shows the result of transforming rev using this style.
rev xs
= case xs of
[] -> let result = []
children = []
node = edt name args result children
in (result, node)
y:ys -> let (v1, t1) = rev ys
(v2, t2) = append v1 [y]
result = v2
children = [t1, t2]
node = edt name args result children
in (result, node)
where
name = "rev"
args = [V xs]
Note the decomposition of the nested function application from the second
branch of the case statement:
before after
append (rev ys) [y] −→ (v1, t1) = rev ys
(v2, t2) = append v1 [y]
The variables v1 and v2 are bound to the original value of the intermediate
applications, and t1 and t2 are bound to EDT nodes.
Figure 8 shows the transformation of append, which follows the same pattern
as rev.
Upon first inspection the effect of the transformation might appear somewhat
daunting. However, it is actually only doing two things: constructing a new EDT
node for the application of the function and, in the process of doing that, collect-
ing its children nodes from the applications that appear in the body. To simplify
things we make use of a helper function called edt which constructs a new EDT
node from the function’s name, arguments, result and children (we’ve skipped the
source location for simplicity). The apparent complexity is largely due to the fact
that the computation of the original value and the EDT node are interwoven.
This style of transformation is quite typical in the literature on Declarative
Debugging. Variants have been proposed by [5], [6], and [7], amongst others.
The main attraction of this style is that it is purely functional. However,
support for higher-order functions is somewhat challenging. The naive version of
the transformation assumes that all function applications result in a value and
298 B. Pope
append xs ys
= case xs of
[] -> let result = ys
children = []
node = edt name args result children
in (result, node)
z:zs -> let (v1, t1) = append zs ys
result = z : v1
children = [t1]
node = edt name args result children
in (result, node)
where
name = "append"
args = [V xs, V ys]
an EDT node, although this is only true for saturated applications. An initial
solution to this problem was proposed in [7] and improved upon in [8].
Exercise 19. What are the types of the transformed versions of rev and append?
Exercise 20. Apply this style of transformation to the definition of map from
Fig. 1? How will you deal with the application of the higher-order argument f
in the body of the second equation?
Notice the new type of rev, in particular its first argument is now an integer
which corresponds to the unique number of its parent. The task of assigning new
unique numbers for each application of rev is performed by the helper function
addNode, whose type is as follows:
addNode :: Int -> String -> [Value] -> (Int -> a) -> a
Each call to addNode does four things:
1. generate a new unique number for the current application;
2. pass that number into the body of the transformed function;
3. record an entry for the application in the global table;
4. return the value of the application as the result.
Writes to the global table are achieved via impure side effects.
The number for each application of rev is passed to the calls in its body
through the variable n, which is introduced by lambda abstraction around the
original function body. The idea is that the new function body — the lambda
abstraction — is applied to each new number for rev inside addNode. Hence the
type of addNode’s fourth argument is (Int -> a), where n takes the value of
the Int and a matches with the type of the original function’s result.
Figure 10 shows the transformation of append, which follows the same pattern
as rev.
You might have noticed some similarity between the use of a global table
in this section to record derivations and the use of a global table to record
applications of higher-order functions in Sec. 3.4. Indeed they share some un-
derlying machinery for performing impure updates of the tables. Just like the
300 B. Pope
function table, you can also see the contents of the derivation table, using the
dump command:
buddha: dump calls
Again, for long running programs the table can get quite large, so be careful
with this command, it can produce a lot of output.
The main advantage of the impure table transformation over the purely func-
tional is that it tends to decouple the generation of the EDT and and evaluation
of the debuggee. This means that we can stop the debuggee at any point and
still have access to the tree produced up to that point. Declarative diagnosis can
often be applied to a partial tree. In the purely functional style this is possible
but more complicated. The separation makes the handling of errors easier. It
doesn’t matter if the debuggee crashes with an exception, the table is still easily
accessible to the debugging code. In the purely functional style the evaluation
of the debuggee and the production of the EDT are interwoven, which makes it
harder to access the EDT if the debuggee crashes.
The main problem with the second transformation style is that it relies on
impure side effects to generate new unique numbers and make updates to the
global table. It is possible that with some clever programming the side effects
could be avoided, but we speculate that this will be at a significant cost to
performance. Without more investigation it is difficult to be definitive on that
point. The problem with the use of impure side effects is that they do not sit
well with the semantics of Haskell. As the old saying goes:
If you lie to the compiler, it will get its revenge.
This is certainly true with GHC, which is a highly optimising compiler. Covert
uses of impure facilities tend to interact very badly with the optimisations that
it performs, and one must program very carefully around them. We could turn
the optimisations off, however that would come at the cost of efficiency in the
debugging executable, which is something we want to avoid.
4 Judgement
The Oracle is assumed to have an internal set of beliefs about the intended
meaning of each function and constant in the program. We call this the In-
tended Interpretation of the program. Derivations in the EDT reveal the actual
behaviour of the program. Judgement is the process of comparing the actual
behaviour of functions and constants with their intended behaviour. Differences
between the two are used to guide the search for buggy nodes.
binary system does not always suit the intuition of the user and we extend it
with two more values: unknown and inadmissible.
Some derivations are just too complicated to be judged. Perhaps they contain
very large values, or lots of higher-order code, or lots of thunks. The best choice
is to defer judgement of these derivations. However, deferral might only postpone
the inevitable need for a judgement. If deferral does not lead to another path to
a bug the Oracle can judge a difficult derivation as unknown. If a buggy node
is found which has one or more unknown children, Buddha will report those
children in its final diagnosis, and remind the user that their correctness was
unknown. The idea is that the true bug may be either due to the buggy node or
one or more of its unknown children, or perhaps one of their descendents.
For some tasks it is either convenient or necessary to write partial functions:
functions which are only defined on a subset of their domain. Most functional
programmers have at some point in their life experienced program crash because
they tried to take the head of an empty list. An important question is how to
judge derivations where the function is actually applied to arguments for which
there is no intended result?
Consider the merge function which takes two sorted lists as arguments and
returns a sorted list as output containing all the values from the input lists.
Due to a bug in some other part of the program it might happen that merge is
given an unsorted list as one of its arguments. In this case Buddha might ask the
Oracle to judge the following derivation:
[32] Main 12 merge
arg 1 = [3,1,2]
arg 2 = [5,6]
result = [3,1,2,5,6]
Let’s assume for the moment that our definition of merge is correct for all sorted
arguments. Is this derivation correct or erroneous? If we judge it to be erroneous
then Buddha will eventually diagnose merge as buggy, assuming that merge only
calls itself. This is a bad diagnosis because the bug is not due to merge, rather
it is due to which ever function provided the invalid argument. To find the
right buggy node we could judge the derivation to be correct. However, it feels
counter-intuitive to say that a derivation is correct when it should never have
happened in the first place. In such circumstances it is more natural to say
that the derivation is inadmissible. It has exactly the same effect as judging the
derivation to be correct, yet it is a more accurate expression of the user’s beliefs.
In the application ‘(&&) False exp’, the value of exp is not needed, so it will
remain as a thunk. It would not be prudent for the debugger to force the evalu-
ation of exp to find its value, first because the computation might be expensive,
and second, in the worst case is might trigger divergence (exp might be non-
terminating or it might raise an exception).
Thunks that remain at the end of the program execution cannot be the cause
of any observed bugs, so it ought to be possible to debug without knowing their
value. Therefore, thunks are treated by Buddha as unknown entities, and it prints
a question mark whenever it encounters one. In the above example, this would
lead to the following derivation:
[19] Main 74 &&
arg 1 = False
arg 2 = ?
result = False
How do you judge derivations that have question marks in them? One approach
is to assume that the Oracle has an intended meaning for functions with all
possible combinations of partial arguments and results. This is convenient for us
as designers of the debugger, but it is not very helpful for the user.
Generally it is easier for the user to model their intended interpretation on
complete values. It tends to simplify the task of saying what a function should
do if you ignore the effects of computation (i.e. strictness and non-termination)
and think in terms of abstract mathematical functions. In this sense you can say
the intended interpretation of && is merely the relation:
(True, True, True), (True, False, False)
(False, True, False), (False, False, False)
A derivation with partial arguments is correct if and only if all possible instan-
tiations of those partial arguments agree with the intended interpretation. The
above derivation for && is correct because, according to the relation above, it
doesn’t matter whether we replace the question mark with True or False, the
result is always False.
Of course && is a very simple example, and many interesting functions are
defined over large or even infinite domains, where it is not feasible to enumerate
all mappings of the function. In such cases the user might have to do some
reasoning before they can make a judgement.
Exercise 22. Judge these derivations (the module names, line numbers and deri-
vation numbers are irrelevant).
[6] Main 55 length
arg 1 = ?
result = 0
[99] Main 55 length
arg 1 = [?]
result = 1
Declarative Debugging with Buddha 303
5 Resource Usage
For all but the briefest runs of a program it is totally infeasible to consider
every function application. In long running programs you will be swamped with
derivations, and most likely all of your available heap space will be consumed.
One way to reduce the number of derivations (and memory usage at the same
time) is to limit the range of functions considered by the debugger. Hopefully
you will have some feeling for where the bug is in your program, and also which
parts are unlikely to be involved. Unit testing can be quite helpful in this regard.
Instead of just testing the program as a whole, one may test smaller pieces of
code separately. A failed unit test gives a much narrower scope for the bug, and
allows any code not touched by the test to be trusted. One may even debug
specialised versions of the program that only execute the top call in a failed test
case, thus avoiding the extra cost of executing large amounts of trusted code.
This idea is discussed in the context of tracing in [9]. Unit tests are supported
in Haskell by QuickCheck [4], and also HUnit8 .
The EDT maintains references to the arguments and results of each function
application. This means that such values cannot be garbage collected as they
might have been in the evaluation of the original program. By not creating nodes
8
http://hunit.sourceforge.net
304 B. Pope
for every function application we allow some values to be garbage collected. Fewer
nodes generally means less memory is needed and less questions will be asked.
How do you reduce the number of nodes in this tree? You can prune it stat-
ically by telling Buddha which functions you trust and which ones you suspect.
For each module in the program you can provide an options file that tells Bud-
dha what to do for each function in the module. If the module’s name is X, the
options file is called called X.opt, and it must be stored in the Buddha directory.
The syntax of the options file is very simple. It has a number of lines and each
line specifies what kind of transformation you want for a given function.
Here’s what you might write for some program:
_ ; Trust
prefixes ; Suspect
convert ; Suspect
Each line contains the name of the function, a semi-colon and then an option
as to what kind of EDT node you want. The underscore matches with anything
(just like in Haskell patterns), so the default is specified on the first line to be
Trust. Any function which is not mentioned specifically gets the default option.
If there is no such default option in the whole file, then the default default is
Suspect. In the case when no option file is present for a module, every function
in the module is transformed with the Suspect option.
What do the options mean?
– Suspect: Create a full node for each application of this function. Such a node
will record the name of the function, its arguments and result, its source
location and will have links to all of its children. However, the children
will be transformed with their own options which will not necessarily be
Suspect. This option tends to make Buddha use a lot of memory, especially
for recursive functions, so please use it sparingly.
– Trust: Don’t create a node for applications of this function, but collect any
children that this function has.
Another way to reduce the size of the EDT is to make use of re-evaluation.
The idea is that only the top few levels of the EDT are produced by the ini-
tial execution of the debuggee. Debugging commences with only a partial tree.
Eventually the traversal of the EDT might come to a derivation whose children
were not created in the initial run. The debugger can regenerate the children
nodes by forcing the re-evaluation of the function application at the parent. In
the first execution of the debuggee, the children are pruned from the EDT. The
purpose of re-evaluating the call at the parent is to cause the previously pruned
children nodes to be re-generated. This allows the EDT to be constructed in a
piecemeal fashion, at the cost of some extra computation time during the debug-
ging session — a classic space/time tradeoff. Re-evaluation was implemented in
a previous version of Buddha, for the purely functional style of transformation,
see [10]. However, it has yet to be incorporated into the latest transformation
style.
Declarative Debugging with Buddha 305
6 Further Reading
Optimistic evaluation of Haskell can reduce the gap between the structure
of the code and the evaluation order by reducing most function arguments ea-
gerly [20]. This is the basis for a step-based debugger called HsDebug [21], built
on top of an experimental version of the Glasgow Haskell Compiler. However, to
preserve non-strict semantics the evaluator must sometimes suspend one com-
putation path and jump to another. This irregular flow of control is likely to be
hard to follow in the step-based debugging style.
Of course no paper on Declarative Debugging would be complete without a
reference to the seminal work of Shapiro, who’s highly influential thesis intro-
duced Algorithmic Debugging to the Prolog language [22], from which the ideas
of Declarative Debugging have emerged.
7 Conclusion
Acknowledgements
I would like to thank all the people who have helped with the preparation of
this paper and who helped organise the 5th International Summer School on
Advanced Functional Programming. In particular: the programme committee,
Varmo Vene, Tarmo Uustalu, and Johan Jeuring, for countless hours of admin-
istration and preparation; the University of Tartu for providing such a great
location; the volunteers who made the school run very smoothly; the various
sponsors who supported the School; the reviewers of this paper for their most
helpful constructive comments; and Lee Naish for all the years of collaboration
on this project.
References
1. Nilsson, H., Spaurd, J.: The evaluation dependence tree as a basis for lazy func-
tional debugging. Automated Software Engineering 4 (1997) 121–150
2. Gansner, E., Koutsofios, E., North, S.: Drawing graphs with dot.
www.research.att.com/sw/tools/graphviz/dotguide.pdf (2002)
3. Jones, N., Mycroft, A.: Dataflow analysis of applicative programs using minimal
function graphs. In: Proceedings of the 13th ACM SIGACT-SIGPLAN symposium
on Principles of Programming Languages, Florida, ACM Press (1986) 296–306
4. Claessen, K., Hughes, J.: Quickcheck: a lightweight tool for random testing of
Haskell programs. In: International Conference on Functional Programming, ACM
Press (2000) 268–279
5. Naish, L., Barbour, T.: Towards a portable lazy functional declarative debugger.
Australian Computer Science Communications 18 (1996) 401–408
6. Sparud, J.: Tracing and Debugging Lazy Functional Computations. PhD thesis,
Chalmers University of Technology, Sweden (1999)
7. Caballero, R., Rodri’guez-Artalejo, M.: A declarative debugging system for lazy
functional logic programs. In Hanus, M., ed.: Electronic Notes in Theoretical
Computer Science. Volume 64., Elsevier Science Publishers (2002)
8. Pope, B., Naish, L.: A program transformation for debugging Haskell-98. Aus-
tralian Computer Science Communications 25 (2003) 227–236 ISBN:0-909925-94-1.
9. Claessen, K., Runciman, C., Chitil, O., Hughes, J., Wallace, M.: Testing and
Tracing Lazy Functional Programs using QuickCheck and Hat. In: 4th Sum-
mer School in Advanced Functional Programming. Number 2638 in LNCS, Oxford
(2003) 59–99
308 B. Pope
10. Pope, B., Naish, L.: Practical aspects of declarative debugging in Haskell-98. In:
Fifth ACM SIGPLAN Conference on Principles and Practice of Declarative Pro-
gramming. (2003) 230–240 ISBN:1-58113-705-2.
11. Wadler, P.: Why no one uses functional languages. SIGPLAN Notices 33 (1998)
23–27
12. Wallace, M., Chitil, O., Brehm, T., Runciman, C.: Multiple-view tracing for
Haskell: a new hat. In: Preliminary Proceedings of the 2001 ACM SIGPLAN
Haskell Workshop. (2001) 151–170
13. Gill, A.: Debugging Haskell by observing intermediate data structures. Technical
report, University of Nottingham (2000) In Proceedings of the 4th Haskell Work-
shop, 2000.
14. Nilsson, H.: Declarative Debugging for Lazy Functional Languages. PhD thesis,
Department of Computer and Information Science Linköpings Universitet, S-581
83, Linköping, Sweden (1998)
15. Nilsson, H.: How to look busy while being as lazy as ever: The implementation of a
lazy functional debugger. Journal of Functional Programming 11 (2001) 629–671
16. Naish, L.: A declarative debugging scheme. Journal of Functional and Logic
Programming 1997 (1997)
17. Naish, L., Barbour, T.: A declarative debugger for a logical-functional language.
In Forsyth, G., Ali, M., eds.: Eighth International Conference on Industrial and
Engineering Applications of Artificial Intelligence and Expert Systems — Invited
and Additional Papers. Volume 2., Melbourne, DSTO General Document 51 (1995)
91–99
18. Naish, L.: Declarative debugging of lazy functional programs. Australian Computer
Science Communications 15 (1993) 287–294
19. Naish, L.: A three-valued declarative debugging scheme. Australian Computer
Science Communications 22 (2000) 166–173
20. Ennals, R., Peyton Jones, S.: Optimistic evaluation: an adaptive evaluation strat-
egy for non-strict programs. In: Proceedings of the Eighth ACM SIGPLAN Con-
ference on Functional Programming. (2003) 287–298
21. Ennals, R., Peyton Jones, S.: HsDebug: Debugging lazy programs by not being
lazy. In Jeuring, J., ed.: ACM SIGPLAN 2003 Haskell Workshop, ACM Press
(2003) 84–87
22. Shapiro, E.: Algorithmic Program Debugging. The MIT Press (1982)
Server-Side Web Programming in WASH
Peter Thiemann
1 Introduction
The basic idea of a web-based application is to make a software system accessible
to the general public by
– creating its user interface in terms of XHTML pages and
– placing the underlying functionality on a web server.
This approach has the appeal that deployment and maintenance of the appli-
cation can be done centrally on the web server, the application works in a dis-
tributed setting without requiring the design of application-specific protocols,
and no specific software needs to be installed on the clients. That is, input and
output is based entirely on XHTML where input is specified either by navigation
via hyperlinks or by using XHTML forms. A form provides an editable associa-
tion list for entering strings and a means to specify where the association list is
to be sent.
However, web applications suffer from some peculiarities that complicate
their development. There are three principal causes for these peculiarities: the
stateless nature of HTTP, the unusual navigation facilities offered by a web
browser, and the reliance on untyped, string-based protocols.
The Hypertext Transfer Protocol [3] is build around the simple idea of a
remote method invocation: a client sends a request message that determines
an object on the server, a method that should be invoked on the object, and
V. Vene and T. Uustalu (Eds.): AFP 2004, LNCS 3622, pp. 309–330, 2005.
c Springer-Verlag Berlin Heidelberg 2005
310 P. Thiemann
parameters for this method. The server performs the requested operation and
returns the results wrapped in a response message. After this exchange, the
connection between client and server is closed (logically, at least) and the next
message from the same client is treated like any other message because the
HTTP-server does not keep any information about processed requests. Hence,
there is no intrinsic notion of a session between a particular client and the server
where the responses depend on the session history of the client. On the user level,
however, most applications require a notion of session where a user proceeds step
by step, is aware of the session’s history, and can issue commands depending
on the current state of the session. Clearly, there is a semantic gap between
the interface provided by HTTP and the interface desired by the application
program.
Web browsers offer advanced navigation that goes beyond the facilities typ-
ically offered in user interfaces [6]. In particular, browsers maintain a history
list of previously visited web locations and offer forward and backward buttons
to navigate freely within this list. Browsers also maintain bookmarks which are
pointers to previously visited locations. Bookmarked locations may be revisited
any time by selecting them from the appropriate menu. Some browsers allow to
clone an existing window or open a link in a new window and continue inde-
pendently with both windows. Finally, it is possible to save the contents of a
window to a file and point the browser to that file later on. While these facilities
are helpful and useful for browsing a static hypertext, they make it hard to define
an appropriate concept of a session when each window is really a dynamically
generated snapshot of the state of some application.
Typical web applications rely on XHTML forms as their input facility. Be-
cause an XHTML form yields an association list and a URL where the data
should be sent, the main data exchanges in such applications are string based:
input fields in forms are named with strings, the input values are strings, and
the pointers in web pages are also strings, albeit in the format of a URL. In this
context, it is very hard to guarantee any kind of consistency. To begin with, the
field names present in a form must be a superset of the field names expected by
the program processing the form input. Otherwise, the program expects values
for inputs that are not present in the form It is even harder to give any typing
guarantees for the entered values themselves because the form processor does
not have this information.1 Finally, there is no way to guarantee that the URLs
mentioned in the hyperlinks and in a form’s action attribute correspond to the
intended functionality, in particular, when they point to scripts.
Implementors of web applications address these problems by defining their
own support for sessions or by relying on third party libraries for sessions. Quite
often, such libraries provide session objects of some sort which have to be main-
tained by the programmer. Unfortunately, many implementations of session ob-
jects only provide a mapping from a specific client to the application specific
data of the client. They often fall short of keeping track of the current locus of
1
The XForms [12] standard will improve on this situation, once implementations are
widely available. Unfortunately, its development seems to be stalled.
Server-Side Web Programming in WASH 311
control of the application.2 Hence, the developers map the control information
to file names and distribute the code of their application over as many pro-
grams as there are interaction states in the program, in the worst case. Clearly,
this approach leads to complex dependencies between the different parts of the
application. In addition, it is hard to detect if users have exercised the above-
mentioned navigation facilities, which leads to further problems and unexpected
responses from the application [5].
These considerations lead us to two questions. First, which abstractions may
be employed to shield the programmer from the problems with sessions, naviga-
tion, and consistent use of strings? Second, given an answer to the first question,
what then is a good approach for designing a web application?
The WASH system [11], a domain-specific language based on Haskell98 [7],
has satisfactory answers to the first question. Its session abstraction handles data
and control information transparently to the programmer: Data is available if
and only if it is in scope and execution continues after a form submission with a
selected callback function. Second, the implementation of sessions is compatible
with arbitrary navigation. Users can go backwards and forwards, they may clone
windows, and save pages without the application programmer providing extra
code for it3 . Finally, WASH provides strongly typed interfaces based on abstract
datatypes for accessing the data entered into the application and for connecting
a form with its functionality in terms of callbacks. It thus replaces external
string consistency with lexical binding wherever possible. The implementation
of WASH can address the consistency problems once and for all because lexical
binding is fully controlled and checked by the compiler.
Our answer to the second question builds on compositionality. WASH inherits
compositionality essentially for free from the underlying Haskell language. In the
WASH context, compositionality means that web applications may be assembled
from independent pagelets, i.e., pieces of web pages which may be specified,
implemented, and tested in isolation. That is, a pagelet integrates form and
function and enables a kind of component-based programming. As pagelets are
represented by Haskell values, Haskell functions can produce them and consume
them, thus providing arbitrary means of parameterization for them. Pagelets are
thus exceptionally amenable to reuse.
The present paper starts of with an introduction to the basic concepts of the
WASH system in Section 2. The remainder, Section 3, considers a disciplined
method for developing web applications using the example of web logging. It
starts with a decomposition of the application into pagelets using a graphical
notation. Then it considers two example pagelets and shows the final wiring.
Each pagelet is naturally divided into a logic part and a presentation part. The
final Section 4 gives a brief introduction to the implementation.
2
Web programming systems that preserve the locus of control in the form of a server-
side continuation include DrScheme [4] and the Cocoon framework [1].
3
The astute reader may wonder if this claim is compatible with server-side state, like
for example a database accessible through the server. We will comment on that point
in Sec.4.
312 P. Thiemann
2 WASH Basics
WASH provides roughly two layers of operations. The first layer provides the
session level operations: displaying a web page, performing server-side I/O op-
erations, and constructing callbacks for inclusion in forms. The second layer
deals with the construction of XHTML output, in particular the generation of
interactive widgets.
Both layers are implemented in a monad-based combinator library, each with
its own monad. The CGI monad implements the session abstraction by keeping
a log of all I/O operations. Hence, it is layered on top of the IO monad and
mediates access to it. To avoid inconsistencies between the current view of the
real world and its logged view for the running session, it is imperative that
the main function of a WASH program does not perform any IO action before
switching into the CGI monad using the function run :: CGI () -> IO ().
The document layer results from applying the monad transformer WithHTML
x to the CGI monad. Its standard incarnation WithHTML x CGI is required to
create interactive widgets. The additional functionality with respect to the base
monad CGI is an abstract interface for the generation of XHTML documents.
The extra parameter x in the type of a document constructor is not used in this
exposition. It enables the compiler to guarantee the validity of the generated
XHTML output just by providing suitable types to the constructors [9].
The function io performs an IO operation and injects its value in the CGI monad.
The type returned by the operation must be an instance of the type classes Read
Server-Side Web Programming in WASH 313
and Show because WASH relies on the methods of these classes for reading and
writing the session log (cf. Sec.4).
The function ask takes the description of an XHTML document and dis-
plays the document on the web browser. The use of WithHTML x CGI in its type
indicates that input widgets may be used to construct the document.
A WASH program is essentially a sequence of io and ask operations which
are glued together using the monadic bind and return operations. The combi-
nator ask takes as a parameter the description of a document in the WithHTML
x CGI monad. This document description embeds submission buttons with at-
tached callbacks of type CGI (). These callbacks take two kinds of parameters,
form input values and standard function parameters.
takes an attribute name and its value (as strings) and creates a singleton se-
quence with just the new attribute node. Text nodes and comments also have
their special constructor functions:
comment :: Monad m => String -> WithHTML x m ()
text :: Monad m => String -> WithHTML x m ()
The argument to text and comment can be an arbitrary string. The WASH
implementation takes care of all character escapes that may be required.
In practice, most of the operators introduced in the previous Section 2.2 can be
avoided by directly putting the desired XHTML fragment into the code. The
WASH preprocessor translates all XHTML fragments in the source program to
constructor operations in the WithHTML x m monad. Inside an XHTML frag-
ment, it is possible to escape to Haskell by enclosing the Haskell code in <% and
%>. The Haskell code may be a term e or a single generator v <- e. As with
the do notation, the generator binds the variable v to the result of computation
e and the binding is available in the rest of the XHTML fragment. The type
of such a term e must be WithHTML x CGI a and the term may again contain
XHTML fragments. There is a specialized version of the code escape bracketed
by <%= and %>. It expects an expression of type String and embed its string
value as a text node in the XHTML fragment. That is, <%= e %> is equivalent
to <% text (e) %>. Further syntax is available for creating attribute nodes, for
embedding attribute values, and for including arbitrary XML files.
Here is a complete program that displays the current date and time.
1 import CGI
2 import Time
3
4 main :: IO ()
5 main =
Server-Side Web Programming in WASH 315
6 run showDate
7
8 showDate :: CGI ()
9 showDate =
10 do theDate <- io $ do clk <- getClockTime
11 cal <- toCalendarTime clk
12 return (calendarTimeToString cal)
13 ask <html>
14 <head><title>The current time</title></head>
15 <body>
16 <h1>The current time</h1>
17 <%= theDate %>
18 </body>
19 </html>
The functionality for calculating the time (lines 10-12) is implemented using the
module Time (imported in line 2) from the standard library. It gets the current
time from the system and converts it to a string using the local timezone and
format information.
The import CGI in line 1 is required in every WASH program. Similarly, in
line 5-6, the main function immediately invokes the main CGI action showDate
through the run function. Because the computation of the date happens in the
IO monad, the io operator must be employed (line 10) to lift its result to the CGI
monad. Finally, the ask operator (line 13) is applied to an XHTML fragment
that contains a string embedding in line 17.
Typically, web pages are not written in isolation but rather as part of a web
site or application that comprises many pages with a similar design. Hence, a
programmer would abstract over the elements that form a standard template
for many documents. The construction of such a template does not involve any
new concepts in WASH. It is sufficient to define the template as a function of
appropriate type.
standardTemplate :: String -> WithHTML x CGI a -> CGI ()
standardTemplate title contents =
ask <html>
<head><title><%= title %></title></head>
<body>
<h1><%= title %></h1>
<% contents %>
</body>
</html>
This template provides the standard skeleton of an XHTML document and ab-
stracts over two aspects of it, a string for the title and a sequence of document
nodes for the contents. In the context of an application, the template will in-
clude further material: stylesheet references, script references, meta information,
and perhaps even parts of a standardized layout.
However, already with this simple template, the showData function becomes
considerably more concise.
316 P. Thiemann
showDate :: CGI ()
showDate =
do theDate <- io $ ...
standardTemplate
"The current time"
(text theDate)
Instead, WASH provides typed abstractions for all XHTML input widgets. Each
of them returns the input values directly in their internal representation. That
is, integers are returned as values of type Int, checkboxes return values of type
Bool, etc. In addition, programmers can define their own input value type by
providing little more than a parser for it.
Instead of naming widgets by string, the WASH constructor of an input wid-
get returns a handle for accessing the input (besides creating the XHTML re-
quired for displaying the widget). The type of the handle depends on the widget,
but each handle-type constructor is an instance of the type class InputHandle.
class InputHandle h where ...
submit :: InputHandle h
=> h INVALID -> (h VALID -> CGI ()) -> HTMLField x y ()
Server-Side Web Programming in WASH 317
The constructor for a textual input field, inputField, uses this type of handle:
inputField :: (Reason a, Read a)
=> WithHTML x CGI ()
-> WithHTML y CGI (InputField a INVALID)
The type parameter a indicates the type of value that is acceptable to the in-
put field. The constraint Read a indicates that a parser for this type must be
available. The type a must also be an instance of class Reason, the members of
which supply an explanation of the input format.
Once a handle has become valid, its value is available through the value
function. The function value is also defined using a type class because it should
be applicable to different handle types.
class HasValue i where
value :: i a VALID -> a
instance HasValue InputField where ...
The variable intValue has indeed type Int and it contains the input value.
One final problem remains to be solved. The submit function takes only one
input handle as a parameter. What if there are multiple handles to pass to the
worker function? It turns out that specially typed tuple constructors are required
for this purpose, F2, F3, and so on. They form tuples of input handles that can
be validated together. Of course, a value cannot be extracted directly from a
tuple of handles, so the worker function first has to take the tuples apart. The
example code in the next section contains a use of F2.
On some occasions, a program dynamically generates an arbitrary number
of handles in a single web page. In most of these cases, each submit function
still takes a finite number of handles. Here are some examples.
1 adder :: CGI ()
2 adder =
3 standardQuery "Adder/1"
4 <#>
5 <p>First number to add <% sum1F <- inputField empty %></p>
6 <p>Second number to add <% sum2F <- inputField empty %></p>
7 <% submit (F2 sum1F sum2F) addThem <[value="Perform addition"]>%>
8 </#>
9
10 addThem (F2 sum1F sum2F) =
11 let sum1, sum2 :: Int
12 sum1 = value sum1F
13 sum2 = value sum2F
14 sum = sum1 + sum2
15 in
16 standardQuery "Adder/2"
17 <#>
18 <p><%= show sum1 %> + <%= show sum2 %> = <%= show sum %></p>
19 <% submit0 adder <[value="Continue"]> %>
20 </#>
The entire application consists of two different screens, Adder/1 (line 3-8)
and Adder/2 (line 15-19). The notation <#> and </#> just serves as a bracket
to combine a number of element nodes to a sequence. In our application, the
XHTML notation is preferable to using the raw document combinators. To see
this, consider lines 5 (or 6) and 7. In line 5, the notation sum1F <- ... binds
the variable sum1F to a handle to the first input field. By our convention, this
binding is valid up to the end of the current XHTML fragment, that is, up to
the closing </#>. Hence, the call to submit can refer directly to sum1F.
The call to submit takes as first argument a pair of handles constructed with
the special F2 operator for pairing handles. Pairs formed with this constructor
may be validated together and the callback function (line 10) can simply pattern-
match against F2 to extract the (now validated) handles again. Finally, the
function value extracts an Int value from each handle (lines 12,13) where the
result type Int is enforced with the type declaration for sum1 and sum2 in line 11.
The callback function is set up so that application logic and presentation are
kept strictly separate. It contains a submission button to restart the application.
This button is constructed using submit0, a specialized version of submit that
does not pass parameters to its callback action. The two submit buttons both
make use of the bracketing notation <[ and ]> for attribute creation to set the
value attribute of the underlying input element.
1 adder :: CGI ()
2 adder =
3 standardQuery "Adder/1"
4 <#>
5 <p>First number to add <input type="text" name="sum1"/></p>
6 <p>Second number to add <input type="text" name="sum2"/></p>
7 <input type="submit" WASH:call="addThem (sum1, sum2)"
8 value="Perform addition" />
9 </#>
10
11 addThem (val1, val2) =
12 let sum :: Int
13 sum = val1 + val2
14 in
15 standardQuery "Adder/2"
16 <#>
17 <p><%= show sum1 %> + <%= show sum2 %> = <%= show sum %></p>
18 <input type="submit" WASH:call="adder" value="Continue" />
19 </#>
of a widget, e.g., they construct an input field or a submission button. These es-
capes can be avoided by having the preprocessor translate the standard XHTML
elements into the correct constructors. The name attribute of the input elements
serves directly as a binding instance of a Haskell identifier for the corresponding
handle. As an example, Fig. 2 contains two textual input elements that bind
the identifiers sum1 and sum2 as well as a submit button.
The submit button makes use of an attribute in a special XML name space
indicated by the prefix WASH. XML attributes in this name space provide extra
information that does not fits into the standard XHTML attributes. In this
case, the WASH:call attribute contains a call template for the callback function.
The attribute value "addThem (sum1, sum2)" indicates that clicking the button
invokes the callback function addThem on the values of the handles sum1 and
sum2. That is, the preprocessor eliminates the need for the F2, F3, . . . tuple
constructors and for the value function in many standard cases. Some cases,
like having a dynamic number of input handles still require special treatment.
The translation of the input element of type submit eliminates the named
tuple constructors in the user program as follows. Suppose that WASH:call="g
(p1 , p2 , ..., pn )".
– The parameter list is transformed to the nested named tuple
(F2 p1 (F2 p2 ( ... (F2 pn F0)...))).
This nested pair becomes the first parameter to the submit function.
– The callback function g is wrapped in a lambda abstraction
(λ (F2 p1 (F2 p2 ( ... (F2 pn F0)...)))
-> g (value p1 , value p2 , ..., value pn )),
Server-Side Web Programming in WASH 321
which becomes the second argument to submit. With this wrapper, the
callback g becomes completely unaware of handles and of named tuples. It
simply becomes a function that takes a tuple of input values and yields a
CGI action.
If no parameters are present, then the preprocessor emits a call to submit0 and
does not wrap the callback function in any way.
WASH allows the creation of customized textual widgets in at least two different
ways. The first way is to create a new data type with a specialized parser for
that type. The other way is to combine and transform existing textual widgets.
New Data Type with Custom Parser. Any data type can serve as a data
type for input fields if it is an instance of Read and an instance of Reason. To
make a type T an instance of Reason it is sufficient to say
instance Reason T
If you want to be really nice, you override the default definition of the method
reason :: T -> String to provide an explanation of T ’s input syntax. reason
must not touch its T argument.
As an example, Figure 3 contains the implementation of a Password data
type, which enforces the rule that a Password is a string of length ≥ 8 with
characters taken from at least three of the four sets: lower case characters, upper
case characters, digits, and special characters.
addPair pairF =
let sum1, sum2 :: Int
(sum1, sum2) = value pairF
in
standardQuery "Adder/2"
<#>
<p><%= show sum1 %> + <%= show sum2 %>=<%= show (sum1+sum2) %></p>
<% submit0 adder <[value="Continue"]> %>
</#>
Fig. 4. Addition with input field for pairs of integers
3 An Example Application
The section considers the disciplined construction of a WASH application with
the example of a web logger. The contributions of the disciplined approach in-
troduced here are twofold.
Server-Side Web Programming in WASH 323
Startpage
appear within web pages. The dotted rectangles correspond to widgets on the
screen. They can only appear within pagelets. Arrows can start from web pages
or pagelets and must end in web pages. Each arrow that starts from a pagelet
models an operation. The widgets in the pagelet provide the input to its opera-
tion. An arrow may fan out to multiple ends if the operation has multiple exits.
For instance, the login arrow splits into a successful end and a failure end.
Solid rectangles may be refined by subsidiary diagrams. Their outgoing ar-
rows must be reconnected to web pages or pagelets in the refined diagram. In-
coming arrows must be reconnected to subsidiary web pages.
Due to space and time constraints, we concentrate on the implementation of
a few pagelets and on the final wiring of the pagelets.
Login functionality is widely used in web applications. The idea is usually that
more functionality is available to registered users, either as a reward for giving
away some personal information and/or to be able to track down users that have
violated the service provider’s contents policy.
The corresponding pagelet should consist of two input fields, one for entering
the user name and another for entering the password. The underlying function-
ality checks the name and password against some database. The pagelet should
allow for some parameterization so that it becomes reusable. In particular, the
definition of the functionality and the visual appearance should be separated
from each other.
Here is a tentative signature for the login function.
The first argument of type Skin describes the visual appearance of the pagelet.
The PasswordChecker is a side-effecting function that is supposed to perform
the password check. It is made into a parameter because its implementation is
likely to differ between systems. The remaining two arguments, SuccessCont
and FailureCont, are the actions taken if the password check succeeds or fails.
The skin argument is supposed to contain the entire visual formatting and
including the choice of input widgets. It should not contain application or pre-
sentation logic. In the present case, it is a function taking the callback for the
sole submit button in the pagelet.
Server-Side Web Programming in WASH 325
loginSkin act =
<table>
<tr><td>Name </td>
<td><input type="text" name="l" /></td>
</tr>
<tr><td>Password </td>
<td><input type="password" name="p" /></td>
</tr>
<tr><td></td>
<td><input type="submit" WASH:call="act( l, p)"
value="Login" /></td>
</tr>
</table>
Clearly, the only implementation decisions taken in this code fragment regard
the visual appearance of the login area: there is a textual input field, a pass-
word input field, and a submit button. The input fields are named l and p
and both are passed to the callback that has to be provided to the submit
function.
The logic of the login pagelet is completely separate in the function login.
login skin pwCheck scont fcont =
skin $ \ (l, p) ->
let logname = unNonEmpty l
pw = unPassword p
in
do registered <- io (pwCheck logname pw)
if registered
then scont logname
else fcont
The application of the skin function to the callback function constructs the
XHTML elements corresponding to the login pagelet. The callback function
implements the logic: it retrieves the values from the input fields at their ex-
pected types, performs the password check, and invokes one of the continua-
tion actions.
There is one unfortunate property of the chosen decomposition in XHTML
skin and functionality. The skin already has to provide for the wiring between
the input widgets and the submission buttons. In the extreme case, the skin
writer (who might be using a tool with a graphical front end) does not know
about the required wiring and may not be familiar with Haskell typing at all.
In such a case, the skin “code” has to assume the worst: all input handles are
passed to all submission buttons. In the worst case, the submit function itself
can be made into a parameter and an appropriate projection function can be
applied to the handle parameter before it is passed to submit.
The loginSkin and login functions are independent of each other and
can be defined in different modules. The definition of the application’s skin
can thus be concentrated into a single separate module just by collecting all
skin definitions.
326 P. Thiemann
Again, the pagelet may be split into the skin and the logic. The skin is
only concerned with the visual appearance. However, it has more parameters
than before because it contains a complicated widget, selectSingle, which
creates a selection box essentially from a list of alternatives. As mentioned before,
selectSingle yields exactly one input handle regardless of the number of items
it selects from.
The logic part has only one novelty. The program needs to lift the CGI
computation getAlternatives to the WithHTML x CGI monad because the skin
is defined in it. Otherwise the code is straightforward.
The use of unText forces the input field to accept (and return to the program)
an arbitrary string of characters.
Server-Side Web Programming in WASH 327
-- application code
blogger :: CGI ()
blogger =
mainPage initialBloggerState ""
data BlogAction =
Visiting | Editing BlogName | Reading BlogName
deriving (Read, Show)
The code for the blogManager function is incomplete in the listing. It only deals
with the case where a user without a login wants to access some blogs. The
implementation of this access, is in the BlogAccess module which provides one
(or more) web pages that either create a new blog or access an existing blog.
The main emphasis of the code is on clearly separating different concerns.
The one part that deals exclusively with the graphical appearance is isolated in
the Skins module. The module Blogger concentrates on the application logic
whereas application specific functions, like myBlogTitles, myPasswordSaver,
and myPasswordCheck, are kept completely separate. Our example implemen-
tation of these functions is in terms of the file system, but it is easy to replace
them by database accesses through a suitable API.
4 A Taste of Implementation
This short section explains the main ideas underlying the implementation of
WASH. It is provided because the implementation has a subtle impact on the
way that WASH deals with server-side state and on the efficiency of a Web
application programmed with WASH. While the efficiency consideration can be
safely dismissed for small applications, the handling of server-side state is crucial
for the semantics.
The main problem in WASH’s implementation is the representation of a
session, that is, a programmed sequence of forms that is driven by the user’s
interactive inputs. The concept of a session is fundamentally at odds with the
stateless nature of HTTP as already explained in the introduction. The most
portable way of running a program through HTTP is the Common Gateway
Interface (CGI) [2]. Such a program is a CGI program. An HTTP request for a
CGI program starts the selected program on the server and passes the param-
eters from the request to the program. The program acts on the parameters,
produces some output—usually another form—that is returned to the browser,
and terminates.
To continue the session, the terminated CGI program somehow has to re-
member the state of the interaction thus far. One common approach is to store
this state on the server and include an pointer to the state into the returned form,
so that the next CGI program can continue from that state. Unfortunately, this
approach is neither scalable nor compatible with the browser’s navigation fea-
tures (the back-button, in particular; see [9] for more discussion). Hence, the
implementation of WASH has made another choice.
Server-Side Web Programming in WASH 329
5 Conclusion
WASH is a web programming system organized around the ideas of type-safe in-
terfaces, abstract datatypes, and compositionality. This combination enables the
modular construction of web applications from pagelets, which are components
integrating functionality and appearance. At the same time, the specifications
of functionality and appearance may be kept separate, as demonstrated in the
paper’s example application. A simple graphical notation for designing web ap-
330 P. Thiemann
plications is put forward. This notation directly reflects the interaction structure
as well as the program structure.
While WASH supports the separation of presentation and logic easily, it is
debatable if a graphic designer has sufficient expertise to perform the rudimen-
tary programming necessary for the presentation part. A more convincing case
could be made if the creation of the presentation part was purely a matter of
XHTML editing. Work towards this goal is in progress.
Acknowledgment
The author is grateful to the second readers for their helpful comments on a
draft of this paper, which led to a number of improvements.
References
1. Apache cocoon project. http://cocoon.apache.org/, June 2004.
2. CGI: Common gateway interface. http://www.w3.org/CGI/, 1999.
3. R. Fielding, J. Gettys, J. Mogul, H. Frystyk, L. Masinter, P. Leach, and T. Berners-
Lee. Hypertext transfer protocol. http://www.faqs.org/rfcs/rfc2616.html,
June 1999.
4. Paul Graunke, Robert Bruce Findler, Shriram Krishnamurthi, and Matthias
Felleisen. Automatically restructuring programs for the Web. In Proceedings of
ASE-2001: The 16th IEEE International Conference on Automated Software En-
gineering, pages 211–222, San Diego, USA, November 2001. IEEE CS Press.
5. Paul T. Graunke, Robert Bruce Findler, Shriram Krishnamurthi, and Matthias
Felleisen. Modeling Web interactions. In Proc. 12th European Symposium on
Programming, Lecture Notes in Computer Science, Warsaw, Poland, April 2003.
Springer-Verlag.
6. Paul T. Graunke and Shriram Krishnamurthi. Advanced control flows for flexible
graphical user interfaces: or, growing GUIs on trees or, bookmarking GUIs. In
Proceedings of the 24th International Conference on Software Engineering (ICSE-
02), pages 277–290, New York, May 19–25 2002. ACM Press.
7. Haskell 98, a non-strict, purely functional language.
http://www.haskell.org/definition, December 1998.
8. Philippe Le Hégaret, Ray Whitmer, and Lauren Wood. W3c document object
model. http://www.w3.org/DOM/, August 2003.
9. Peter Thiemann. An embedded domain-specific language for type-safe server-side
Web-scripting. ACM Transactions on Internet Technology, 5(1):1–46, 2005.
10. Philip Wadler. Monads for functional programming. In Advanced Functional
Programming, volume 925 of Lecture Notes in Computer Science, pages 24–52.
Springer-Verlag, May 1995.
11. Web authoring system in Haskell (WASH). http://www.informatik.uni-
freiburg.de/ thiemann/haskell/WASH, March 2001.
12. XForms - the next generation of Web forms. http://www.w3.org/MarkUp/Forms/,
May 2003.
Refactoring Functional Programs
Simon Thompson
1 Introduction
Refactoring [8] is about improving the design of existing computer programs and
systems; as such it is familiar to every programmer, software engineer and de-
signer. Its key characteristic is the focus on structural change, strictly separated
from changes in functionality. A structural change can make a program simpler,
by removing duplicate code, say, or can be the preparatory step for an upgrade
or extension of a system.
Program restructuring has a long history. As early as 1978 Robert Floyd in
his Turing Award lecture [7] encouraged programmers to reflect on and revise
their programs as an integral part of their practice. Griswold’s thesis on auto-
mated assistance for LISP program restructuring [9] introduced some of the ideas
developed here and Opdyke’s thesis [22] examined refactoring in the context of
object-oriented frameworks. Martin Fowler brought the field to prominence with
his book on refactoring object-oriented programs [8]. The refactoring browser, or
‘refactory’ [3], for Smalltalk is notable among the first generation of OO tools;
a number of Java tools are now widely available. The best known of these is
the refactoring tool for Java in Eclipse [5]. More comprehensive reviews of the
refactoring literature are available at the web page for [8] and at our web site.1
Refactorings are one sort of program transformation; they differ from other
kinds of program transformation in a number of ways. Traditional transforma-
tions usually have a ‘direction’: they are applied to make a program more time or
space efficient, say. On the other hand, refactorings are typically bi-directional: a
refactoring to widen the scope of a local definition could equally well be applied
in reverse to localise a global definition.
It is also characteristic of refactorings that they are ‘diffuse’ and ‘bureau-
cratic’: that is, their effect is not limited to a particular point in a program, and
they require care and precision in their execution. Consider the example of the
simplest possible refactoring: renaming a component of a program. To effect this
1
http://www.cs.kent.ac.uk/projects/refactor-fp/
V. Vene and T. Uustalu (Eds.): AFP 2004, LNCS 3622, pp. 331–357, 2005.
c Springer-Verlag Berlin Heidelberg 2005
332 S. Thompson
change requires not only the component definition to be changed, but also every
use of the component must be similarly modified. This involves changing every
file or module which might use the component, potentially tens or hundreds of
modules. Moreover, it is vital not to change any components hidden in other
parts of the system which happen to share the same name.
It is, of course, possible to do refactorings ‘by hand’, but this process is
tedious and, more importantly, error-prone. Automated support for refactorings
makes them safe and easy to perform, equally easy to undo, and also secure in
their implementation. The Refactoring Functional Programs 2 [17] project at the
University of Kent is building the HaRe [12] system to support refactorings for
Haskell programs.
HaRe is designed as a serious tool for use by practising programmers: HaRe
supports the whole of Haskell 98; it is integrated into standard development
environments and it preserves the ‘look and feel’ of refactored programs. HaRe
is built using a number of existing libraries: Programatica [11] on which to
build the language-analysis components, and Strafunski [19] which gives general
support for tree transformations.
These notes begin presenting overviews of design for functional programs
and the HaRe system. The core of the paper is an exposition of the basics of
refactoring: a detailed description of generalisation is presented as an example of
a structural refactoring in Section 4, and the impact of modules on refactoring
is examined in Section 5.
A number of data-oriented refactorings are given Section 6: principal among
these is the transformation taking a concrete data type into an ADT, which is
implemented in HaRe as composition of simpler refactorings. As well as provid-
ing a repertoire of built-in refactorings, HaRe provides an API by which other
refactorings can be constructed; this is the subject of Section 7. The notes con-
clude with a discussion of conclusions and directions for the research.
I am very grateful indeed to my colleagues Huiqing Li and Claus Reinke,
interns Nguyen Viet Chau and Jon Cowie, and research students Cyris Ryder
and Chris Brown for their collaboration in the project. I would also like to thank
the referees for their suggestions and corrections.
If we accept the final reason, which appears to be the closest to existing practice,
we are forced to ask how design emerges. A general principle is the move from
the concrete to the abstract, and from the specific to the general. Specifically,
for Haskell, we can use the following strategies:
Generalisation. A function is written with a specific purpose: it is generalised
by making some of the particular behaviour into an argument.
Higher-Order Functions. This particular case of generalisation is character-
istic of modern functional programming: specific behaviour is abstracted into a
function, which becomes a parameter.
Commonality. Two parts of a program are identified as being identical or at
least similar; they can be replaced by invocations of a single function (with
appropriate parameters).
Data Abstraction. Concrete, algebraic data types provide an excellent start-
ing point, but are difficult to modify: a move to an abstract type gives the pro-
grammer flexibility to modify the implementation without modifying any client
code.
Overloading. The introduction of a class and its instances allows set of
names to be overloaded: programs thus become usable in a variety of contexts.
This can make programs more readable, and also replace a number of similar
definitions by a single, overloaded, one.
Monadification. This particular case of overloading allows explicit computa-
tional effects to become an implicit part of a system; once this transformation
has taken place it is possible to modify the monad being used without changing
the client code. A number of monads can be combined using monad transform-
ers [14].
The HaRe tool supports many of these ‘design abstractions’. Using a refactoring
tool allows programmers to take a much more exploratory and speculative ap-
proach to design: large-scale refactorings can be accomplished in a single step,
and equally importantly can be undone with the same effort. In this way Haskell
programming and pedagogy can become very different from current practice.
Refactoring for Haskell is supported by the HaRe tool [12] built at the University
of Kent as a part of the project Refactoring Functional Programs. The system
was designed to be a tool useable by the working programmer, rather than a
334 S. Thompson
the system infrastructure used for implementing refactorings and other transfor-
mations in HaRe; this is addressed in more detail in Section 7.
HaRe, embedded in Emacs, is shown in Figures 1 and 2. A new Refactor
menu has been added to the user interface: menu items group refactorings, and
submenus identify the particular refactoring to be applied. Input is supplied by
the cursor position, which can be used to indicate an identifier to be renamed,
say, and from the keyboard, to give the replacement identifier, for instance.
Figure 1 shows a program defining and using a concrete data type; Figure 2
shows the result of refactoring this to an abstract data type.
3.2 Implementation
Syntax. The subject of the refactoring (or program transformation) is the ab-
stract syntax tree (AST) for the parsed program. To preserve comments and
layout, information about comments and source code locations for all tokens is
also necessary.
Static Semantics. In the case of renaming a function f it is necessary to check
that this binding of f does not capture any existing uses of f. The binding
analysis provides this information.
Module Analysis. In a multi-module project, analysis must include all mod-
ules. For example, renaming the function f must percolate through all modules
of a project which import this binding of f.
Type System. If a function g is generalised (as in Section 4) then its type
declaration will need to be adjusted accordingly.
It is therefore clear that we require the full functionality of a Haskell front-end
in order to implement the refactorings completely and safely. In this project we
have used the Programatica front end [11], which supports all aspects of analysis
of Haskell 98. The correct implementation of a refactoring consists of four parts,
shown in Figure 3.
Information Gathering and Condition Checking. The refactoring will
only be performed if it preserves the semantics of the program; examples of
some of the conditions are given above. Verifying these conditions requires in-
formation, such as the set of identifiers in scope at a particular point in the
program, to be gathered from the AST by traversing it.
Transformation. Once the conditions are verified, it is possible to perform the
refactoring, which is a transformation of the AST.
Program Rendering. Once transformed, source code for the new program
needs to be generated, conforming to the original program layout as much as
possible.
Information gathering and transformation consist for the most part of ’boiler-
plate’ code: generic operations are performed at the majority of AST nodes, with
the real work being performed by ad hoc operations at particular kinds of node.
These hybrid generic / specific traversals are supported by a number of systems:
in HaRe we use Strafunski [19]; other systems include [15,16].
Refactoring Functional Programs 337
4 Structual Refactorings
Generalisation
Left to right comment: In the ex- Right to left comment: The inverse
ample shown, a single expression is se- can be seen as a sequence of simpler
lected. It is possible to abstract over a refactorings.
number of occurrences of the (syntacti-
cally) identical expression by preceding – A definition of a special case is in-
this refactoring by troduced: fmt = format "\n" and
any uses of format "\n" (outside
– a transformation to a single equa- its definition) are folded to fmt.
tion defined by a case expression; – Using generative folding, the def-
– the introduction of a local defini- inition of format is specialised to
tion of a name for the common ex- a definition of fmt. (Folds in the
pression. style of Burstall and Darlington
are called generative as they will
and by following the refactoring by the generate a new definition.)
appropriate inverse refactorings. – If all uses of format take the
In a multi-module system, some of parameter "\n" then no uses of
the free variables in the selected sub- format remain. Its definition can
expression might not be accessible to be removed, and fmt can be re-
the call sites in some client modules. In- named to format.
stead of explicitly exporting and/or im-
porting these variables, the refactorer
creates an auxiliary function (fGen,
say) in the module containing the defi-
nition to represent the sub-expression,
and makes it accessible to the client
modules.
(cont.)
Left to right conditions: There are Right to left conditions: The suc-
two conditions on the refactoring. cessful specialisation depends upon the
definition of the function to have a par-
– Adding the new formal parame- ticular form: the particular argument
ter should not capture any existing to be removed has to be a constant pa-
uses of variables. rameter: that is, it should appear un-
– The abstracted sub-expression, e changed in every recursive call.
say, becomes the first argument of The definition of the original function
the new function at every use of it. can only be removed if it is only used
For every new occurrence of e it is in the specialised form.
a requirement that the bindings of
all free identifiers within e are re-
solved in the same way that they
are in the original occurence.
Analysis required: Static analysis of bindings; call graph; module analysis. If the
type declaration is to be modified, then type inference will be needed.
– If a definition is moved from a local scope to the top level, it may be that
some names move out of their scope: this could leave them undefined, or
bound to a different definition.
– In the case of generalisation, a new formal parameter is added to the def-
inition in question: this may also disturb the binding structure, capturing
references to an object of the same name defined at the top level.
Capture can occur in two ways: the new identifier may be captured, as when f
is renamed to g:
h x = ... h ... f ... g ... h x = ... h ... g ... g ...
where where
g y = ... g y = ...
f x = ... g x = ...
or it may capture other uses, as when a local definition f is renamed to g:
h x = ... h ... f ... g ... h x = ... h ... g ... g ...
where where
f y = ... f ... g ... g y = ... g ... g ...
g x = ... g x = ...
In the next section we explore the impact of modules on the refactoring process
for Haskell.
If the conditions are satisfied then the refactoring can be achieved by moving
the definition from A to B with some follow-up actions.
– Modify the import/export lists in the modules A and B and the client modules
of A and B as necessary.
– Change uses of A.f to B.f or f in all affected modules.
– Resolve any ambiguity that might arise.
Other refactorings within the module system include: moving a group of defi-
nitions, moving type, class and instance definitions, and merging and splitting
modules.
6 Data-Oriented Refactorings
This section looks in more detail at a number of larger-scale, data-oriented,
refactorings. It is characteristic of all of these that they are bi-directional, with
the context determining the appropriate direction. Some of these refactorings
are described in the case study of [26]. The section concludes with an overview
of other, type-based, refactorings.
data Tree a
= Leaf a |
Node a (Tree a) (Tree a)
The definition has two cases: a Leaf and a (recursive) Node. Correspondingly, a
function to flatten a tree into a list has two clauses: the first deals with a leaf,
and the second processes a node, recursively:
flatten :: Tree a -> [a]
module Tree (Tree, leaf, node, isLeaf, isNode, val, left, right) where
data Tree a
= Leaf a |
Node a (Tree a) (Tree a)
leaf = Leaf
node = Node
Add Field Names. Names are added to the fields of the data type. Names are
chosen by the system, but these can be changed using the renaming refactoring.
Add Discrimiators. By default, discriminators are named ‘isCon’ for the con-
structor Con. If functions of this name already exist, other names are chosen.
Add Constructors. Functions con corresponding to the constructor Con are
introduced.
Remove Nested Patterns. A particular problem is pressented by patterns
containing constructors from other datatypes. Using the Tree example again,
consider the fragment
in which a list constructor occurs within a pattern from the Tree datatype. We
will have to replace this pattern with a variable, and thus we lose the list pattern
match too. So, we need to deal with this nested pattern first, thus:5
f t
| isLeaf t = case (val t) of
[x] -> x+17
Create ADT Interface. Move the type definition into a separate file with an
interface containing the selectors, discriminators and constructor functions.
Views [28,1] give a mechanism for pattern matching to cohabit with type ab-
straction. It would be possible to augment the refactoring to include the appro-
priate view, and to retain pattern matching definitions whilst introducing type
abstraction, if the revised proposal [1] were to be incorporated into Haskell.
The abstraction of Tree in Section 6.1 gives a minimal interface to the type:
values can be constructed and manipulated, but no other functions are included
in the ‘capsule’ or module which delimits the type representation.
Arguably, more functions, such as flatten in our running example. might
be included in the capsule. What are the arguments for and against this?
5
It may also be necessary to amalgamate a number of clauses before performing this
step, since it is not possible to ‘fall through’ a case statement.
344 S. Thompson
data Tree a
= Leaf { val::a, flatten:: [a] } |
Node { val::a, left,right::(Tree a), flatten::[a] }
On the other hand, with the right-hand definition it is possible to treat Plus
explicitly, as in
However, it is not just possible but necessary to define a Plus case for every
function working over the right-hand variant of Expr, thus requiring more effort
and offering more opportunity for error.6
In any particular situation, the context will be needed to determine which
approach to use. Note, however, that the transition from left to right can seen
as a refactoring: the definitions thus produced may then be transformed to yield
a more efficient version as is possible for the literals function.
instance Sh Rect
area (Rect h w) = h*w
perim (Rect h w) = 2*(h+w)
convert shapes to a single type (e.g. via show) to turn a case analysis over types
into a corresponding case over values.
Each representation will be preferable in certain circumstances, just as row-
major and column-major array representations are appropriate for different al-
gorithms.8 The transformation from left to right can be seen as the result of a
sequence of simpler refactorings:
– introducing the algebraic ‘subtypes’ corresponding to the constructors of the
original type: in this case Circle and Rect;
– introducing a class definition for the functions: here the class Sh;
– introducing the instance declarations for each ‘subtype’,
– and finally introducing the existential type: in this example, Shape.
common to Bin nodes can be written in a general form, and the pattern matching
over the original Expr type can be reconstructed thus:
This approach has the advantage that it is, in one way at least, more straightfor-
ward to modify. To add division to the expression type, it is a matter of adding
to the enumerated type an extra possibility, Div, and adding a corresponding
clause to the definition of evalOp.
Note that moving between representations requires the transformation of all
definitions that either use or return an Expr.
6.7 Monadification
– a program with explicit actions – such as a state ‘threaded’ through the eval-
uation – is made into a program which explicitly uses the monadic operations
return and >>=, or indeed their ‘sugared’ version, the do notation.
An example of what is required can be see in Figures 11 and 12. Figure 11 shows
a type of side-effecting expressions, and a store type. An example of the side
effects are seen in
y := (x := x+1) + (x := x+1)
Evaluating this expression in a store where x has the value 3 results in y being
assigned 9: the first sub expression has the value 4, the second 5.
Figure 12 gives two versions of an evaluator for these expressions. On the
left-hand side is an evaluator which passes the Store around explicitly. The key
case is the evaluation of Add e1 e2 where we can see that e2 is evaluated in the
store st1, which may have been modified by the evaluation of e1.
On the right-hand side is the monadic version of the code. How easy is it
to transform the left-hand side to the right? It is a combination of unfolding
and folding function definitions, combined with the transformation between a
where clause and a let. Unfolding and folding of functions defined in instance
declarations necessitates a type analysis in order to associate uses of identifiers
with their definitions. Existing work on describing monad intoduction includes
Erwig and Ren’s monadification [6] and Lämmel’s monad introduction [13].
data Expr
= Lit Integer | -- Literal integer value
Vbl Var | -- Assignable variables
Add Expr Expr | -- Expression addition: e1+e2
Assign Var Expr -- Assignment: x:=e
eval :: Expr -> Store -> (Integer, Store) evalST :: Expr -> State Store Integer
Some of these refactorings are already implemented in HaRe; others are being
developed. The next section offers users the possibility of implementing refac-
torings for themselves.
Using these libraries we have built other libraries of utilities for syntax ma-
nipulation: functions to collect all free identifiers in an expression, substitution
functions and so forth.
Two library layers are necessary because of our need to preserve program
layout and comments. In common with the vast majority of compilers, Pro-
gramatica’s abstract syntax tree (AST) omits comments, and contains only a
limited amount of source code location information.
To keep track of complete comment and layout data we work with the token
stream output by the lexical analyser, as well as the AST. When a program is
modified we update both the AST and the token stream, and we output the
Composite refactorings
Primitive refactorings
RefacUtils
RefacLocUtils
Programatica Strafunski
source code program by combining the information held by them both. This
necessitates that the utilities we design must manipulate both AST and token
stream; we provide two libraries to do this.
RefacUtils: this library hides the token stream manipulation, offering a set
of high-level tree manipulation functions which will manipulate syntactic frag-
ments; operations provided include insert, substitute, swap and so forth. These
are built on top of our other library, which is described next.
fragments within an AST, and so will be usable in many contexts. The code in
Figure 14 also illustrates the Programatica ‘two level’ syntax in action: the Exp
constructors witness the recursive knot-typing.9
The code in Figure 14 makes the ‘swap’ transformation but raises an error at
any point where the function is used with less than two arguments. In a full im-
plementation this condition would be checked prior to applying the refactoring,
with two possibilities when the condition fails.
– No action is taken unless all applications have at least two arguments.
– Compensating action is taken in cases with fewer arguments. In this case it is
possible to replace these instances of a function f, say, with calls to flip f,
where flip f a b = f b a. Note that in particular this handles ‘explicit’
applications of a function of the form f $ a $ b.
Full details of the API and function-by-function Haddock [10] documentation
are contained in the HaRe distribution. Details of implementing a number of
fusion transformations are given in [20].
8 Reflecting on Refactoring
The work we have seen so far raises a number of questions and directions for
future work.
9
The two-level syntax is exemplified by a definition of lists. First a type constructor
function is defined, data L a l = Nil | Cons a l and then the recursive type is
defined to be the fixed point of L, thus: newtype List a = List (L a (List a)).
Since types cannot be recursive in Haskell, the fixed point introduces a wrapping
constructor, List here. For example, under this approach the list [2] will be given
by the term List (Cons 2 (List Nil)).
Refactoring Functional Programs 353
10
This is illustrated in the case study [26].
354 S. Thompson
of the tool comes from implementing a set of clearly-defined, simple and useful
refactorings, rather than attempting to be comprehensive.
Not (Quite) a Refactoring? Some operations on programs are not precisely
refactorings, but can be supported by the same infrastructure, and would be of
value to programmers. Examples include:
– Add a new constructor to a data type:11 this should not only add the con-
structor but also add new clauses to definitions which use pattern matching
over this type.
– Add a field to a constructor of a data type: this would require modification
to every pattern match and use of this constructor.
– Create a new skeleton definition for a function over a data type: one clause
would have to be introduced for each constructor.
Building a tool like HaRe makes us focus on some of the details of the design of
Haskell, and how it might be improved or extended.
The Correspondence Principle. At first sight it appears that there are cor-
respondences between definitions and expressions [24], thus:
Expressions Definitions
Conditional if ... then ... else ... guard
Local definition let where
Abstraction \p -> ... f p = ...
Pattern matching case x of p ... f p = ...
In fact, it is not possible to translate freely between one construct and its corre-
spondent. In general, constructs associated with definitions can be ‘open ended’
whereas expressions may not.
Take a particular case: a clause of a function may just define values for certain
arguments because patterns or guards may not exhaust all the possibilities;
values for other arguments may be defined by subsequent clauses. This is not
the case with if ... then ... else ... and case: speaking operationally,
once entered they will give a value for all possible arguments; it is not possible
to fall through to a subsequent construct.
Arguably this reflects a weakness in the design of Haskell, and could be
rectified by tightening up the form of definitions (compulsory otherwise and so
forth), but this would not be acceptable to the majority of Haskell users.
Scoped Instance Declarations. In Haskell it is impossible to prevent an in-
stance declaration from being exported by a module. The lack of scoped class
11
It is arguable that this is a refactoring, in fact. Adding a constructor only has an
effect when that constructor is used, although this could arise indirectly through use
of a derived instance of Read.
Refactoring Functional Programs 355
References
23. Daniel J. Russell. FAD: Functional Analysis and Design Methodology. PhD thesis,
University of Kent, 2000.
24. Robert D. Tennent. Principles of Programming Languages. Prentice Hall, 1979.
25. Simon Thompson. Minesweeper. http://www.cs.kent.ac.uk/people/staff/sjt/
craft2e/Games/.
26. Simon Thompson and Claus Reinke. A Case Study in Refactoring Functional
Programs. In Brazilian Symposium on Programming Languages, 2003.
27. The Unified Modeling Language. http://www.uml.org/.
28. Philip Wadler. Views: a way for pattern-matching to cohabit with data abstraction.
In Proceedings of 14th ACM Symposium on Principles of Programming Languages.
ACM Press, January 1987. (Revised March 1987).
Author Index