The Conception, Evolution, and Application of Functional Programming Languages
The Conception, Evolution, and Application of Functional Programming Languages
Abstract
The foundations of functional programming languages are examined from both
historical and technical perspectives. Their evolution is traced through several
critical periods: early work on lambda calculus and combinatory calculus, Lisp,
Iswim, FP, ML, and modern functional languages such as Miranda
1
and Haskell.
The fundamental premises on which the functional programming methodology
stands are critically analyzed with respect to philosophical, theoretical, and prag-
matic concerns. Particular attention is paid to the main features that characterize
modern functional languages: higher-order functions, lazy evaluation, equations
and pattern-matching, strong static typing and type inference, and data abstrac-
tion. In addition, current research areassuch as parallelism, non-determinism,
input/output, and state-oriented computationsare examined with the goal of pre-
dicting the future development and application of functional languages.
Categories and Subject Descriptors: D.1.1 [Programming Techniques]: Applicative
(Functional) Programming; D.3.2 [Programming Languages]: Language Classications
applicative languages; data-ow languages; nonprocedural languages; very high-
level languages; F.4.1 [Mathematical Logic and Formal Languages]: Mathematical
Logiclambda calculus and related systems; K.2 [History of Computing]: Software.
General Terms: Languages.
Additional Key Words and Phrases: referential transparency, higher-order func-
tions, types, lazy evaluation, data abstraction.
1 Introduction
The earliest programming languages were developed with one simple goal in mind: to
provide a vehicle through which one could control the behavior of computers. Not
e, if i = j
x
j
, if i }= j
[e
1
/x](e
2
e
3
) = ([e
1
/x]e
2
) ([e
1
/x]e
3
)
[e
1
/x
i
](x
j
.e
2
) =
x
j
.e
2
, if i = j
x
j
.[e
1
/x
i
]e
2
, if i }= j and x
j
} fv(e
1
)
x
k
.[e
1
/x
i
]([x
k
/x
j
]e
2
), otherwise, where k }= i, k }= j,
and x
k
} fv(e
1
) fv(e
2
)
The last rule is the subtle one, since it is where a name conict could occur, and is
resolved by making a name change. The following example demonstrates application
5
The notation d D means that d is a typical element of the set D, whose elements may be
distinguished by subscripting. In the case of identiers, we assume that each x
i
is unique; i.e., x
i
}= x
j
if
i }= j. The notation d ::= alt1 alt2 altn is standard BNF syntax.
6
In denotational semantics the notation e[v/x] is used to denote the function e
x = v. Our notation of placing the brackets in front of the expression is to emphasize that
[v/x]e is a syntactic transformation on the expression e itself.
8
of all three rules:
[y/x]((y.x)(x.x)x) (z.y)(x.x)y
To complete the lambda calculus we dene three simple rewrite rules on lambda
expressions:
1. -conversion (renaming): x
i
.e x
j
.[x
j
/x
i
]e, where x
j
} fv(e).
2. -conversion (application): (x.e
1
)e
2
[e
2
/x]e
1
.
3. -conversion: x.(e x) e, if x } fv(e).
These rules, together with the standard equivalence relation rules for reexivity, sym-
metricity, and transitivity, induce a theory of convertibility on the lambda calculus,
which can be shown to be consistent as a mathematical system.
7
The well-known
Church-Rosser Theorem[CR36] (actually two theorems) is what embodies the strongest
form of consistency, and has to do with a notion of reduction, which is the same as
convertibility but restricted so that -conversion and -conversion only happen in one
direction:
1. -reduction: (x.e
1
)e
2
[e
2
/x]e
1
.
2. -reduction: x.(e x) e, if x } fv(e).
We write e
1
e
2
if e
2
can be derived from zero or more - or -reductions or -
conversions; in other words
cap-
tures the notion of reducibility, and
e
1
then there exists an e
2
such that e
0
e
2
and
e
1
e
2
.
8
In other words, if e
0
and e
1
are intra-convertible, then there exists a third term
(possible the same as e
0
or e
1
) to which they can both be reduced.
Corollary: No lambda expression can be converted to two distinct normal forms (ignor-
ing dierence due to -conversion).
One consequence of this result is that how we arive at the normal form does not
matter; i.e. the order of evaluation is irrelevant (this has important consequences for
parallel evaluation strategies). The question then arises as to whether or not it is always
possible to nd the normal form (assuming it exists). We begin with some denitions.
Denition: Anormal-order reduction is a sequential reduction in which, whenever there
is more than one reducible expression (called a redex), the left-most one is chosen rst.
In contrast, an applicative-order reduction is a sequential reduction in which the left-
most innermost redex is chosen rst.
Church-Rosser Theorem II: If e
0
e
1
and e
1
is in normal form, then there exists a
normal-order reduction from e
0
to e
1
.
This is a very satisfying result; it says that if a normal form exists, we can always
nd it; i.e. just use normal-order reduction. To see why applicative-order reduction is
not always adequate, consider the following example:
applicative-order: normal-order:
(x. y) ((x. x x) (x. x x)) (x. y) ((x. x x) (x. x x))
(x. y) ((x. x x) (x. x x)) y
.
.
.
We will return to the trade-os between normal- and applicative-order reduction in
Section 3.2. For now we simply note that the strongest completeness and consistency
results have been achieved with normal-order reduction.
In actuality, one of Churchs (and others) motivations for developing the lambda
calculus in the rst place was to form a foundation for all of mathematics (in the way
that, for example, set theory is claimed to provide such a foundation). Unfortunately, all
attempts to extend the lambda calculus suciently to form such a foundation failed to
yield a consistent theory. Churchs original extended system was shown inconsistent
by the Kleene-Rosser Paradox [KR35]; a simpler inconsistency proof is embodied in
what is known as the Curry Paradox [Ros82]. The only consistent systems that have
8
Church and Rossers original proofs of their theorems are rather long, and many have tried to improve
on them since. The shortest proof I am aware of for the rst theorem is fairly recent, and aptly due to
Rosser [Ros82].
10
been derived from the lambda calculus are much too weak to claim as a foundation for
mathematics, and the problem remains open today.
These inconsistencies, although disappointing in a foundational sense, did not slow
down research on the lambda calculus, which turned out to be quite a nice model
of functions and of computation in general. The Church-Rosser Theorem was an ex-
tremely powerful consistency result for a computation model, and in fact rewrite sys-
tems completely dierent from the lambda calculus are often described as possessing
the Church-Rosser property or even anthropomorphically as being Church-Rosser.
2.1.3 Recursion, -Denability, and Churchs Thesis
Another nice property of the lambda calculus is embodied in the following theorem:
Fixpoint Theorem: Every lambda expression e has a xpoint e
such that (e e
.
Proof: Take e
1
1
e
2
2
)
1
. Modifying the pure lambda calculus in this way, we arrive at the
24
pure typed lambda calculus:
b BasTyp Basic types
Typ Derived types
where ::= b
1
2
x
Id Typed identiers
e Exp Typed lambda expressions
where e ::= x
(e
1
1
e
2
2
)
1
(x
2
. e
1
)
1
for which we then provide the following reduction rules:
1. Typed--conversion: (x
1
1
.e
) (x
1
2
.[x
1
2
/x
1
1
]e
), where x
1
2
} fv(e
).
2. Typed--conversion: ((x
2
. e
1
1
) e
2
2
) [e
2
2
/x
2
]e
1
1
.
3. Typed--conversion: x
1
. (e
2
x
1
) e
2
, if x
1
} fv(e
2
).
To preserve type correctness we assume that the typed identiers x
1
and x
2
, where
1
2
, are distinct identiers. Note then how every expression in our new calculus
carries with it its proper type, and thus type-checking is built-in to the syntax.
Unfortunately, there is a serious problem with this result: our new calculus has lost
the power of -denability! In fact, every term in the pure typed lambda calculus can
be shown to be strongly normalizable, meaning each has a normal form, and there is an
eective procedure for reducing each of themto its normal form[FLO85]. Consequently
we can only compute a rather small subset of functions from integers to integers
namely the extended polynomials [Bar84].
16
The reader can gain some insight into the problem by trying to write the denition
of the Y combinatorthat paradoxical entity that allowed us to express recursionas a
typed term. The diculty lies in properly typing self-application (recall the discussion
in Section 2.1), since typing (e e) requires that e have both the type
2
1
and
2
,
which cannot be done within the structure we have given. It can, in fact, be shown that
the pure typed lambda calculus has no xpoint operator.
Fortunately, there is a clever way to solve this dilemma. Instead of relying on self
application to implement recursion, simply add to the calculus a family of constant
xpoint operators similar to Y, only typed. To do this we rst move into the typed
lambda calculus with constants, in which a domain of constants Con is added as in
16
On the bright side, some researchers view the strong normalization property as a feature, since it
means that all programs are guaranteed to terminate. Indeed this property forms the basis of much of
the recent work on using constructive type theory as a foundation for programming languages.
25
the (untyped) lambda calculus with constants. We then include in Con a typed xpoint
operator of the form Y
: ( )
Then for each xpoint operator Y
(e
(Y
The reader may easily verify that type consistency is maintained by this rule.
By ignoring the type information, we can see that the above -rule corresponds
to the conversion (Y f) (f (Y f)) in the untyped case, and the same trick for
implementing recursion with Y as discussed in Section 2.1 can be used here, thus
regaining -denability. For example, a non-recursive denition of the typed factorial
function would be the same as the untyped version given earlier, except that Y
would
be used where = Int Int.
In addition to this calculus, we can derive a typed recursive lambda calculus with
constants in the same way that we did in the untyped case, except that instead of the
unrestricted Y combinator we use the typed versions dened above.
At this point our type system is about at par with that of a strongly and explicitly
typed language such as Pascal or Ada. However, we would like to do better. As men-
tioned in Section 2.6, the designers of ML extended the state-of-the-art of type systems
in two signicant ways:
They permitted polymorphic functions.
They used type inference to infer types rather than requiring that they be declared
explicitly.
As an example of a polymorphic function, consider:
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs
The rst line is a type signature that declares the type of map; it can be read as for all
types a and b, map is a function that takes two arguments, a function from a into b and
a list of elements of type a, and returns a list of elements of type b.
[Type signatures are optional in Haskell; the type system is able to infer them auto-
matically. Also note that the type constructor -> is right associative, which is consistent
with the left associativity of function application.]
26
So map can be used to map square down a list of integers, or head down a list of
lists, etc. In a monomorphic langage such as Pascal one would have to dene a separate
function for each of these. The advantage of polymorphism should be clear.
One way to achieve polymorphism in our calculus is to add a domain of type vari-
ables and extend the domain of derived types accordingly:
b BasTyp Basic types
v TypId Type variables
Typ Derived types
where ::= b v
1
2
Thus, for example, v is a proper type, and can be read as if the type variable was
universally quantied: for all types v, the type v . To accommodate this we must
change the rule for -conversion to read:
2. Typed--conversion with type variables:
(a) ((x
2
. e
1
1
) e
2
2
) ([e
2
2
/x
2
]e
1
1
)
1
(b) ((x
v
. e
1
1
) e
2
2
) ([
2
/v]([e
2
2
/x
v
]e
1
1
))
1
where substitution on type variables is dened in the obvious way. Note that this
rule implies the validity of expressions of the form (e
v
1
1
e
2
2
)
1
. Similar changes are
required to accommodate expressions such as (e
v
1
e
2
)
v
.
But alas, now that type variables have been introduced, it is no longer clear whether
a program is properly typedit is not built-in to the static syntactic structure of the
calculus. In fact, the type checking problem for this calculus is undecideable, being a
variant of a problem known as partial polymorphic type inference [Boe85, Pfe88].
Rather than trying to deal with the type checking problem directly, we might go one
step further with our calculus and try to attain MLs lack of a requirement for explicit
typing. We can achieve this by simply erasing all type annotations and then trying to
solve the seemingly harder problem of inferring types of completely naked terms.
Surprisingly, it is not known whether an eective type inference algorithm exists for
this calculus, even though the problem of partial polymorphic type inference, known
to be undecideable, seems as if it should be easier.
Fortunately, Hindley [Hin69] and Milner [Mil78] independently discovered a restricted
polymorphic type system that is almost as rich as that provided by our calculus, and
for which type inference is decidable. In other words, there exist certain terms in the
calculus presented above that one could argue are properly typed, but would not be
allowed in the Hindley-Milner system. The system still allows polymorphism, such as
exhibited by map dened earlier, and is able to infer the type of functions such as map
without any type signatures present. However, the use of such polymorphismis limited
to the scope in which map was dened. For example, the following program:
27
silly map f g
where f :: Int -> Int
g :: Char -> Char
map :: (a -> b) -> [a] -> [b]
silly m f g = (m f num_list, m g char_list)
[(e1,e2) is a tuple] results in a type error, since map is passed as an argument and
then instantiated in two dierent ways; i.e. once as type (Int -> Int) -> [Int] ->
[Int] and once as type (Char -> Char) -> [Char] -> [Char]. If map were in-
stantiated in several ways within the scope in which it was dened, or if m were only
instantiated in one way within the function silly, there would have been no problem.
This example demonstrates a fundamental limitation to the Hindley-Milner type
system, but in practice the class of programs that the system rejects is not large, and is
certainly smaller than that rejected by any existing type-checking algorithmfor conven-
tional languages in that, if nothing else, it allows polymorphism. Many other functional
languages have since then incorporated what amounts to a Hindley-Milner type system,
including Miranda and Haskell. It is beyond the scope of this article to discuss the de-
tails of type inference, but the reader may nd good pragmatic discussions in [Han]
and [DM82], and a good theoretical discussion in [Mil78].
As a nal comment we point out that an alternative way to gain polymorphism is to
introduce types as values and give them at least some degree of rst-class status (as
we did earlier for functions), for example allowing themto be passed as arguments and
returned as results. Allowing them to be passed as arguments only (and then used in
type annotations in the standard way) results in what is known as the polymorphic or
second-order typed lambda calculus. Girard [Gir72] and Reynolds [Rey74] discovered
and studied this type system independently, and it appears to have great expressive
power. However, it turns out to be essentially equivalent to the system we developed
earlier, and has the same diculties with respect to type inference. It is, nevertheless,
an active area of current research (see [CW85] and [Rey85] for good summaries of this
work).
2.6.2 MLs References
A reference is essentially a pointer to a cell containing values of a particular type; they
are created by the (pseudo-)function ref. For example, ref 5 evaluates to an integer
referencea pointer to a cell that is allowed to contain only integers, and in this case
having initial contents 5. The contents of a cell can be read using the prex operator
!. Thus if x is bound to ref 5 then !x returns 5.
The cell pointed to by a reference may be updated via assignment using the inx
operator :=. Continuing with the above example, x := 10, although an expression,
28
has the side-eect of updating the cell pointed to by x with the value 10. Subsequent
evaluations of !x will then return 10. Of course, to make the notion of subsequent
well-dened, it is necessary to introduce sequencing constructs; indeed ML even has
an iterative while construct.
References in ML amount to assignable variables in a conventional programming
language, and are only notable in that they can be included within a type structure
such as Hindley-Milners, and can be relegated to a minor role in a language that is
generally proclaimed as being functional. A proper treatment of references within a
Hindley-Milner type system can be found in [Tof88].
2.6.3 Modules
Modules in ML are called structures, and are essentially reied environments. The type
of a structure is captured in its signature, and contains all of the static properties of a
module that are needed by some other module that might use it. The use of one module
by another is captured by special functions called functors that map structures to new
structures. This capability is sometimes called a parameterized module or generic
package.
For example, a new signature called SIG (think of it as a new type) may be declared
by:
signature SIG =
sig
val x : int
val succ : int -> int
end
in which the types of two identiers have been declared, x of type int and succ of type
int->int. The following structure S (think of it as an environment) has the implied
signature SIG dened above:
structure S =
struct
val x = 5
val succ x = x+1
end
If we then dene the following functor F (think of it as a function from structures to
structures):
29
functor F(T:SIG) =
struct
val y = T.x + 1
val add2 x = T.succ(T.succ(x))
end
then the new structure declaration:
structure U = F(S)
is equivalent to having written:
structure U =
struct
val y = x + 1
val add2 x = succ(succ(x))
val x = 5
val succ x = x+1
end
except that the signature of U does not include bindings for x and succ (i.e. they are
hidden).
Although seemingly simple, the ML module facility has one very noteworthy feature:
Structures are (at least partially) rst-class in that functors take them as arguments
and return themas values. Amore conservative design (such as adopted in Miranda and
Haskell) might require all modules to be named, thus relegating them to second-class
status. Of course, this rst-class treatment has to be ended somewhere if type-checking
is to remain eective, and in ML that is manifested in the fact that structures can be
passed to functors only (for example they cannot be placed in lists), the signature dec-
laration of a functors argument is mandatory in the functor declaration, and functors
themselves are not rst-class.
It is not clear whether this almost-rst-class treatment of structures is worth the
extra complexity, but the ML module facility is certainly the most sophisticated of
those found in existing functional programming languages, and it is achieved with no
loss of static type-checking ability, including the fact that modules may be compiled
independently and later linked via functor application.
2.7 SASL, KRC, and Miranda
At the same time ML and FP were being developed, David Turner, rst at the Univer-
sity of St. Andrews and later at the University of Kent, was busy working on another
30
style of functional languages resulting in a series of three languages that characterize
most faithfully the modern school of functional programming ideas. More than any
other researcher, Turner argued eloquently for the value of lazy evaluation, higher-
order functions, and the use of recursion equations as a syntactic sugaring for the
lambda calculus [Tur81, Tur82]. Turners use of recurrence equations was consistent
with Landins argument ten years earlier, as well as Burges excellent treatise on recur-
sive programming techniques [Bur75] and Burstall and Darlingtons work on program
transformation [BD77]. But the prodigious use of higher-order functions and lazy eval-
uation, especially the latter, was something new, and was to become a hallmark of
modern functional programming techniques.
In the development of SASL (St. Andrews Static Language) [Tur76], KRC (Kent Recur-
sive Calculator) [Tur81], and Miranda
17
[Tur85], Turner concentrated on making things
easier on the programmer, and thus he introduced various sorts of syntactic sugar. In
particular, using SASLs syntax for equations gave programs a certain mathematical a-
vor, since equations were deemed applicable through the use of guards and Landins
notion of the o-side rule was revived. For example, this denition of the factorial
function:
fac n = 1, n=0
= n*fac(n-1), n>0
looks a lot like the mathematical version:
fac n =
1 if n = 0
nfac(n1) if n > 0
(In Haskell this program would be written with slightly dierent syntax as:
fac n | n==0 = 1
| n>0 = n*fac(n-1)
More on equations and pattern-matching may be found in Section 3.4.)
Another nice aspect of SASLs equational style is that it facilitates the use of higher-
order functions through currying. For example, if we dene:
add x y = x+y
then add 1 is a function that adds one to its argument.
KRC is an embellishment of SASL primarily through the addition of ZF expressions
(which were intended to resemble Zemelo-Frankel set abstraction and whose syntax
was originally suggested by John Darlington), as well as various other short-hands for
lists (such as [a..b] to denote the list of integers from a to b, and [a..] to denote
the innite sequence starting with a). For example (using Haskell syntax):
17
Miranda is one of the few (perhaps the only) functional languages to be marketed commercially.
31
[ x*x | x <- [1..100], odd(x) ]
is the list of squares of the odd numbers from 1 to 100, and is similar to:
x
2
x 1, 2, . . . , 100 odd(x)
except that the former is a list, the latter is a set. In fact Turner used the term set
abstraction as synonymous with ZF expression, but in fact both terms are somewhat
misleading since the expressions actually denote lists, not true sets. The more popular
current term is list comprehension,
18
which is what I will call them in the remainder
of this paper. As an example of the power of list comprehensions, here is a rather
concise and perspicuous denition of quicksort:
qs [] = []
qs (x:xs) = qs [y | y<-xs, y<x ] ++ [x] ++
qs [y | y<-xs, y>=x]
[++ is the inx append operator.]
Miranda is in turn an embellishment of KRC, primarily in its treatment of types:
it is strongly typed, using a Hindley-Milner type system, and it allows user dened
concrete and abstract datatypes (both of these ideas were presumably borrowed from
ML; see Sections 2.6 and 2.6.1). One interesting innovation in syntax in Miranda is its
use of sections (rst suggested by Richard Bird), which are a convenient way to convert
partially applied inx operators into functional values. For example, the expressions
(+), (x+), and (+x) correspond to the functions f, g, and h, respectively, dened by:
f x y = x+y
g y = x+y
h y = y+x
Partly because of the presence of sections, Miranda does not provide syntax for lambda
abstractions. (In contrast, the Haskell designers chose to have lambda abstractions, and
thus chose not to have sections.)
Turner was perhaps the foremost proponent of both higher-order functions and
lazy evaluation, although the ideas originated elsewhere. As I have done with data
abstraction, discussion of both of these topics is delayed until Sections 3.1 and 3.2,
respectively, where they are discussed in a more general context.
18
A term popularized by Phil Wadler.
32
2.8 Dataow Languages
In the area of computer architecture, beginning predominantly with Jack Dennis work
in the early 70s [DM74], there arose the notion of dataow, a computer architecture
organized solely around the data dependencies in a program, resulting in high degrees
of parallelism. Since data dependencies were paramount, and articial sequentiality
was objectionable, the languages designed to support such machines were essentially
functional languages, although historically they have been called dataow languages.
In fact they do have a fewdistinguishing features, typically reecting the idiosyncrasies
of the dataow architecture (just as imperative languages reect the von Neumann ar-
chitecture): they are typically rst-order (reecting the diculty in constructing clo-
sures in the dataow model), strict (reecting the data-driven mode of operation that
was most popular and easiest to implement), and in certain cases do not even allow
recursion (reecting Dennis original static dataow design, rather than, for example,
Arvinds dynamic tagged-token model [AG77, AK81]). A good summary of work on
dataow machines, at least through 1981, can be found in [TBH82]; more recently, see
[Veg84].
The two most important dataow languages developed during this era were Dennis
et al.s Val [AD79, Mcg82] and Arvind et al.s Id [AG82]. More recently, Val has evolved
into SISAL [MAGD83], and Id into Id Nouveau [NPA86]. The former has retained much
of the strict and rst-order semantics of dataow languages, whereas the latter has
many of the features that characterize modern functional languages.
Kellers FGL [KJRL80] and Davis DDN [Dav78] are also notable developments that
accompanied a urry of activity on dataow machines at the University of Utah in the
late 70s. Yet another interesting dataow language is Ashcroft and Wadges Lucid
[AW76a, AW76b, WA85], whose distinguishing feature is the use of identiers to rep-
resent streams of values (in a temporal, dataow sense), thus allowing the expression
of iteration in a rather concise manner. The authors also developed an algebra for
reasoning about Lucid programs.
2.9 Others
In the late 70s and early 80s a surprising number of other modern functional lan-
guages appeared, most in the context of implementation eorts. These included Hope
at Edinburgh University [BMS80], FEL at Utah [Kel82], Lazy ML (LML) at Chalmers [Aug84],
Al at Yale [Hud84], Ponder at Cambridge [Fai85], Orwell at Oxford [WM88], Daisy at
Indiana [Joh88], Twentel at the University of Twente [Kro87], and Tui at Victoria Uni-
versity [Bou88].
Perhaps the most notable of these languages was Hope, designed and implemented
by Rod Burstall, David MacQueen, and Ron Sannella at Edinburgh University [BMS80].
33
Their goal was to produce a very simple programming language which encourages
the production of clear and manipulable programs. Hope is strongly typed, allows
polymorphism, but requires explicit type declarations as part of all function denitions
(which also allowed a useful form of overloading). It has lazy lists, but otherwise is
strict. It also has a simple module facility. But perhaps the most signicant aspect
of Hope is its user-dened concrete datatypes and the ability to pattern-match against
them. ML, in fact, did not originally have these features; they were borrowed from
Hope in the design of SML.
To quote Bird and Wadler [BW88], this proliferation of functional languages was
a testament to the vitality of the subject, although by 1987 there existed so many
functional languages that there truly was a Tower of Babel, and something had to be
done! The funny thing was, the semantic underpinnings of these languages were fairly
consistent, and thus the researchers in the eld had very little trouble understanding
each others programs, so the motivation within the research community to standardize
on a language was not high.
Nevertheless, in September of 1987 a meeting was held at the FPCA Conference
in Portland, Oregon, to discuss the problems that this proliferation of languages was
creating. There was a strong consensus that the general use of modern, non-strict
functional languages was being hampered by the lack of a common language. Thus
it was decided that a committee should be formed to design such a language, pro-
viding faster communication of new ideas, a stable foundation for real applications
development, and a vehicle through which other people would be encouraged to learn
and use functional languages. The result of that committees eort was a purely func-
tional programming language called Haskell [HWe88], named after Haskell B. Curry,
and described in the next section.
2.10 Haskell
Haskell is a general purpose, purely functional programming language exhibiting many
of the recent innovations in functional (as well as other) programming language re-
search, including higher-order functions, lazy evaluation, static polymorphic typing,
user-dened datatypes, pattern-matching, and list comprehensions. It is also a very
complete language in that it has a module facility, a well-dened functional I/O system,
and a rich set of primitive datatypes, including lists, arrays, arbitrary and xed preci-
sion integers, and oating-point numbers. In this sense Haskell represents both the
culmination and solidication of many years of research on functional languagesthe
design was inuenced by languages as old as Iswim and as new as Miranda.
Haskell also has several interesting new features; most notably, a systematic treat-
ment of overloading, an orthogonal abstract datatype facility, a universal and purely
functional I/O system, and, by analogy to list comprehensions, a notion of array com-
prehensions.
34
Haskell is not a small language. The decision to emphasize certain features such as
pattern-matching and user-dened datatypes, and the desire for a complete and prac-
tical language that includes such things as I/O and modules, necessitates a somewhat
large design. The Haskell Report also provides a denotational semantics for both the
static and dynamic behavior of the language; it is considerably more complex than the
simple semantics dened in Section 3.5 for the lambda calculus, but then again one
wouldnt really want to program in as sparse a language as the lambda calculus.
Will Haskell become a standard? Will it succeed as a useful programming language?
Only time will tell. As with any other language development, it is not only the quality
of the design that counts, but also the ready availability of good implementations, and
the backing from vendors, government agencies, and researchers alike. At this date it
is too early to tell what role each of these factors will play.
I will end our historical development of functional languages here, without elabo-
rating on the details of Haskell just yet. Those details will surface in signicant ways in
the next section, where the most important features of modern function languages are
discussed; and in the following section, where more advanced ideas and active research
areas are discussed.
3 Distinguishing Features of Modern Functional Languages
The reader will recall that I chose to delay detailed discussion of four distinguishing
features of modern functional languageshigher-order functions, lazy evaluation, data
abstraction mechanisms, and equations/pattern-matching. Now that we have com-
pleted our study of the historical development of functional languages, we can return
to those features. Most of the discussion will center on howthe features are manifested
in Haskell, ML, and Miranda.
3.1 Higher-Order Functions
If functions are treated as rst-class values in a languageallowing themto be stored
in data structures, passed as arguments, and returned as resultsthey are referred to
as higher-order functions. I have not said too much about the utility of higher-order
functions thus far, although they exist in most of the functional languages that weve
discussed, including of course the lambda calculus. Their utility has in fact been argued
in many circles, including ones outside of functional programming, most notably the
Scheme community (see, for example, [ASS85]).
The main philosophical argument for higher-order functions is that functions are
values just like any others, so why not give them the same rst-class status? But
35
there are also compelling pragmatic reasons for wanting higher-order functions. Sim-
ply stated, the function is the primary abstraction mechanism over values, and thus
facilitating the use of functions increases the utility of that kind of abstraction.
As an example of a higher-order function, consider:
twice f x = f (f x)
which takes its rst argument, a function f, and applies it twice to its second ar-
gument, x. The syntax used here is important: twice as written is curried, meaning
that when applied to one argument it returns a function which then takes one more
argument, the second argument above. For example, the function add2:
add2 = twice succ
where succ x = x+1
is a function that will add 2 to its argument. Making function application associate
to the left facilitates this mechanism, since twice succ x is equivalent to (twice
succ) x so everything works out just ne.
In modern functional languages functions can be created in several ways. One way
is to name them using equations, as above, but another way is to create them directly
as lambda abstractions, thus rendering them nameless, as in the Haskell expression:
\x -> x+1
[in lambda calculus this would be written x.x+1] which is the same as the successor
function succ dened above. add2 can then be dened more succinctly as:
add2 = twice (\x -> x+1)
Froma pragmatic viewpoint we can understand the utility of higher-order functions
by analyzing the utility of abstraction in general. As we all know from introductory
programming, a function is an abstraction of values over some common behavior (an
expression). Limiting the values over which the abstraction occurs to non-functions
seems unreasonable; lifting that restriction results in higher-order functions. Hughes
makes a slightly dierent but equally compelling argument in [Hug84], where he em-
phasizes the importance of modularity in programming and argues convincingly that
higher-order functions increase modularity by serving as a mechanism for glueing
program fragments together. That glueing property comes not just from the ability to
compose functions, but also the ability to abstract over functional behavior as described
above.
As an example, suppose in the course of program construction we dene a function
to add together the elements of a list:
36
sum [] = 0
sum (x:xs) = add x (sum xs)
Then suppose we later dene a function to multiply the elements of a list:
prod [] = 1
prod (x:xs) = mul x (prod xs)
But now we notice a repeating pattern, and anticipate that we might see it again, so
we ask ourselves if we can possibly abstract the common behavior. In fact this is easy
to do: we note that add/mul and 0/1 are the variable elements in the behavior, and
thus we parameterize them; that is, we make them formal parameters, say f and init.
Calling the new function fold, the equivalent of sum/prod will be fold f init, and
thus we arrive at:
(fold f init) [] = init
(fold f init) (x:xs) = f x ((fold f init) xs)
where the parentheses around fold f init are used only for emphasis, and are oth-
erwise superuous.
From this we can now derive new denitions for sum and product:
sum = fold add 0
prod = fold mul 1
Of course now that the fold abstraction has been made, many other useful functions
can be dened, even something as seemingly unrelated as append:
append xs ys = fold (:) ys xs
[An inx operator may be passed as an argument by surrounding it in parentheses;
thus (:) is equivalent to \x y -> x:y.] This version of append simply replaces the
[] at the end of the list xs with the list ys.
It is easy to verify that the new denitions are equivalent to the old using simple
equational reasoning and induction. It is also important to note that in arriving at
the main abstraction we did nothing out of the ordinarywe are just applying classical
data abstraction principles in as unrestricted a way as possible, and that means allowing
functions to be rst-class citizens.
37
3.2 Non-Strict Semantics (Lazy Evaluation)
3.2.1 Fundamentals
The normal-order reduction rules of the lambda calculus are the most general in that
they are guaranteed to produce a normal form if in fact one exists (see Section 2.1). In
other words, they result in termination of the rewriting process most often, and the
strategy lies at the heart of the Church-Rosser Theorem. Furthermore, as argued earlier,
normal-order reduction allows recursion to be emulated with the Y combinator, thus
giving the lambda calculus the most powerful formof eective computability, captured
in Churchs Thesis.
Given all this, it is quite natural to consider using normal-order reduction as the
computational basis for a programming language. Unfortunately, normal-order reduc-
tion, implemented naively, is hopelessly inecient. To see why, consider this simple
normal-order reduction sequence:
(x. (+ x x)) ( 5 4)
(+ ( 5 4) ( 5 4))
(+ 20 ( 5 4))
(+ 20 20)
40
Note that the multiplication ( 5 4) is done twice. In the general case this could be
an arbitrarily large computation, and it could potentially be repeated as many times
as there are occurrences of the formal parameter (for this reason an analogy is often
drawn between normal-order reduction and call-by-name parameter passing in Algol).
In practice this can happen quite often, reecting the simple fact that results are often
shared.
One solution to this problem is to resort to something other than normal-order
reduction, such as applicative-order reduction, which for the above term yields the
following reduction sequence:
(x. (+ x x)) ( 5 4)
(x. (+ x x)) 20
(+ 20 20)
40
Note that the argument is evaluated before the -reduction is performed (similar to a
call-by-value parameter-passing mechanism), and thus the normal form is reached in
three steps instead of four, with no recomputation. The problem with this solution
is that it requires the introduction of a special reduction rule to implement recursion
(such as gained through the -rule for McCarthys conditional), and furthermore there
38
are examples where it does more work than normal-order reduction! For example,
consider:
applicative-order: normal-order:
(x. 1) ( 5 4) (x. 1) ( 5 4)
(x. 1) 20 1
1
or even worse (repeated from Section 2.1):
applicative-order: normal-order:
(x. 1) ((x. x x) (x. x x)) (x. 1) ((x. x x) (x. x x))
(x. 1) ((x. x x) (x. x x)) 1
.
.
.
which in the applicative-order case does not terminate! Despite these problems, most
of the early functional languages, including pure Lisp, FP, ML, Hope, all of the dataow
languages, and others, used applicative-order semantics.
19
In addition to overcoming
the eciency problem of normal-order reduction, applicative-order reduction could be
implemented with relative ease using the call-by-value compiler technology that had
been developed for conventional imperative programming languages.
Nevertheless, the appeal of normal-order reduction cannot be ignored. Returning
to lambda calculus basics, we can try to get to the root of the eciency problem, which
seems to be the following: reduction in the lambda calculus is normally described
as string reduction, which precludes any possibility of sharing. If instead we were
to describe it as a graph reduction process, perhaps sharing could be achieved. This
idea was rst suggested by Wadsworth in his PhD thesis in 1971 [Wad71, Chapter 4],
in which he outlined a graph reduction strategy that utilized pointers to implement
sharing. Using this strategy results in the following reduction sequence for the rst
example given earlier:
(x. (+ x x)) ( 5 4)
(+ )
( 5 4)
(+ )
20
40
which takes the same number of steps as the applicative-order reduction sequence.
19
Actually this is not quite truemost implementations of these languages use an applicative-order
reduction strategy for the top-level redices only, thus yielding what is known as a weak head normal
form. This strategy turns out to be easier to implement than complete applicative-order reduction, and
also permits certain versions of the Y combinator to be implemented without special rules. See [Bur75]
for an example of this using Landins SECD machine.
39
We will call an implementation of normal-order reduction in which recomputation
is avoided lazy evaluation (another term often used is call-by-need). Its key feature is
that arguments in function calls are evaluated at most once. It possesses the full power
of normal-order reduction while being more ecient than applicative-order reduction
in that at most once sometimes amounts to no computation at all!
Despite the appeal of lazy evaluation and this apparent solution to the eciency
problem, it took a good ten years more for researchers to discover ways to implement
it eciently compared to conventional programming languages. The chief problemhas
to do with the diculty in implementing lazy graph reduction mechanisms on conven-
tional computers, which seemto be better suited to call-by-value strategies. Simulating
the unevaluated portions of the graph in a call-by-value environment amounts to e-
ciently implementing closures, or thunks, which have some inherent, non-trivial costs
[BHY88]. It is beyond the scope of this paper to discuss the details of these implemen-
tation concerns, but see Peyton-Jones [PJ87] for an excellent summary.
Rather than live with conventional computers, one could alternatively build spe-
cialized graph reduction or dataow hardware, but so far this has not resulted in any
practical, much less commercially available, machines. Nevertheless, this work is quite
promising, and good summaries of work in this area can be found in articles by Tre-
leaven et al. [TBH82] and Vegdahl [Veg84], both of which are reprinted in [Tha87].
3.2.2 Expressiveness
Assuming that we can implement lazy evaluation eciently (current practice is consid-
ered acceptably good), we should return to the question of why we want it in the rst
place. Previously we argued on philosophical groundsit is the most general evalua-
tion policybut is lazy evaluation useful to programmers in practice? The answer is
an emphatic yes, which I hope to show via a two-fold argument.
First of all, lazy evaluation frees a programmer from concerns about evaluation
order. The fact is, programmers are generally concerned about the eciency of their
programs, and thus they prefer not evaluating things that are not absolutely necessary.
As a simple example, suppose we may need to know the greatest common divisor of
b and c in some computation involving x. In a modern functional language we might
write:
f a x
where a = gcd b c
without worrying about a being evaluated needlesslyif in the computation of f a x
the value of a is needed, it will be computed, otherwise not. If we were to have written
this program in Scheme, for example, we might try:
40
(let ( (a (gcd b c)) )
(f a x))
which will always evaluate a. Knowing that f doesnt always need that value, and being
concerned about eciency, we may decide to rewrite this as:
(let ( (a (delay (gcd b c))) )
(f a x))
which requires modifying f so as to force its rst argument. Alternatively we could
just write (f b c x), which requires modifying f so as to compute the gcd internally.
Both of these solutions are severe violations of modularity, and they arise out of the
programmers concern about evaluation order. Lazy evaluation eliminates that concern
and preserves modularity.
The second argument for lazy evaluation is perhaps the one more often heard:
the ability to compute with unbounded (innite) data structures. The idea of lazily
evaluating data structures was rst proposed by Vuillemin [Vui74], but similar ideas
were developed independently by Henderson and Morris [HM76], and Friedman and
Wise [FW76]. In a later series of papers [Tur81, Tur82] Turner provides a strong ar-
gument for using lazy lists, especially when combined with list comprehensions (see
Section 2.7) and higher-order functions (see Section 3.1). Aside from Turners elegant
examples, Hughes presents an argument [Hug84] based on facilitating modularity in
programming, where, along with higher-order functions, lazy evaluation is described
as a way to glue pieces of programs together.
The primary power of lazily evaluated data structures comes from its utility in sep-
arating data from control. The idea is that a programmer should be able to describe
a specic data structure without worrying about how it gets evaluated. Thus, for ex-
ample, one could describe the sequence of natural numbers by the following simple
program:
nats = 0 : map succ nats
or alternatively by:
numsfrom n = n : numsfrom (n+1)
nats = numsfrom 0
These are examples of innite lists, or streams, and in a language that did not support
lazy evaluation would cause the program to diverge. With lazy evaluation these data
structures are only evaluated as they are needed, on demand. For example, we could
dene a function that lters out only those elements satisfying a property p:
41
filter p (x:xs) = if (p x) then (x:rest) else rest
where rest = filter p xs
in which case filter p nats could be written knowing that the degree of the lists
computation will be determined by its contexti.e. the consumer of the result. Thus
filter has no operational control within it, and can be combined with other functions
in a modular way. For example one could compose it with a function to square each
element in a list:
map (\x->x*x) . filter p
[ . is the inx composition operator.] This kind of modular result, in which data
is separated from control, is one of the key features of lazy evaluation. Many more
examples of this kind may be found in [Hug84].
3.3 Data Abstraction
Independently of the development of functional languages there has been considerable
work on data abstraction in general, and on strong typing, user-dened datatypes, and
type checking in particular. Some of this work has also taken on a theoretical avor,
not only in foundational mathematics where logicians have used types to resolve many
famous paradoxes, but also in formal semantics where types aid our understanding of
programming languages.
Fueling the theoretical work are two signicant pragmatic advantages of using data
abstraction and strong typing in ones programming methodology. First, data abstrac-
tion improves the modularity, security, and clarity of programs. Modularity is improved
because one can abstract away from implementation (i.e. representation) details; secu-
rity is improved because interface violations are automatically prohibited; and clarity
is improved because data abstraction has an almost self-documenting avor.
Second, strong static typing helps in debugging since one is guaranteed that if a pro-
gram compiles successfully that no error can occur at run-time due to type violations.
It also leads to more ecient implementations, since it allows one to eliminate most
run-time tag bits and type testing. Thus there is little performance penalty for using
data abstraction techniques.
Of course, these issues are true for any programming language, and for that reason
a thorough treatment of types and data abstraction is outside the scope of this survey;
the reader may nd an exellent summary in [CW85]. The basic idea behind the Hindley-
Milner type system was discussed in Section 2.6.1. I will concentrate in this section on
how data abstraction is manifest in modern functional languages.
42
3.3.1 Concrete Datatypes
As mentioned earlier, there is a strong argument for wanting language features that
facilitate data abstraction, whether or not the language is functional. In fact such
mechanisms were rst developed in the context of imperative languages such as Simula,
Clu, Euclid, and others. It is only natural that they be included in functional languages.
ML, as we mentioned, was the rst functional language to do this, but many others
soon followed suit.
In this subsection I will describe concrete (or algebraic) datatypes as well as type
synonyms. I will use Haskell syntax, but the ideas are essentially identical (at least
semantically) to those used in ML and Miranda.
New algebraic datatypes may be dened along with their constructors using data
declarations, as in the following denitions of lists and trees:
data List a = Nil | Cons a (List a)
data Tree b = Empty | Node b (List (Tree b))
The identiers List and Tree are called type constructors, and the identiers a and
b are called type variables which are implicitly universally quantied over the scope
of the data declaration. The identiers Nil, Cons, Empty, and Node are called data
constructors, or just constructors, with Nil and Empty being nullary constructors. [Note
that both type constructors and data constructors are capitalized, so that the latter can
be used without confusion in pattern-matching (as discussed in the next section) and
to avoid confusion with type variables (such as a and b in the above example).]
List and Tree are called type constructors since they construct types from other
types. For example, Tree Ints is the type of trees of integers. Reading from the data
declaration for Tree, we see then that a tree of integers is either Empty, or a Node
containing a list of more trees of integers.
We can now see that the previously given type signature for map:
map :: (a -> b) -> [a] -> [b]
is equivalent to:
map :: (a -> b) -> (List a) -> (List b)
That is, [...] in a type expression is just syntax for application of the type con-
structor List. Similarly, we can think of -> as an inx type constructor that creates
the type of all functions from its rst argument (a type) to its second (also a type).
43
[A useful property to note is the consistent syntax used in Haskell for expressions
and types. Specically, if T
i
is the type of expression or pattern e
i
, then the expressions
\e
1
->e
2
, [e
1
], and (e
1
,e
2
) have the types T
1
->T
2
, [T
1
], and (T
1
,T
2
), respectively.]
Instances of these new types are built simply by using the constructors. Thus
Empty is an empty tree, and Node 5 Empty is a very simple tree of integers with one
element. The type of the instance is inferred via the same type inference mechanism
that infers types of polymorphic functions, as described previously.
Dening new concrete datatypes is fairly common not only in functional languages
but also in imperative languages, although the polymorphismoerred by modern func-
tional languages makes it all the more attractive.
Type synonyms are a way of creating new names for types, such as in the following:
type Intree = Tree Ints
type Flattener = Intree -> [Ints]
Note that Intree is used in the denition of Flattener. type declarations do not
introduce new types (as data declarations do), but rather are a convenient way to
introduce new names (i.e. synonyms) for existing types.
3.3.2 Abstract Datatypes
Another idea in data abstraction originating in imperative languages is the notion of
an abstract datatype (ADT) in which the details of the implementation of a datatype
are hidden from the users of that type, thus enhancing modularity and security. The
traditional way to do this is exemplied by MLs ADT facility and emulated in Miranda.
Although the Haskell designers chose a dierent approach to ADTs (described below),
the following example of a queue ADT is written as if Haskell had MLs kind of ADTs,
using the keyword abstype:
abstype Queue a = Q [a]
where first (Q as) = last as
isempty (Q []) = True
isempty (Q as) = False
...
The main point is that the functions first, isempty, etc. are visible in the scope of
the abstype declaration, but the constructor Q, including its type, is not. Thus a user of
the ADT has no idea whether queues are implemented as lists (as shown here) or some
other data structure. The advantage of this, of course, is that one is free to change the
representation type without fear of breaking some other code that uses the ADT.
44
3.3.3 Haskells Orthogonal Design
In Haskell a rather dierent approach was taken to ADTs. The observation was made
that the main dierence between a concrete and abstract datatype was that the lat-
ter had a certain degree of information hiding built into it. So instead of thinking of
abstract datatypes and information hiding as going hand-in-hand, the two were made
orthogonal components of the language. More specically, concrete datatypes were
made the only data abstraction mechanism, and to that an expose declaration was
added to control information hiding, or visibility.
For example, to get the eect of the earlier denition of a queue, one would write:
expose Queue, first, isempty
from data Queue a = Q [a]
first (Q as) = last as
isempty (Q []) = True
isempty (Q as) = False
...
Since Q is not explicitly listed in the expose declaration, it becomes hidden from the
user of the ADT.
The advantage of this approach to ADTs is more exibility. For example, suppose
we also wish to hide isempty, or perhaps some auxiliary function dened in the nested
scope. This is trivially done with the orthogonal design but much harder with the ML
design as described so far. Indeed, to alleviate this problem the ML designers provided
an additional construct, a local declaration, with which one can hide local declarations.
Another advantage of the orthogonal design is that the same mechanism can be used
at the top level of a module to control visibility of the internals of the module to the
external world. In other words, the expose mechanism is very general, and can be
nested. Haskell utilizes a conservative module system that relies on this capability.
A disadvantage of the orthogonal approach is that if the most typical ADT scenario
only requires hiding the representation type, the user will have to think through the
details in each case rather than having the hiding done automatically by using abstype.
3.4 Equations and Pattern-Matching
One of the programming methodology attributes that is strongly encouraged in the
modern school of functional programming is the use of equational reasoning in the
design and construction of programs. The lack of side eects accounts for the primary
ability to apply equational reasoning, but there are syntactic features that can facilitate
it as well. Using equations as part of the syntax is the most obvious of these, but along
45
with this goes pattern-matching, in which one can write several equations in dening
the same function, only one of which is presumably applicable in a given situation. Thus
modern functional languages have tried to maximize the expressiveness of pattern-
matching.
At rst blush, equations and pattern-matching seem fairly intuitive and relatively
innocuous. Indeed, we have already given many examples that use pattern-matching,
without having said much about the details of how it works. But in fact pattern-
matching can have surprisingly subtle eects on the semantics of a language, and thus
should be carefully and precisely dened.
3.4.1 Pattern-Matching Basics
Pattern-matching should actually be viewed as the primitive behavior of a case expres-
sion, which has the general form:
case e of
pat1 -> e1
pat2 -> e2
...
patn -> en
Intuitively, if the structure of e matches pati then the result of the case expression is
ei. A set of equations of the form:
f pat1 = e1
f pat2 = e2
...
f patn = en
can then be thought of as shorthand for:
f = \x -> case x of
pat1 -> e1
pat2 -> e2
...
patn -> en
Despite this translation, for convenience I will use the equational syntax in the remain-
der of this section.
The question to be asked rst is just what the pattern-matching process consists
of. For example, what exactly are we pattern-matching against? One idea that seems
reasonable is to allowone to pattern-match against constants and data structures. Thus
fac can be dened by:
46
fac 0 = 1
n = n*fac(n-1)
[The tick mark in the second equation is an abbreviation for fac.] But note that this
relies on a top-to-bottom reading of the program, since (fac 0) actually matches
both equations. We can remove this ambiguity by adding a guard (recall the discussion
in Section 2.7), which in Haskell looks like:
fac 0 = 1
n | n>0 = n*fac(n-1)
As we have already demonstrated through several examples, it is also reasonable to
pattern-match against lists:
length [] = 0
(x:xs) = 1 + length xs
and for that matter any data structure, including user-dened ones:
data Tree2 a = Leaf a | Branch (Tree2 a) (Tree2 a)
fringe (Leaf x) = [x]
(Branch left right) = fringe left ++ fringe right
where ++ is the inx append operator.
Another idea that seems desirable is the ability to repeat formal parameters on the
left-hand-side to denote that arguments in those positions must have the same value,
as in the second line of the following denition:
member x [] = False
x (x:xs) = True
x (y:xs) = member x xs
[This is not legal Haskell syntax, since such repetition of identiers is not allowed.]
However, care must be taken with this approach since in something like:
alleq [x,x,x] = True
y = False
it is not clear in what order the elements of the list are to be evaluated. For exam-
ple, if they are evaluated left-to-right then alleq [1,2,bot], where bot is any non-
terminating computation, will return False, whereas with a right-to-left order the pro-
gram will diverge. One solution that would at least guarantee consistency in this ap-
proach is to insist that all three positions are evaluated, so that the program diverges
if any of the arguments diverge.
47
In general the problemof what gets evaluated, and when, is perhaps the most subtle
aspect of reasoning about pattern-matching, and suggests that the pattern-matching
algorithm be fairly simple so as not to mislead the user. Thus in Haskell the above
repetition of identiers is disallowedequations must be linearbut some functional
languages allow it (for example Miranda and Al [Hud84]).
A particularly subtle version of this problem is captured in the following example.
Consider these denitions:
data Silly a = Foo a | Other
bar (Foo x) = 0
Other = 1
Then a call bar bot will diverge, since bar must be strict in order that it can distinguish
between the two kinds of arguments that it might receive. But now consider this small
modication:
data Silly a = Foo a
bar (Foo x) = 0
Then a call bar bot seems like it should return 0, since bar need not be strictit
can only receive one kind of argument, and thus does not need to examine it unless
it is needed in computing the result, which in this case it is not. Given the goal of
a language that is lazy as possible, this seems like the right solution, and was the
approach adopted in Haskell.
Two useful discussions on the subject of pattern-matching can be found in [Aug85]
and [Wad].
3.4.2 Connecting Equations
Let us now turn to the more global issue of how the individual equations are connected
together as a group. As mentioned earlier, one simple way to do this is give the equa-
tions a top-to-bottom priority, so that in:
fac 0 = 1
n = n*(fac(n-1))
the second equation is tried only after the rst one has failed. This is the solution
adopted in many functional languages, including Haskell and Miranda.
An alternative method is to insist that the equations be disjoint, thus rendering
the order irrelevant. One signicant motivation for this is the desire to reason about
48
the applicability of an equation independently of the others, thus facilitating equational
reasoning. The question is, howcan one guarantee disjointness? For equations without
guards, the disjointness property can be determined statically; i.e. by just examining
the patterns. Unfortunately, when unrestricted guards are allowed, the problem obvi-
ously becomes undecidable, since it amounts to determining the equality of arbitrary
recursive predicates. This in turn can be solved by resolving the guard disjointness at
run-time. On the other hand, this solution ensures correctness only for values actually
encountered at run-time, and thus the programmer might apply equational reasoning
erroneously to as yet un-encountered values!
The two ideas could also be combined by providing two dierent syntaxes for join-
ing equations. For example, using the hypothetical keyword else (not valid in Haskell):
sameShallowStructure [a] [c] = True
[a,b] [c,d] = True
else
x y = False
The rst two equations would be combined using a disjoint semantics; together they
would then be combined with the third using a top-to-bottomsemantics. Thus the third
equation acts as an otherwise clause in the case that the rst two fail. A design of this
sort was considered for Haskell early on, but the complexities of disjointness, especially
in the context of guards, were considered too great and the design was eventually
rejected.
3.4.3 Argument Order
In the same sense that it is desireable to have the order of equations be irrelevant,
it is also desireable to have the order of arguments be irrelevant. In exploring this
possibility, consider rst the functions f and g dened by:
f 1 1 = 1
f 2 x = 2
g 1 1 = 1
g x 2 = 2
which dier only in the order of their arguments. Nowconsider what f 2 bot should
evaluate to. Clearly the equations for f are disjoint, clearly the expression matchs only
the second equation, and since we want a non-strict language it seems the answer
should clearly be 2. For a compiler to achieve this it must always evaluate the rst
argument to f rst.
49
Now consider the expression g bot 2by the same argument given above the
result should also be 2, but now the compiler must be sure to always evaluate the
second argument to g rst. Can a compiler always determine the correct order in which
to evaluate its arguments?
To help answer this question, rst consider this intriguing example (due to Berry
[Ber78]):
f 0 1 x = 1
f 1 x 0 = 2
f x 0 1 = 3
Clearly these equations are disjoint. So what is the value of f 0 1 bot? Desired
answer: 1. And what is the value of f 1 bot 0? Desired answer: 2. And what is the
value of f bot 0 1? Desired answer: 3. But now the most dicult question: In what
order should the arguments be evaluated? If we evaluate the third one rst, then the
answer to the rst question cannot be 1. If we evaluate the second one rst, then the
answer to the second question cannot be 2. If we evaluate the rst one rst, then the
answer to the third question cannot be 3. In fact there is no sequential order that will
allow us to answer these three questions the way we would likesome kind of parallel
evaluation is required.
This subtle problem is solvable in several ways, but they all require some kind
of compromiseinsisting on a parallel (or pseudo-parallel) implementation, rejecting
certain seemingly valid programs, making equations more strict than one would like,
or giving up on the independence of the order of evaluation of arguments. In Haskell
the last solution was chosenperform a left-to-right evaluation of the arguments
because it presented the simplest semantics to the user, which was judged to be more
important than making the order of arguments irrelevant. Another way to explain this
is to think of equations as syntax for nested lambda expressions, in which case one
might not expect symmetry with respect to the arguments anyway.
3.5 Formal Semantics
Simultaneously with work on functional languages Scott, Strachey, and others were
busy establishing the foundations of denotational semantics, now the most widely
used tool for describing the formal semantics of programming languages. There was
a close connection between this work and functional languages primarily because the
lambda calculus served as one of the simplest programming languages with enough
useful properties to make its formal semantics interesting. In particular, the lambda
calculus had a notion of self-application, which implied that certain domains had to
contain their own function spaces. That is, it required a domain D that was a solution
50
to the following domain equation:
D = D D
At rst this seems impossiblesurely there are more functions from D into D than
there are elements in Dbut Scott was able to show that indeed such domains existed,
as long as one was willing to restrict the allowable functions in certain (quite reasonable)
ways, and by treating =as an isomorphismrather than an equality [Sco70]. Scotts work
served as the mathematical foundation for Stracheys work [MS76] on the denotational
semantics of programming languages; see [Sto77] and [Sch85] for thorough treatments.
Denotational semantics and functional programming have close connections, and
the functional programming community emphasizes the importance of formal seman-
tics in general. For completeness, and to show how simple the denotational semantics
of a functional language can be, we give the semantics of the recursive lambda calculus
with constants dened in Section 2.3.
Bas = Int +Bool + Basic values
D = Bas +(D D) Denotable values
Env = Id D Environments
1 : Exp Env D
J : Con D
1[[x]]env = env[[x]]
1[[c]]env = J[[c]]
1[[e
1
e
2
]]env = (1[[e
1
]]env) (1[[e
1
]]env)
1[[x.e]]env = v.1[[e]]env[v/x]
1[[e where x
1
= e
1
; ; x
n
= e
n
]]env
= 1[[e]]env
where env
= x env
. env[ (1[[e
1
]]env
) / x
1
,
.
.
.
(1[[e
n
]]env
) / x
n
]
This semantics is relatively simple, but in moving to a more complex language such
as Miranda or Haskell the semantics can become signicantly more complex, due to
the many syntactic features that make the languages convenient to use. In addition,
one must state precisely the static semantics as well, including type checking, pattern-
matching usage, etc. Once all is said and done, the formal semantics of Haskell con-
sumes about 20 pages.
51
4 Advanced Features and Active Research Areas
Some of the most recent ideas in functional language design are new enough that they
should be regarded as on-going research. Nevertheless many of themare sound enough
to have been included in current language designs. In this section we will explore
a variety of such ideas, beginning with some of the innovative ideas in the Haskell
design. Some of the topics have a theoretical avor, such as extensions of the Hindley-
Milner type system; some have a pragmatic avor, such as expressing non-determinism,
ecient arrays, and I/O; and some involve the testing of new application areas, such as
parallel and distributed computation. All in all studying these topics should provide
insight into the goals of functional programming as well as some of the problems in
achieving those goals.
4.1 Overloading
The kind of polymorphism exhibited by the Hindley-Milner type system is what Stra-
chey called parametric polymorphism, to distinguish it fromanother kind that he called
ad hoc polymorphism, or overloading. The two can be distinguished in the following
way: a function with parametric polymorphism does not care what type certain of its
arguments have, and thus it behaves identically regardless of the type. In contrast,
a function with ad hoc polymorphism does care, and in fact may behave dierently
for dierent types. Stated another way, ad hoc polymorphism is really just a syntac-
tic device for overloading a particular function name or symbol with more than one
meaning.
For example, the function map dened earlier exhibits parametric polymorphism,
and has typing:
map :: (a -> b) -> [a] -> [b]
Regardless of the kind of list given to map it behaves in the same way. In contrast,
consider the function + which we normally wish to behave dierently for integer and
oating point numbers, and not at all (i.e. be a static error) for non-numeric arguments.
Another common example is the function == (equality) which certainly behaves dier-
ently when comparing the equlity of two numbers versus, say, two lists.
Ad hoc polymorphism is normally (and I suppose appropriately!) treated in an ad
hoc manner. Worse, there is no accepted convention for doing this; indeed ML, Miranda,
and Hope all do it dierently. Recently, however, a uniform treatment for handling
overloading was discovered independently by Kaes [Kae88] (in trying to generalize MLs
ad hoc equality types) and Wadler and Blott (as part of the process of dening Haskell).
BelowI will describe the solution as adopted in Haskell; details may be found in [WB89].
52
The basic idea is introduce a notion of type classes that capture a collection of
overloaded operators in a consistent way. A class declaration is used to introduce a
new type class and the overloaded operators that must be supported by any type that
is an instance of that class. An instance declaration declares that a certain type is an
instance of a certain class, and thus included in the declaration are the denitions of
the overloaded operators instantiated on the named type.
For example, say that we wish to overload + and negate on types Int and Float.
To do so, we introduce a new type class called Num:
class Num a where
(+) :: a -> a -> a
negate :: a -> a
This declaration may be read a type a belongs to the class Num if there are (overloaded)
functions + and negate, of the appropriate types, dened on it.
We may then declare Int and Float to be instances of this class, as follows:
instance Num Int where
x + y = addInt x y
negate x = negateInt x
instance Num Float where
x + y = addFloat x y
negate x = negateFloat x
[Note how inx operators are dened; Haskells lexical syntax prevents ambiguities.]
where addInt, negateInt, addFloat, and negateFloat are assumed in this case to
be pre-dened functions, but in general could be any user-dened function. The rst
declaration above may be read Int is an instance of the class Num as witnessed by
these denitions of + and negate.
Using type classes one can thus treat overloading in a consistent, arguably elegant,
way. Another nice feature is that type classes naturally support a notion of inheritance.
For example, we may dene a class Eq by:
class Eq a where
(==) :: a -> a -> Bool
Given this class, we would certainly expect all members of the class Num, say, to have
== dened on them. Thus the class declaration for Num could be changed to:
class Eq a => Num a where
(+) :: a -> a -> a
negate :: a -> a
53
which can be read as only members of the class Eq may be members of the class Num,
and a type a belongs to the class Num if ... <as before>. Given this class declaration,
instance declarations for Num must include a denition of ==, as in:
instance Num Int where
x + y = addInt x y
negate x = negateInt x
x == y = eqInt x y
The Haskell Report uses this inheritance mechanism to dene a very rich hierarchical
numeric structure which reects fairly well a mathematicians view of numbers.
The traditional Hindley-Milner type system is extended in Haskell to include type
classes. The resulting type system is able to verify that the overloaded operators do
have the appropriate type. However, it is possible (but not likely) for ambiguous situa-
tions to arise, which in Haskell result in type error but can be reconciled explicitly by
the user (see [HWe88] for the details).
4.2 Purely Functional Yet Universal I/O
To many the notion of I/O conjures an image of state, side-eects, and sequencing.
Is there any hope at achieving purely functional, yet universal, and of course ecient
I/O? Suprisingly, the answer is yes. Perhaps even more surprising is that over the years
there have emerged not one, but two seemingly very dierent solutions:
The lazy stream model, in which temporal events are modelled as lists, whose
lazy semantics mimics the demand-driven behavior of processes.
The continuation model in which temporality is modelled via explicit continua-
tions.
Although papers have been written advocating both solutions, and indeed they are
very dierent in style, the two solutions turn out to be exactly equivalent in terms of
expressiveness; in fact there is an almost trivial translation from one to the other. The
Haskell I/O system takes advantage of this fact and provides a unied framework that
supports both styles. The specic I/O operations available in each style are identical
what diers is the way they are expressedand thus programs in either style may be
combined with a well-dened semantics. In addition, although certain of the primitives
rely on non-deterministic behavior in the operating system, referential transparency is
still retained internal to a Haskell program.
In this section the two styles will be described as they appear in Haskell, together
with the translation of one in terms of the other. Details of the actual Haskell design
54
------- ------------ -------
| merge |------->| operating |------->| split |
------- | system | -------
| | | ------------ | | |
| | | | | |
|...| | | |...|
| | | ---------- | | |
| | ----------| program1 |<--------- | |
| | ---------- | |
| | | |
| | ---------- | |
| ------------| program2 |<----------- |
stream of| ---------- |stream of
requests | . |responses
| . |
| . |
| ---------- |
----------------| programn |<---------------
----------
Figure 1: Functional I/O
may be found in [HWe88], and a good discussion of the tradeos between the styles,
including examples, may be found in [HS88]. ML, by the way, uses an imperative, refer-
entially opaque, form of I/O (perhaps not surprising given the presence of references);
Miranda uses a rather restricted form of the stream model; and Hope uses the contin-
uation model but with a strict (i.e. call-by-value) semantics.
To begin, we can paint an appealing functional view of a collection of programs ex-
ecuting within an operating system (OS) as shown in Figure 1. With this view programs
are assumed to communicate with the OS via messagesprograms issue requests to
the OS and receive responses from the OS.
Ignoring for now the OS itself as well as the merge and split operations, a pro-
gram can be seen as a function from a stream (i.e. list) of responses to a stream of
requests. Although the above picture is quite intuitive, this latter description may
seem counterintuitivehow can a program receive a list of responses before it has
generated any requests? But remember that we are using a lazy (i.e. non-strict) lan-
guage, and thus the program is not obliged to examine any of the responses before it
issues its rst request. This application of lazy evaluation is in fact a very common
style of programming in functional languages.
Thus a Haskell program engaged in I/O is required to have type Behavior, where:
55
type Behavior = [Response] -> [Request]
[Recall from Section 3.3 that [Response] is the type consisting of lists of values of
type Response.] The main operational idea is that the nth response is the reply of the
operating system to the nth request.
For simplicity we will assume that there are only two kinds of requests and three
kinds of responses, as dened below:
data Request = ReadFile Name | WriteFile Name Contents
data Response = Success | Return Contents | Failure ErrorMsg
type Name = String
type Contents = String
type ErrorMsg = String
[This is a subset of the requests available in Haskell.]
As an example, given this request list:
[ ..., WriteFile fname s1, Readfile fname, ... ]
and the corresponding response list:
[ ..., Success, Return s2, ... ]
then s1 == s2, unless there were some intervening external eect.
In contrast, the continuation model is normally characterized by a set of transac-
tions. Each transaction typically takes a success continuation and a failure continuation
as arguments. These continuations in turn are simply functions that generate more
transactions. For example:
data Transaction = ReadFile Name FailCont RetCont
| WriteFile Name Contents FailCont SuccCont
| Done
type FailCont = ErrorMsg -> Transaction
type RetCont = Contents -> Transaction
type SuccCont = Transaction
[In Haskell the transactions are actually provided as functions rather than construc-
tors; see below.] The special transaction Done represents program termination. These
declarations should be compared with those for the stream model given earlier.
Returning to the simple example given earlier, the request and response list are no
longer separate entities since their eect is interwoven into the continuation struc-
ture, yielding something like:
56
WriteFile fname s1 exit
(ReadFile fname exit
(\s2 -> ...) )
where exit errmsg = Done
in which case, as before, we would expect s1 == s2 in the absence of external eects.
This is essentially the way I/O is handled in Hope.
Although these two styles seem very dierent, there is a very simple translation of
the continuation model into the stream model. In Haskell, instead of dening the new
datatype Transaction, a set of functions are dened that accomplish the same task,
but that are really stream transformers in the request/response style. In other words,
the type Transaction should be precisely the type Behavior, and should not be a new
datatype at all. Thus we arrive at:
readFile :: Name -> FailCont -> RetCont -> Behavior
writeFile :: Name -> Contents -> FailCont -> SuccCont -> Behavior
done :: Behavior
type FailCont = ErrorMsg -> Behavior
type RetCont = Contents -> Behavior
type SuccCont = Behavior
readFile name fail succ resps =
(ReadFile name) : case (head resps) of
Return contents -> succ contents (tail resps)
Failure msg -> fail msg (tail resps)
writeFile name contents fail succ resps =
(WriteFile name contents) : case (head resps) of
Success -> succ (tail resps)
Failure msg -> fail msg (tail resps)
done resps = []
This pleasing and very ecient translation allows one to write Haskell programs in
either style and mix them freely.
The complete design, of course, includes a fairly rich set of primitive requests be-
sides ReadFile and WriteFile, including a set of requests for communicating through
channels, which include things such as standard input and output. One of these re-
quests, in fact, takes a list of input channel names and returns their non-deterministic
merge. Because the merge itself is implemented in the operating system, referential
transparency is retained within a Haskell program.
Another useful aspect of the Haskell design is that it includes a partial (but rigorous)
specication of the behavior of the OS itself. For example, it introduces the notion of
57
an agent that consumes data on output channels and produces data on input channels.
The user is then modelled as an agent that consumes standard output and produces
standard input. This particular agent is required to be strict in the standard output,
corresponding to the notion that the user reads the terminal display before typing
at the keyboard! No other language design that I am aware of has gone this far in
specifying the I/O system with this degree of precision; it is usually left implicit in
the specication. It is particularly important, however, in this context because the
proper semantics relies critically on how the OS consumes and produces the request
and response lists.
To conclude this section I will show two Haskell programs that perform the follow-
ing task: prompt the user for the name of a le, read and echo the le name, and then
look up and display the contents of the le on standard-output. The rst version uses
the stream model, the second the continuation model.
main resps =
[ AppendChannel "stdout" "please type a filename\CR\",
if (resps!!1 == Success) then (ReadChannel "stdin"),
AppendChannel "stdout" fname,
if (resps!!3 == Success) then (ReadFile fname),
AppendChannel "stdout" (case resps!!4 of
Failure msg -> "cant open"
Return file_contents -> file_contents)
] where fname = case resps!!2 of
Return user_input -> get_line user_input
[The operator !! is the list selection operator; thus xs!!n is the nth element in the list
xs.]
main = appendChannel "stdout" "please type a filename\CR\" exit
(readChannel "stdin" exit (\user_input ->
appendChannel "stdout" fname exit
(readFile fname (\msg -> appendChannel "stdout" "cant open" exit done)
(\contents ->
appendChannel "stdout" contents exit done))
where fname = get_line user_input))
exit msg = done
4.3 Arrays
As it turns out, arrays can be expressed rather nicely in a functional language, and in
fact all of the arguments about mathematical elegance fall in line when using arrays.
58
This is especially true of program development in scientic computation, where text-
book matrix algebra can often be translated almost verbatiminto a functional language.
The main philosophy is to treat the entire array as a single entitiy dened declaratively,
rather than as a place-holder of values that is updated incrementally. This in fact is
the basis of the APL philosophy (see Section 2.4), and some researchers have concen-
trated on combining functional programming ideas with those from APL [Tu86, TP86].
The reader may nd good general discussions of arrays in functional programming
languages in [Wad86, Hud86a, Wis87]. In the remainder of this section I will describe
Haskells arrays, which originated from some ideas in Id Nouveau [NPA86].
Haskell has a family of multi-dimensional non-strict immutable arrays whose spe-
cial interaction with list comprehensions provides a convenient array comprehension
syntax for dening arrays monolithically. As an example, here is how to dene a vector
of squares of the integers from 1 to n:
a = array (1,n) [ (i,i*i) | i <- [1..n] ]
The rst argument to array is a tuple of bounds, and thus this array has size n and is
indexed from 1 to n. The second argument is a list of index/value pairs, and is written
here as a conventional list comprehension. The ith element of an array a is written
a!i, and thus in the above case we have that a!i = i*i.
There are several useful semantic properties of Haskells arrays. First, they can be
recursivehere is an example of dening the rst nnumbers in the Fibonacci sequence:
fib = array (1,n) ( [ (0,1), (1,1) ] ++
[ (i,fib!(i-1)+fib!(i-2)) | i <- [2..n] ] )
This example demonstrates how one can use an array as a cache, which in this case
turns an exponentially poor algorithm into an ecient linear one.
Another important property is that array comprehensions are constructed lazily,
and thus the order of the elements in the list is completely irrelevant. For example,
we can construct a m-by-n matrix using a wavefront recurrence where the north and
west borders are 1, and each other element is the sum of its north, north-west, and
west neighbors, as follows:
a = array ((1,m),(1,n))
( [ ((1,1),1) ] ++
[ ((i,1),1) | i <- [2..m] ] ++
[ ((1,j),1) | j <- [2..n] ] ++
[ ((i,j), a!(i-1,j) + a!(i,j-1) + a!(i-1,j-1))
| i <- [2..m], j <- [2..n] ] )
59
The elements in this result can be accessed in any orderthe demand-driven eect
of lazy evaluation will cause the necessary elements to be evaluated in an order con-
strained only by data dependencies. It is this property that makes array comprehen-
sions so useful in scientic computation, where recurrence equations express the same
kind of data dependencies. In implementing such recurrences in Fortran one must be
sure that the elements are evaluated in an order consistent with the dependencies
lazy evaluation accomplishes that for us.
On the other hand, although elegant, array comprehensions may be dicult to im-
plement eciently. There are two main diculties: the standard problem of over-
coming the ineciencies of lazy evaluation, and the problem of avoiding the con-
struction of the many intermediate lists that the second argument to array seems
to need. A discussion of ways to overcome these problems is found in [AH89]. Al-
ternative designs for functional arrays and their implementations may be found in
[Hol83, Hug85a, AHN87, Wis87].
Another problemis that array comprehensions are not quite expressiveness enough
to capture all behaviors. The most conspicuous example of this is the case where an
array is being used as an accumulator, say in building a histogram, and thus one actu-
ally wants an incremental update eect. Thus in Haskell a function called accumArray
is provided to capture this kind of behavior in a way consistent with the monolithic
nature of array comprehensions (similar ideas are found in [SH86, Wad86]). However,
it is not clear that this is the most general solution to the problem. An alternative
approach is to dene an incremental update operator on arrays, but then even nas-
tier eciency problems arise, since (conceptually at least) the updates involve copying.
Work on detecting when it is safe to implement such updates destructively has resulted
in at least one ecient implementation [BH88, Blo88, HB85], although the analysis itself
is costly.
Nevertheless, array comprehensions have the potential for being very useful, and
many interesting applications have already been programmed using them (see [HA88]
for some examples). Hopefully future research will lead to solutions to the remaining
problems.
4.4 Views
Pattern-matching (see Section 3.4) is very useful in writing compact and readable pro-
grams. Unfortunately, knowledge of the concrete representation of an object is nec-
essary before pattern-matching can be invoked, which seems to be at odds with the
notion of an abstract datatype. To reconcile this conict Wadler introduced a notion
of views [Wad87].
Aview declaration introduces a newalgebraic datatype, just like a data declaration,
but in addition establishes an isomorphism between the values of this new type and a
subset of the values of an existing algebraic datatype. For example:
60
data Complex = Rectangular Float Float
view Complex = Polar Float Float
where toView (Rectangular x y) = Polar (sqrt (x**2+y**2))
(arctan (y/x))
fromView (Polar r t) = Rectangular (r*(cos t))
(r*(sin t))
[Views are not part of Haskell, but as with abstract datatypes we will use Haskell syntax,
here extended with the keyword view.] Given the datatype Complex, we can read the
view declaration as: one view of Complex contains only Polar values; to translate
from a Rectangular to a Polar value, use the function toView; to translate the other
way, use fromView.
Having declared this viewwe can nowuse pattern-matching using either the Rectangular
constructor or the Polar constructor; similarly, new objects of type Complex can be
built with either the Rectangular or Polar constructors. They have precisely the same
status, and the coercions are done automatically. For example:
rotate (Polar r t) angle = Polar r (t+angle)
As the example stands, objects of type Complex are concretely represented with the
Rectangular constructor, but this decision could be reversed by making Polar the
concrete constructor and Rectangular the view, without altering any of the functions
which manipulate objects of type Complex.
Whereas traditionally abstract data types are regarded as hiding the representation,
with views one can reveal as many representations (zero, one, or more) as are required.
As a nal example, consider this denition of Peanos view of the natural number
subset of integers:
view Integer = Zero | Succ Integer
where fromView Zero = 0
(Succ n) | n>=0 = n+1
toView 0 = Zero
n | n>0 = Succ (n-1)
With this view, 7 is viewed as equivalent to
Succ (Succ (Succ (Succ (Succ (Succ (Succ Zero))))))
Note that fromView denes a mapping of any nite element of peano into an integer,
and toView denes the inverse mapping. Given this view, we can write denitions such
as
61
fac Zero = 1
(Succ n) = (Succ n) * (fac n)
which is very useful froman equational reasoning standpoint, since it allows us to use
an abstract representation of integers without incurring any performance overhead
the view declarations provide enough information to map all of the abstractions back
into concrete implementations at compile time.
On the other hand, perhaps the most displeasing aspect of views is that an implicit
coercion is taking place, which may be confusing to the user. For example, in:
case (Foo a b) of
Foo x y -> exp
one cannot be sure in exp that a==x and b==y! Although views were considered in an
initial version of Haskell, they were eventually discarded, in a large part because of this
problem.
4.5 Parallel Functional Programming
An often-heralded advantage of functional languages is that parallelism in a functional
program is implicit; it is manifested solely through data dependencies and the seman-
tics of primitive operators. This is in contrast to more conventional languages, where
explicit constructs are typically used to invoke, synchronize, and in general coordi-
nate the concurrent activities. In fact, as discussed earlier, many functional languages
were developed simultaneously with work on highly-parallel dataow and reduction
machines, and such research continues today.
In most of this work parallelism in a functional program is detected by the system
and allocated to processors automatically. Although in certain constrained classes of
functional languages the mapping of process to processor can be determined optimally
[Che86, DI85], in the general case the optimal strategy is undecidable, so heuristics such
as load-balancing are often employed instead.
But what if a programmer knows a good (perhaps optimal) mapping strategy for a
program executing on a particular machine, but the compiler is not smart enough to
determine it? And even if the compiler is smart enough, how does one reason about
such behavior? One could argue that the programmer should not be concerned about
such details, but that is a dicult argument to make to someone whose job is precisely
to invent such algorithms!
To meet these needs various researchers have designed extensions to functional
languages, resulting in what I like to call para-functional programming languages. The
extensions amount to a meta-language (for example annotations) to express the desired
62
behavior. Examples include annotations to control evaluation order [Bur84, DW87,
Sri85], prioritize tasks, and map processes to processors [Hud86c, HS86, KL85]. Sim-
ilar work has taken place in the Prolog community [Sha84]. In addition, research has
resulted in formal operational semantics for such extensions [Hud86b, HA87]. In the
remainder of this section one kind of para-functional behavior will be demonstrated:
that of mapping program to machine (based on the work in [Hud86c, HS86]).
The fundamental idea behind process-to-processor mapping is quite simple. Con-
sider the expression e1+e2. The strict semantics of + allows the subexpressions e1 and
e2 to be executed in parallelthis is an example of what is meant by saying that the
parallelism in a functional program is implicit. But suppose now that we wish to ex-
press precisely where (i.e., on which processor) the subexpressions are to be evaluated;
we may do so quite simply by annotating the subexpressions with appropriate map-
ping information. An expression annotated in this way is called a mapped expression,
which has the following form:
exp on proc
[on is a hypothetical keyword, and is not valid Haskell.] which intuitively declares that
exp is to be computed on the processor identied by proc. The expression exp is the
body of the mapped expression, and represents the value to which the overall expres-
sion will evaluate (and thus can be any valid expression, including another mapped
expression). The expression proc must evaluate to a processor id. Without loss of
generality the processor ids, or pids, are assumed to be integers, and there is some
pre-dened mapping from those integers to the physical processors they denote.
Returning nowto the example, we may annotate the expression (e1+e2) as follows:
(e1 on 0) + (e2 on 1)
where 0 and 1 are processor ids. Of course, this static mapping is not very interesting.
It would be nice, for example, if we were able to refer to a processor relative to the
currently executing one. We can do this through the use of the reserved identier self,
which when evaluated returns the pid of the currently executing processor. Using self
we can now be more creative. For example, suppose we have a ring of n processors
that are numbered consecutively; we may then rewrite the above expression as:
(e1 on left self) + (e2 on right self)
where left pid = mod (pid-1) n
right pid = mod (pid+1) n
[mod x y computes x modulo y.] which denotes the computation of the two subex-
pressions in parallel on the two neighboring processors, with the sum being computed
on self.
63
To see that it is desirable to dynamically bind self, consider that one may wish
successive invocations of a recursive call to be executed on dierent processorsthis
is not easily expressed with lexically bound annotations. For example, consider the
following list-of-factorials program, again using a ring of processors:
(map fac [2,3,4]) on 0
where map f [] = []
f (x:xs) = f x : ((map f xs) on (right self))
Note that the recursive call to map is mapped onto the processor to the right of the
current one, and thus the elements 2, 6, and 24 in the result list are computed on
processors 0, 1, and 2, respectively.
Para-functional programming languages have been shown to be adequate in ex-
pressing a wide range of deterministic parallel algorithms clearly and concisely [Hud86c,
HS86]. It remains to be seen, however, whether the pragmatic concerns that motivate
these kinds of language extensions persist, and if they do, whether or not compilers can
become smart enough to perform the optimizations automatically. Indeed these same
questions can be asked about other language extensions, such as the memoization
techniques discussed in the next section.
4.6 Caching and Memoization
Consider this simple denition of the Fibonacci function:
fib 0 = 1
1 = 1
n = fib (n-1) + fib (n-2)
Although simple, it is hopelessly inecient. One could rewrite it in one of the classic
ways, but then the simplicity and elegance of the original denition is lost. Keller
and Sleep suggest an elegant alternative: provide syntax for expressing the caching or
memoization of selected functions [KS86]. For example, the syntax might take the form
of a declaration that precedes the function denition, as in:
memo fib using cache
fib 0 = 1
1 = 1
n = fib (n-1) + fib (n-2)
which would be syntactic sugar for:
64
fib = cache fib1
where fib1 0 = 1
1 = 1
n = fib (n-1) + fib (n-2)
The point is that cache is a user-dened function that species a strategy for caching
values of fib. For example, to cache values in an array, one might dene cache by:
cache fn = \n -> (array (0,max) [(i,fn i) | i<-[0..max]]) ! n
where we assume max is the largest argument to which fib will be applied. Expanding
out the denitions and syntax yields:
fib n = (array (0,max) [(i,fib1 i) | i<-[0..max]]) ! n
where fib1 0 = 1
1 = 1
n = fib (n-1) + fib (n-2)
which is exactly the desired result.
20
As a methodology this is very nice, since libraries
of useful caching functionals may be produced and reused to cache many dierent
functions. There are limitations to the approach, as well as extensions, all of which are
described in [KS86].
One of the limitations of this approach is that in general it can only be used with
strict functions, and even then the expense of performing equality checks on, for exam-
ple, list arguments can be expensive. As a solution to this problem, Hughes introduced
the notion of lazy memo-functions, in which the caching strategy utilizes an identity
test (EQ in Lisp terminology) instead of an equality test [Hug85b]. Such a strategy can
no longer be considered as syntactic sugar, since an identity predicate is not something
normally provided as a primitive in functional languages because it is implementation
dependent. Nevertheless, if built into a language lazy memo-functions provide a very
ecient (constant time) caching mechanism, and allow very elegant solutions to a class
of problems not solved by Keller and Sleeps strategy: those involving innite data
structures. For example, consider this denition of the innite list of ones:
ones = 1 : ones
Any reasonable implementation will represent this as a cyclic list, thus consuming
constant space. Another common idiom is the use of higher order functions such as
map:
twos = map (\x->2*x) ones
20
This is essentially the same solution as the one given in Section 4.3.
65
But now note that only the cleverest of implementations will represent this as a cyclic
list, since map normally generates a new list cell on every recursive call. By lazy mem-
oizing map, however, the cyclic list will be recovered. To see how, note that the rst
recursive call to map will be map (\x->2*x) (tail ones) but (tail ones) is
identical to ones (remember that ones is cyclic), and thus map is called with arguments
identical to the rst call. Thus the old value is returned, and the cycle is created. Many
more practical examples are discussed in [Hug85b].
The interesting thing about memoization in general is that it begins to touch on
some of the limitations of functional languagesin particular the inability to side eect
global objects such as cachesand solutions such as lazy memo-functions represent
useful compromises. It remains to be seen whether more general solutions can be
found that eliminate the need for these special-purpose features.
4.7 Non-Determinism
Most programmers (including the very idealistic among us) admit the need for non-
determinism, despite the semantic diculties that it introduces. It seems to be an
essential ingredient of real-time systems, such as operating systems and device con-
trollers. Non-determinism in imperative languages is typically manifested by run-
ning in parallel several processes that are side-eecting some global statethe non-
determinism is thus implicit in the semantics of the language. In functional languages
non-determinism is manifested through the use of primitive operators such as amb
or mergethe non-determinism is thus made explicit. Several papers have been pub-
lished on the use of such primitives in functional programming, and it appears quite
reasonable to programconventional non-deterministic applications using them[Hen82,
Sto85]. The problem is, once introduced, non-determinism completely destroys refer-
ential transparency, as we shall see.
By way of introduction, McCarthy [McC63] dened a binary non-deterministic oper-
ator called amb having the following behavior:
amb(e
1
, ) = e
1
amb(, e
2
) = e
2
amb(e
1
, e
2
) = either e
1
or e
2
, chosen nondeterministically
The operational reading of amb(e
1
, e
2
) is that e
1
and e
2
are evaluated in parallel,
and the one that completes rst is returned as the value of the expression.
To see how referential transparency is lost, consider this simple example:
(amb 1 2) + (amb 1 2)
Is the answer 2 or 4? Or perhaps 3? It is this last answer that indicates that referential
transparency is lostthere does not appear to be any simple syntactic mechanism for
66
ensuring that one could not replace equals-for-equals in a misleading way. Shortly we
will discuss possible solutions to this problem, but for now let us look at an example
using this style of non-determinism.
Using amb one can easily dene things such as merge that non-deterministically
merge two lists, or streams:
21
merge as bs = amb (if (as==[]) then bs else (head as : merge (tail as) bs))
(if (bs==[]) then as else (head bs : merge as (tail bs)))
which then can be used, for example, in combining streams of characters fromdierent
computer terminals:
process (merge term1 term2)
Using this is a basis, Henderson shows how many operating system problems can be
solved in a pseudo-functional language [Hen82]. Hudak uses non-determinism of a
slightly dierent kind to emulate the parallel updating of arrays [Hud86a].
Although satisfying in the sense of being able to solve real-world kinds of non-
determinism, these solutions are dissatisfying in the way they destroy referential trans-
parency. One might argue that the situation is at least somewhat better than the con-
ventional imperative one in that the non-determinismis at least made explicit, and thus
one could induce extra caution when reasoning about those sections of a program ex-
hibiting non-deterministic behavior. The only problem with this is that determining
which sections of a program are non-deterministic may be dicultit is not a lexi-
cal property, but rather is dynamic, since any function may call a non-deterministic
sub-function.
At least two solutions have been proposed to this problem. One, proposed by War-
ren Burton [Bur88], is to provide a tree-shaped oracle as an argument to a program
from which non-deterministic choices may be selected. By passing the tree and its
sub-trees around explicitly, referential transparency can be preserved. The problem
with this approach is that carrying the oracle around explicitly is cumbersome at best.
On the other hand, functional programmers already carry around a greater amount of
state to avoid problems with side-eects, so perhaps the extra burden is not too great!
Another (at least partial) solution was proposed by Stoye [Sto85] in which all of the
non-determinism in a program is forced to be in one place. Indeed to some extent this
21
Note that this version of merge:
merge [] bs = bs
merge as [] = as
merge (a:as) (b:bs) = amb (a : merge as (b:bs)) (b : merge (a:as) bs)
is not correct, since merge bs evaluates to , whereas we would like it to be bs ++ , which in
fact the denition in the text yields.
67
was the solution adopted in Haskell, although for somewhat dierent reasons. The
problem with this approach is that the non-determinism is not eliminated completely,
but rather centralized. It allows reasoning equationally within the isolated pieces, but
not within the collection of pieces as a whole. Nevertheless, the isolation (at least in
Haskell) is achieved syntactically, and thus it is easy to determine when equational
reasoning is valid. A general discussion of these issues is found in [HS88].
An interesting variation on these two ideas is to combine themcentralize the non-
determinismand then use an oracle to dene it in a referentially transparent way. Thus
the disadvantages of both approaches would seem to dissappear.
In any case, I should point out that none of these solutions makes reasoning about
non-determinism any easier, they just make reasoning about programs easier.
4.8 Extensions to Polymorphic Type Inference
The Hindley-Milner type system has certain limitations; an example of this was given
in Section 2.6.1. Some success has been achieved in extending the type system to
include other kinds of data objects, but surprisingly little success has been achieved
at removing the fundamental limitations while still retaining the tractability of the
type inference problem. It is a somewhat fragile system, but is fortunately expressive
enough to base practical languages on it. Nevertheless, research continues in this area.
Independently of type inference, considerable research is underway on the expres-
siveness of type systems in general. The most obvious thing to do is allow types to
be rst-class, thus allowing abstraction over them in the obvious way. Through gen-
eralizations of the type system it is possible to model such things as parameterized
modules, inheritance, and sub-typing. This area has indeed taken on a character of its
own; a good summary of current work may be found in both [CW85] and [Rey85].
4.9 Combining Other Programming Language Paradigms
A time-honored tradition in programming language design is to come up with hybrid
designs that combine the best features of several dierent paradigms, and functional
programming language research has not escaped that tradition! I will only discuss
briey two such hybrids here, although others exist.
The rst is combining logic programming with functional programming. The log-
ical variable permits two-way matching (via unication) in logic programming lan-
guages, as opposed to the one-way matching (via pattern-matching) in functional lan-
guages, and thus seems like a desirable feature to have in a language. Indeed its
declarative nature ts in well with the ideals of functional programming. However
the integration is not easymany proposals have been made yet none are completely
68
satisfactory, especially in the context of higher-order functions and lazy evaluation.
See [DL85] for a good summary of results.
The second area reects an attempt to combine the state-oriented behavior of im-
perative languages in a semantically clean way. The same problems arise here as they
do with non-determinismsimply adding the assignment statement means that equa-
tional reasoning must always be qualied, since it is dicult to determine whether
or not a deeply nested call is made to a function that induces a side-eect. However,
there are some possible solution to this, most notably work by Giord and Lucassen
[GL86, LG88] in which eects are captured in the type system. In Giords system it
is possible to determine from a procedures type whether or not it is side-eect free.
However, it is not currently known whether such a system can be made into a type
inference system in which type declarations are not required. This is an active area of
current research.
5 Dispelling Myths About Functional Programming
To gain further insight into the nature of functional languages, it is helpful to discuss,
with the hope of dispelling, certain myths that have arisen over the years.
Myth #1: Functional programming is the antithesis of conventional imperative pro-
gramming.
This myth is largely responsible for alienating imperative programmers from func-
tional languages. But in fact, there is much in common between the two styles of
programming, which I hope to make evident by two simple arguments.
Consider rst that one of the key evolutionary characteristics of high-level impera-
tive programming languages has been the use of expressions to denote a result rather
than a sequence of statements to put together a result imperatively and in piecemeal.
Expressions were an important feature of Fortran, for example, had more of a mathe-
matical avor, and freed the programmer of low-level operational detail (this burden
was of course transferred to the compiler). From Fortran expressions, to functions in
Pascal, to expression oriented programming style in Schemethese advances are all
on the same evolutionary path. Functional programming can be seen as carrying this
evolution to its logical conclusioneverything is an expression.
The second argument is based on an analogy between functional (i.e. side-eect-
free) programming and structured (i.e. goto-less) programming. The fact is, it is hard to
imagine doing without either gotos or assignment statements, until one is shown what
to use in their place. In the case of goto, one uses instead structured commands, and
in the case of assignment statements one uses instead lexical binding and recursion.
As an example, this simple program fragment with gotos:
69
x := init;
i := 0;
loop: x := f(x,i);
i := i+1;
if i<10 goto loop;
can be rewritten in a structured style as:
x := init;
i := 0;
while i<10
begin x := f(x,i);
i := i+1
end;
In capturing this disciplined use of goto, arbitrary jumps into or out of the body of
the block now cannot be made. Although this can be viewed as a constraint, most
people now feel that the resulting disciplined style of programming is clearer, easier to
maintain, etc.
But in fact more discipline is evident here than just the judicious use of goto. Note in
the original programfragment that x and i are assigned to exactly once in each iteration
of the loop; the variable i, in fact, is only being used to control the loop termination
criteria, and the nal value of x is intended as the value computed by the loop. This
disciplined use of assignment can be captured by the following assignment-free Haskell
program:
loop init 0
where loop x i = if i<10 then (loop (f x i) (i+1))
else x
Functions (and procedures) can in fact be thought of as a disciplined use of goto and
assignmentthe transfer of control to the body of the function and the subseqent
return capture a discplined use of goto, and the formal to actual parameter binding
captures a disciplined use of assignment.
By inventing a bit of syntactic sugar to capture the essence of tail recursion, the
above program could be rewritten as:
let x = init
i = 0
in while i<10
begin next x = f(x,i)
70
next i = i+1
end
result x
[This syntactic sugar is not found in Haskell, although some other functional (especially
dataow) languages have similar features, including Id, Val, and Lucid.] where the form
next x = ... is a construct (next is a keyword) used to express what the value of
x will be on the next iteration of the loop. Note the similarity of this program to
the structured one given earlier. In order to properly enforce a disciplined use of
assignment, we can constrain the syntax so that only one next statement is allowed
for each identier (stated another way, this constraint means that it is a trivial matter
to convert such programs into the tail recursive form shown earlier). If one thinks of
next x and next i as new identiers (just as the formal parameters of loop can
be thought of as new identiers for every call to loop), then referential transparency is
preserved. Functional programming advocates argue that this results in a better style
of programming, in much the same way that structured programming advocates argue
for their cause.
Thus the analogy between goto-less programming and assignment-free program-
ming runs deep. When Dijkstra rst introduced structured programming, much of the
programming community was aghasthow could one do without goto? But as peo-
ple programmed in the new style, it was realized that what was being imposed was
a discipline for good programming, not a police state to inhibit expressiveness.
Exactly the same can be said of side-eect-free programming, and its advocates hope
that as people become more comfortable programming in the functional style, they will
appreciate the good sides of the discipline thus imposed.
When viewed in this way functional languages can be seen as a logical step in the
evolution of imperative languagesthus, of course, rendering them non-imperative.
On the other hand, it is exactly this purity that some programmers object to, and one
could argue that just as a tasteful use of goto here or there is acceptable, so is a
tasteful use of a side-eect. Such small impurities certainly shouldnt invalidate the
functional programming style, and thus may be acceptable.
Myth #2: Functional programming languages are toys.
The rst step toward dispelling this myth is to cite examples of ecient implemen-
tations of functional languages, of which there now exists several. The Al compiler
at Yale, for example, generates code that is competitive with that generated by con-
ventional language compilers [You88]. Other notable compiler eorts include the LML
compiler at Chalmers University [Aug84], the Ponder compiler at Cambridge University
[Fai85], and the ML compilers developed at Bell Labs and Princeton [AM87].
On the other hand, there are still inherent ineciencies that cannot be ignored.
Higher-order functions and lazy evaluation certainly increase expressiveness, but in the
71
general case the overhead of, for example, the dynamic storage management necessary
to support them, cannot be eliminated.
The second step toward dispelling this myth amounts to pointing to real applica-
tions development in functional languages, including real-world situations involving
non-determinism, databases, parallelism, etc. Although examples of this sort are not
plentiful (primarily because of the youth of the eld) and are hard to cite (since pa-
pers are not usually written about applications), they do exist. For example, I know of
several:
1. The dataow groups at MIT and the functional programming groups at Yale have
written numerous functional programs for scientic computation. So has two
national labs: Los Alamos and Lawrence Livermore.
2. MCC has written a reasonably large expert system (EMYCIN) in SASL.
3. At least one company is currently marketing a commercial product (a CAD pack-
age) that uses a lazy functional language.
4. A group at IBM uses a lazy functional language for graphics and animation.
5. The LML (lazy ML) compiler at Chalmers was written almost entirely in LML, and
the new Haskell compilers at both Glasgow and Yale are being written in Haskell.
6. GEC Hirst Research Lab is using a program for designing VLSI circuits that was
written by some researchers at Oxford using a lazy functional language.
I am sure that there are many other examples that I am not aware of. In particular,
there are many Scheme and Lisp programs that are predominantly side-eect-free, and
could properly be thought of as functional programs.
Myth #3: Functional languages cannot deal with state.
This criticismis often expressed as a question: howcan one programin a language
that does not have a notion of state? The answer, of course, is that you cannot, and
in fact functional languages deal with state very nicely, although the state is expressed
explicitly rather than implicitly. So the issue is more a matter of how one expresses state
and manipulations of it.
State in a functional programis usually carried around explicitly in one of two ways:
(1) in the values of bound variables of functions, in which case it is updated by making
a recursive call to the function with new values as arguments, or (2) in a data struc-
ture that is updated non-destructively so that, at least conceptually, the old value
of the data structure remains intact and can be accessed later. Although declarative
and referentially transparent, this treatment of state can present problems to an im-
plementation, but it is certainly not a problem in expressiveness. Furthermore, the
72
implementation problems are not as bad as they rst seem, and recent work has gone
a long way toward solving them. For example, a compiler can convert tail recursions
into loops and single-threaded data structures into mutable ones.
It turns out that, with respect to expressiveness, one can use higher-order functions
not only to manipulate state, but also to make it appear implicit. To see how, consider
the imperative program given earlier:
x := init;
i := 0;
loop: x := f(x,i);
i := i + 1;
if (i < 10) goto loop;
We can model the implicit state in this program as a pair (xval,ival), and dene
several functions that update this state in a way that mimics the assignment statements:
x (xval,ival) xval = (xval,ival)
i (xval,ival) ival = (xval,ival)
x (x,i) = x
i (x,i) = i
const v s = v
We will take advantage of the fact that these functions are dened in curried form.
Note how x and i are used to update the state, and x and i are used to access the
state. For example, the following function, when applied to the state, will increment i:
\s -> i s ((i s) + 1)
For expository purposes we would like to make the state as implicit as possible,
and thus we express the result as a composition of higher-order functions. To facilitate
this, and to make the result look as much like the original program as possible, we
dene the following higher-order inx operators and functions:
22
f := g = \s -> f s (g s)
f ; g = \s -> g (f s)
goto f = f
f + g = \s -> f s + g s
f < g = \s -> f s < g s
if p c = \s -> (if (p s) then (c s) else s)
22
It is interesting to note that :=, const, and goto correspond precisely to the combinators S, K, and I,
and ; is almost the combinator B, but is actually CB.
73
[I am cheating slightly here in that ; is a reserved operator in Haskell, and thus cannot
really be redened in this way.]
Given these denitions we can now write the following functional (albeit contrived)
version of the imperative program given earlier:
x := const init;
i := const 0;
loop where
loop = x := f;
i := i + const 1;
if (i < const 10) (goto loop)
This result is rather disquietingit looks very much like the original imperative pro-
gram! Of course, we worked hard to make it that way, which in the general case is much
harder to do, and it is certainly not the recommended way to do functional program-
ming. Nevertheless it exemplies the power and exibility of higher-order functions
note how they are used here both to manipulate the state and to implement the goto
(where in particular the denition of loop is recursive, since the goto implements a
loop).
6 Conclusions
In this paper I have attempted to present functional programming in all of its many
shapes and forms. In so doing I have only touched on the surface of many issues. It is
hoped that enough of a foundation has been given that the interested researcher may
explore particularly interesting topics in more depth, and that the interested program-
mer may learn to use functional languages in a variety of applications.
The reader will note that I have said very little about how to implement functional
languages, primarily because doing that subject justice would probably double the
size of this paper! Nevertheless, I feel compelled to point the interested reader in
the right direction for such a study. By far the best single reference to sequential
implementations is Peyton Jones text [PJ87], although recently other techniques have
appeared viable (see [BHY88, BPJR88, FW87]). Parallel implementations have taken
a variety of forms. On commercial machines the state of the art on parallel graph
reduction implementations may be found in [GH88, Gol88b, Gol88a]. The latest on
special-purpose parallel graph reducers can be found in [PJCSH87, WW87]. For a very
dierent kind of implementation see [HM88]. References to dataow machines were
given in Section 2.8.
Acknowledgements. Thanks to the Lisp and Functional Programming Research
Group at Yale, who inspire much of my work and serve as my chief sounding board. A
74
special thanks is also extended to the Haskell Committee, through which I learned more
than I care to admit. In addition, the following people provided valuable comments on
earlier drafts of the paper: Kei Davis, Alex Ferguson, John Launchbury, and Phil Wadler
(all from the University of Glasgow); Juan Guzman, Siau-Cheng Khoo, Amir Kishon,
Raman Sundaresh, and Pradeep Varma (all from Yale); David Wise (Indiana University);
and three anonymous referees,
I also wish to thank my funding agencies: The Department of Energy under grant
FG02-86ER25012, the Defense Advanced Research Projects Agency under grant N00014-
88-K-0573, and the National Science Foundation under grant DCR-8451415. Without
their generous support none of this work would have been possible.
In writing this survey paper I have tried to acknowledge accurately the signicant
technical contributions in each of the areas of functional programming that I have
covered. Unfortunately, that is a very dicult task, and I apologize in advance for any
errors or omissions.
75
References
[AD79] W.B. Ackerman and J.B Dennis. VAL a value-oriented algorithmic
language preliminary reference manual. Laboratory for Computer Sci-
ence MIT/LCS/TR-218, MIT, June 1979.
[AG77] Arvind and K.P. Gostelow. A computer capable of exchanging processors
for time. In Proceedings IFIP Congress, pages 849853, June 1977.
[AG82] Arvind and K.P. Gostelow. The U-interpreter. Computer, 15(2):4250, Febru-
ary 1982.
[AH89] S. Anderson and P. Hudak. Ecient Compilation of Haskell Array Compre-
hensions. Technical Report YALEU/DCS/RR693, Yale University, Depart-
ment of Computer Science, March 1989.
[AHN87] A. Aasa, S. Holmstrom, and C. Nilsson. An Eciency Comparison of Some
Representations of Purely Functional Arrays. Technical Report 33, Program-
ming Methodology Group, Chalmers University of Technology, May 1987.
[AK81] Arvind and V. Kathail. Amultiple processor data owmachine that supports
generalized procedures. In Proceedings 8th Annual Symposium Computer
Architecture, pages 291302, ACM SIGARCH 9(3), May 1981.
[AM87] A.W. Appel and D.B. MacQueen. A standard ML compiler. In Proceedings of
1987 Functional Programming Languages and Computer Architecture Con-
ference, pages 301324, Springer-Verlag LNCS 274, September 1987.
[ASS85] H. Abelson, G.J. Sussman, and J. Sussman. Structure and Interpretation of
Computer Programs. The MIT Press (Cambridge, MA) and McGraw-Hill Book
Company (New York, NY), 1985.
[Aug84] L. Augustsson. Acompiler for Lazy ML. In Proceedings 1984 ACMConference
on LISP and Functional Programming, pages 218227, August 1984.
[Aug85] L. Augustsson. Compiling pattern-matching. In Functional Programming
Languages and Computer Architecture, pages 368381, Springer-Verlag
LNCS 201, September 1985.
[AW76a] E.A. Ashcroft and W.W. Wadge. Lucid a formal system for writing and
proving programs. SIAM Journal of Computing, 5(3):336354, September
1976.
[AW76b] E.A. Ashcroft and W.W. Wadge. Lucid, a nonprocedural language with itera-
tion. Communications of the ACM, 20:519526, 1976.
76
[Bac78] J. Backus. Can programming be liberated from the von Neumann style? A
functional style and its algebra of programs. CACM, 21(8):613641, August
1978.
[Bar84] H.P. Barendregt. The Lambda Calculus, Its Syntax and Semantics. North-
Holland, Amsterdam, 1984. Revised edition.
[BD77] R.M. Burstall and J. Darlington. A transformation system for developing
recursive programs. JACM, 24(1):4467, 1977.
[Ber78] G. Berry. Squentialit de lvaluation formelle des -expressions. In Pro-
ceedings 3-e Colloque International sur la Programmation, March 1978.
[BH88] A. Bloss and P. Hudak. Path semantics. In Proceedings of Third Work-
shop on the Mathematical Foundations of Programming Language Seman-
tics, Springer-Verlag LNCS (to appear), 1988.
[BHY88] A. Bloss, P. Hudak, and J. Young. Code optimizations for lazy evaluation.
Lisp and Symbolic Computation: An International Journal, 1:147164, 1988.
[Blo88] A. Bloss. Path Analysis: Using Order-of-Evaluation Information to Optimize
Lazy Functional Languages. PhD thesis, Yale University, Department of
Computer Science, 1988.
[BMS80] R.M. Burstall, D.B. MacQueen, and D.T. Sannella. HOPE: an experimental
applicative language. In The 1980 LISP Conference, pages 136143, Stanford
University, August 1980.
[Boe85] H-J. Boehm. Partial polymorphic type inference is undecidable. In Proceed-
ings of 26th Symposium on Foundations of Computer Science, pages 339
345, October 1985.
[Bou88] B.E. Boutel. Tui Language Manual. Technical Report CSD-8-021, Victoria
University of Wellington, Department of Computer Science, 1988.
[BPJR88] G.L. Burn, S.L. Peyton Jones, and J.D. Robson. The spineless G-machine.
In Proceedings 1988 ACM Conference on Lisp and Functional Programming,
pages 244258, ACMSIGPLAN/SIGACT/SIGART, Salt Lake City, Utah, August
1988.
[Bur75] W.H. Burge. Recursive Programming Techniques. Addison-Wesley, Reading,
MA, 1975.
[Bur84] F.W. Burton. Annotations to control parallelism and reduction order in the
distributed evaluation of functional programs. ACM Transactions on Pro-
gramming Languages and Systems, 6(2):159174, April 1984.
77
[Bur88] F.W. Burton. Nondeterminism with referential transparency in functional
programming languages. The Computer Journal, 31(3):243247, 1988.
[BW88] R. Bird and P. Wadler. Introduction to Functional Programming. Prentice
Hall, New York, 1988.
[BWW86] J. Backus, J.H. Williams, and E.L. Wimmers. FL Language Manual (Prelim-
inary Version). Technical Report RJ 5339 (54809) Computer Science, IBM
Almaden Research Center, November 1986.
[Car76] R. Cartwright. APractical Formal Semantic Denition and Verication System
for Typed Lisp. Technical Report, Stanford Artical Intelligence Laboratory
AIM-296, 1976.
[CF58] H.B. Curry and R. Feys. Combinatory Logic, Vol. 1. North-Holland, Amster-
dam, 1958.
[Che86] M.C. Chen. Transformations of parallel programs in crystal. In Information
Processing 86, pages 455462, IFIP, Elsevier Science Publishers B.V. (North-
Holland), 1986.
[Chu33] A. Church. A set of postulates for the foundation of logic. Annals of Math-
ematics, 2(33-34):346366,839864, 1932-1933.
[Chu41] A. Church. The Calculi of Lambda Conversion. Princeton University Press,
Princeton, NJ, 1941.
[CR36] A. Church and J.B. Rosser. Some properties of conversion. Transactions of
the American Mathematical Society, 39:472482, 1936.
[Cur30] H.B. Curry. Grundlagen der kombinatorischen logik. American Journal of
Mathematics, 52:509536,789834, 1930.
[CW85] L. Cardelli and P. Wegner. On understanding types, data abstraction, and
polymorphism. Computing Surveys, 17(4):471522, December 1985.
[Dav78] A.L. Davis. The architecture and system method of DDM-1: a recursively-
structured data driven machine. In Proceedings Fifth Annual Symposium on
Computer Architecture, 1978.
[DI85] J.-M. Delosme and I.C.F. Ipsen. An illustration of a methodology for the
construction of ecient systolic architectures in VLSI. In Proceedings 2nd
International Symposium on VLSI Technology, Systems, and Applications,
pages 268273, 1985.
[DL85] D. Degroot and G. Lindstrom. Functional and Logic Programming. Prentice-
Hall, 1985.
78
[DM74] J.B. Dennis and D.P. Misunas. A preliminary architecture for a basic data-
ow processor. In Proceedings of the 2nd Annual Symposium on Computer
Architecture, pages 126132, ACM, IEEE, 1974.
[DM82] L. Damas and R. Milner. Principle type schemes for functional languages. In
9th ACMSymposiumon Principles of Programming Languages, ACM, August
1982.
[DW87] J. Darlington and L. While. Controlling the behavior of functional language
systems. In Proceedings of 1987 Functional Programming Languages and
Computer Architecture Conference, pages 278300, Springer-Verlag LNCS
274, September 1987.
[Fai85] J. Fairbairn. Design and Implementation of a Simple Typed Language Based
on the Lambda Calculus. PhD thesis, University of Cambridge, May 1985.
Available as Computer Laboratory TR No. 75.
[FH88] A.J. Field and P.G. Harrison. Functional Programming. Adison-Wesley, Work-
ingham, England, 1988.
[FLO85] S. Fortune, D. Leivant, and M. ODonnell. The expressiveness of simple and
second-order type structures. JACM, 30(1):151185, January 1985.
[FW76] D.P. Friedman and D.S. Wise. Cons should not evaluate its arguments. In
Automata, Languages and Programming, pages 257284, Edinburgh Uni-
versity Press, 1976.
[FW87] J. Fairbairn and S. Wray. Tim: a simple, lazy abstract machine to execute
supercombinators. In Proceedings of 1987 Functional Programming Lan-
guages and Computer Architecture Conference, pages 3445, Springer Ver-
lag LNCS 274, September 1987.
[GH88] B. Goldberg and P. Hudak. Implementing functional programs on a hyper-
cube multiprocessor. In Proceedings of Third Conference on Hypercube Con-
current Computers and Applications, ACM, January 1988.
[GHG60] H. Gelernter, J.R. Hansen, and C.L. Gerberich. A FORTRAN-compiled list
processing language. JACM, 7(2):87101, 1960.
[GHW81] J. Guttag, J. Horning, and J. Williams. FP with data abstraction and strong
typing. In Proceedings of the 1981 Conference on Functional Programming
Languages and Computer Architecture, pages 1124, ACM, 1981.
[Gir72] J.-Y. Girard. Interprtation Fonctionelle et Elimination des Coupures dans
lArithmtique dOrdre Suprieur. PhD thesis, Univ. of Paris, 1972.
79
[GL86] D.K. Giord and J.M. Lucassen. Integrating functional and imperative pro-
gramming. In Proceedings 1986 ACM Conference on Lisp and Functional
Programming, pages 2838, ACM SIGPLAN/SIGACT/SIGART, August 1986.
[GMM*78] M. Gordon, R. Milner, L. Morris, M. Newey, and C. Wadswirth. Ametalanguage
for interactive proof in LCF. In Conference Record of the Fifth Annual ACM
Symposium on Principles of Programming Languages, pages 119130, ACM,
1978.
[GMW79] M.J. Gordon, R. Milner, and C.P. Wadsworth. Edinburgh LCF. Springer-Verlag
LNCS 78, Berlin, 1979.
[Gol88a] B. Goldberg. Buckwheat: graph reduction on a shared memory multiproces-
sor. In Proceedings 1988 ACM Conference on Lisp and Functional Program-
ming, pages 4051, ACM SIGPLAN/SIGACT/SIGART, Salt Lake City, Utah,
August 1988.
[Gol88b] B. Goldberg. Multiprocessor Execution of Functional Programs. PhD thesis,
Yale University, Department of Computer Science, 1988. Available as tech-
nical report YALEU/DCS/RR-618.
[HA87] P. Hudak and S. Anderson. Pomset interpretations of parallel functional
programs. In Proceedings of 1987 Functional Programming Languages and
Computer Architecture Conference, pages 234256, Springer Verlag LNCS
274, September 1987.
[HA88] P. Hudak and S. Anderson. Haskell Solutions to the Language Session Prob-
lems at the 1988 Salishan High-Speed Computing Conference. Technical
Report YALEU/DCS/RR-627, Yale University, Department of Computer Sci-
ence, January 1988.
[Han] P. Hancock. Polymorphic type-checking. Chapters 8 and 9 in [PJ87].
[HB85] P. Hudak and A. Bloss. The aggregate update problem in functional pro-
gramming systems. In 12th ACM Symposium on Principles of Programming
Languages, pages 300314, ACM, 1985.
[Hen80] P. Henderson. Functional Programming: Application and Implementation.
Prentice-Hall, Englewood Clis, NJ, 1980.
[Hen82] P. Henderson. Purely functional operating systems. In Functional Program-
ming and Its Applications: An Advanced Course, pages 177192, Cambridge
University Press, 1982.
[Hin69] R. Hindley. The principle type scheme of an object in combinatory logic.
Transactions of the American Mathematical Society, 146:2960, December
1969.
80
[HM76] Henderson and Morris. A lazy evaluator. In 3rd ACM Symposium on Princi-
ples of Programming Languages, pages 95103, ACM, January 1976.
[HM88] P. Hudak and E. Mohr. Graphinators and the duality of SIMD and MIMD.
In Proceedings 1988 ACM Conference on Lisp and Functional Programming,
pages 224234, ACMSIGPLAN/SIGACT/SIGART, Salt Lake City, Utah, August
1988.
[Hol83] S. Holmstrom. How to handle large data structures in functional languages.
In Proc. of SERC/Chalmers Workshop on Declarative Programming Lan-
guages, 1983.
[HS86] P. Hudak and L. Smith. Para-functional programming: a paradigm for pro-
gramming multiprocessor systems. In 12th ACM Symposium on Principles
of Programming Languages, pages 243254, January 1986.
[HS88] P. Hudak and R. Sundaresh. On the Expressiveness of Purely Functional I/O
Systems. Technical Report YALEU/DCS/RR-665, Yale University, Depart-
ment of Computer Science, December 1988.
[Hud84] P. Hudak. ALFL Reference Manual and Programmers Guide. Research Re-
port YALEU/DCS/RR-322, Second Edition, Yale University, October 1984.
[Hud86a] P. Hudak. Arrays, non-determinism, side-eects, and parallelism: a func-
tional perspective. In Proceedings of the Santa Fe Graph Reduction Work-
shop, pages 312327, Los Alamos/MCC, Springer-Verlag LNCS 279, October
1986.
[Hud86b] P. Hudak. Denotational semantics of a para-functional programming lan-
guage. International Journal of Parallel Programming, 15(2):103125, April
1986.
[Hud86c] P. Hudak. Para-functional programming. Computer, 19(8):6071, August
1986.
[Hug84] J. Hughes. Why Functional Programming Matters. Technical Report 16, Pro-
gramming Methodology Group, Chalmers University of Technology, Novem-
ber 1984.
[Hug85a] J. Hughes. An Ecient Implementation of Purely Functional Arrays. Techni-
cal Report, Programming Methodology Group, Chalmers University of Tech-
nology, 1985.
[Hug85b] J. Hughes. Lazy memo-functions. In Functional Programming Languages
and Computer Architecture, pages 129146, Springer-Verlag LNCS 201,
September 1985.
81
[HWe88] P. Hudak and P. Wadler (editors). Report on the Functional Programming
Language Haskell. Technical Report YALEU/DCS/RR656, Yale University,
Department of Computer Science, November 1988.
[Ive62] K. Iverson. A Programming Language. Wiley, New York, 1962.
[Joh88] S.D. Johnson. Daisy Programming Manual. Technical Report, Indiana Uni-
versity Computer Science Department, 1988. (in progress, draft available
on request).
[Kae88] S. Kaes. Parametric polymorphsim. In Proceedings of the 2nd Eupropean
Symposium on Programming, Springer-Verlag LNCS 300, March 1988.
[Kel82] R.M. Keller. FEL programmers guide. AMPS TR 7, University of Utah, March
1982.
[KJRL80] R.M. Keller, B. Jayaraman, D. Rose, and G. Lindstrom. FGL programmers
guide. AMPS Technical Memo 1, Department of Computer Science, Univer-
sity of Utah, July 1980.
[KL85] R.M. Keller and G. Lindstrom. Approaching distributed database implemen-
tations through functional programming concepts. In Intl Conference on
Distributed Systems, May 1985.
[Kle36] S.C. Kleene. -denability and recursiveness. Duke Mathematical Journal,
2:340353, 1936.
[KR35] S.C. Kleene and J.B. Rosser. The inconsistency of certain forms of logic.
Annals of Mathematics, 2(36):630636, 1935.
[Kro87] H.J. Kroeze. The TWENTEL System(Version 1). Technical Report, Department
of Computer Science, University of Twente, The Netherlands, 1986/87.
[KS86] R.M. Keller and R. Sleep. Applicative caching. ACM Transactions on Pro-
gramming Languages and Systems, 8(1):88108, January 1986.
[Lan64] P.J. Landin. The mechanical evaluation of expressions. Computer Journal,
6(4):308320, January 1964.
[Lan65] P.J. Landin. A correspondance between ALGOL 60 and Churchs lambda
notation. Communications of the ACM, 8:89101,158165, 1965.
[Lan66] P.J. Landin. The next 700 programming languages. Communications of the
ACM, 9(3):157166, 1966.
[LG88] J.M. Lucassen and D.K. Giord. Polymorphic eect systems. In Proceed-
ings of 15th ACM Symposium on Principles of Programming Languages,
pages 4757, January 1988.
82
[MAGD83] J. McGraw, S. Allan, J. Glauert, and I. Dobes. SISAL: Streams and Iteration
in a Single-Assignment Language, Language Reference Manual. Technical
Report, Lawrence Livermore National Laboratory, M-146, July 1983.
[Mar51] A.A. Markov. Teoriya algorifmov (Theory of algorithms). Trudy Mat. Inst.
Steklov, 38:176189, 1951.
[McC60] J. McCarthy. Recursive functions of symbolic expressions and their compu-
tation by machine, Part I. CACM, 3(4):184195, April 1960.
[McC63] J. McCarthy. A basis for a mathematical theory of computation. In Com-
puter Programming and Formal Systems, pages 3370, North Holland, Am-
sterdam, 1963.
[McC78] J. McCarthy. History of Lisp. In Preprints of Proceedings of ACM SIGPLAN
History of Programming Languages Conference, pages 217223, 1978. Pub-
lished as SIGPLAN Notices 13(8), August 1978.
[Mcg82] J.R. Mcgraw. The VAL language: description and analysis. TOPLAS, 4(1):44
82, January 1982.
[Mil78] R.A. Milner. A theory of type polymorphism in programming. Journal of
Computer and System Sciences, 17(3):348375, December 1978.
[Mil84] R. Milner. A proposal for Standard ML. In Proceedings 1984 ACMConference
on LISP and Functional Programming, pages 184197, ACM, August 1984.
[MS76] R.E. Milne and C. Strachey. A Theory of Programming Language Semantics.
Chapman and Hall, London, and John Wiley, New York, 1976.
[Mul88] L.R. Mullin. AMathematics of Arrays. PhDthesis, Computer and Information
Science and CASE Center, Syracuse University, December 1988.
[NPA86] R.S. Nikhil, K. Pingali, and Arvind. Id Nouveau. Computation Structures
Group Memo 265, Massachusetts Institute of Technology, Laboratory for
Computer Science, July 1986.
[Pfe88] F. Pfenning. Partial polymorphic type inference and higher-order unication.
In Proceedings 1988 ACM Conference on Lisp and Functional Programming,
pages 153163, ACMSIGPLAN/SIGACT/SIGART, Salt Lake City, Utah, August
1988.
[PJ87] S.L. Peyton Jones. The Implementation of Functional Programming Lan-
guages. Prentice-Hall International, Englewood Clis, NJ, 1987.
83
[PJCSH87] S.L. Peyton Jones, C. Clack, J. Salkild, and M. Hardie. GRIPa high-
performance architecture for parallel graph reduction. In Proceedings of
1987 Functional Programming Languages and Computer Architecture Con-
ference, pages 98112, Springer-Verlag LNCS 274, September 1987.
[Pos43] E.L. Post. Formal reductions of the general combinatorial decision problem.
American Journal of Mathematics, 65:197215, 1943.
[RCe86] J. Rees and W. Clinger (eds.). The revised
3
report on the algorithmic language
Scheme. SIGPLAN Notices, 21(12):3779, December 1986.
[Rey74] J.C. Reynolds. Towards a theory of type structure. In Proc. Colloque sur la
Programmation, pages 408425, Springer-Verlag LNCS 19, 1974.
[Rey85] J.C. Reynolds. Three approaches to type structure. In Mathematical Foun-
dations of Software Development, pages 97138, Springer-Verlag LNCS 185,
March 1985.
[Ros82] J.B. Rosser. Highlights of the history of the lambda-calculus. In Proceedings
1982 ACMConference on LISP and Functional Programming, pages 216225,
ACM, August 1982.
[Sch24] M. Schnnkel. Uber die bausteine der mathematischen logik. Mathematis-
che Annalen, 92:305, 1924.
[Sch85] D.A. Schmidt. Detecting global variables in denotational specications. ACM
Transactions on Programming Languages and Systems, 7(2):299310, 1985.
[Sco70] D.S. Scott. Outline of a Mathematical Theory of Computation. Programming
Research Group PRG-2, Oxford University, November 1970.
[SH86] Jr. Steele, Guy L. and W. Daniel Hillis. Connection machine lisp: ne-
grained parallel symbolic processing. In Proceedings 1986 ACM Con-
ference on Lisp and Functional Programming, pages 279297, ACM SIG-
PLAN/SIGACT/SIGART, Cambridge, Massachusetts, August 1986.
[Sha84] E. Shapiro. Systolic Programming: AParadigmof Parallel Processing. Depart-
ment of Applied Mathematics CS84-21, The Weizmann Institute of Science,
August 1984.
[Sri85] N.S. Sridharan. Semi-Applicative Programming: An Example. Technical Re-
port, BBN Laboratories, November 1985.
[Sto77] J.E. Stoy. Denotational Semantics: The Scott-Strachey Approach to Program-
ming Language Theory. The MIT Press, Cambridge, Mass., 1977.
[Sto85] W. Stoye. ANewScheme for Writing Functional Operating Systems. Technical
Report 56, University of Cambridge, Computer Laboratory, 1985.
84
[TBH82] P.C. Treleaven, D.R. Brownbridge, and R.P. Hopkins. Data-driven and
demand-driven computer architectures. Computing Surveys, 14(1):93143,
March 1982.
[Tha87] S.S. Thakkar, editor. Selected Reprints on Dataow and Reduction Architec-
tures. The Computer Society Press, Washington, DC, 1987.
[Tof88] MTofte. Operational Semantics and Polymorphic Type Inference. PhDthesis,
University of Edinburgh, Department of Computer Science (CST-52-88), May
1988.
[TP86] H-C. Tu and A.J. Perlis. FAC: a functional APL language. IEEE Software,
3(1):3645, January 1986.
[Tra88] B.A. Trakhtenbrot. Comparing the Church and Turing Approaches: Two
Prophetic Messages. Technical Report 98/88, Eskenasy Institute of Com-
puter Science, Tel-Aviv University, April 1988.
[Tu86] H-C. Tu. FAC: Functional Array Calculator and its Application to APL and
Functional Programming. PhD thesis, Yale University, Department of Com-
puter Science, April 1986. Available as Research Report YALEU/DCS/RR-
468.
[Tur36] A.M. Turing. On computable numbers with an application to the entschei-
dungsproblem. Proceedings of the London Mathematical Society, 42:230
265, 1936.
[Tur37] A.M. Turing. Computability and -denability. Journal of Symbolic Logic,
2:153163, 1937.
[Tur76] D.A. Turner. SASL language manual. Technical Report, University of St.
Andrews, 1976.
[Tur79] D.A. Turner. A new implementation technique for applicative languages.
Software Practice and Experience, 9:3149, 1979.
[Tur81] D.A. Turner. The semantic elegance of applicative languages. In Proceed-
ings of the 1981 Conference on Functional Programming Languages and
Computer Architecture, pages 8592, ACM, 1981.
[Tur82] D.A. Turner. Recursion equations as a programming language. In Func-
tional Programming and Its Applications: An Advanced Course, pages 128,
Cambridge University Press, 1982.
[Tur85] D.A. Turner. Miranda: a non-strict functional language with polymorphic
types. In Functional Programming Languages and Computer Architecture,
pages 116, Springer-Verlag LNCS 201, September 1985.
85
[Veg84] S.R. Vegdahl. A survey of proposed architectures for the execution of func-
tional languages. IEEE Transactions on Computers, C-23(12):10501071, De-
cember 1984.
[vH67] J. van Heijenoort. FromFrege to Gdel. Harvard University Press, Cambridge,
MA, 1967.
[Vui74] J. Vuillemin. Correct and optimal implementations of recursion in a sim-
ple programming language. Journal of Computer and Systems Science, 9(3),
1974.
[WA85] W.W. Wadge and E.A. Ashcroft. Lucid, the DataowProgramming Language.
Academic Press, London, 1985.
[Wad] P. Wadler. Ecient compilation of pattern-matching. Chapter 5 in [PJ87].
[Wad71] C.P. Wadsworth. Semantics and Pragmatics of the Lambda Calculus. PhD
thesis, Oxford University, 1971.
[Wad86] P. Wadler. A new array operation. In Workshop on Graph Reduction Tech-
niques, Springer-Verlag LNCS 279, October 1986.
[Wad87] P. Wadler. Views: A Way for Pattern-Matching to Cohabit with Data Ab-
straction. Technical Report 34, Programming Methodology Group, Chalmers
University of Technology, March 1987. Preliminary version appeared in the
Proceedings of the 14th ACM Symposium on Principles of Programming
Languages, January 1987.
[WB89] P. Wadler and S. Blott. Howto make ad hoc polymorphismless ad hoc. In Pro-
ceedings of 16th ACM Symposium on Principles of Programming Languages,
pages 6076, January 1989.
[Weg68] P. Wegner. Programming Languages, Information Structures, and Machine
Organization. McGraw-Hill, 1968.
[Wik88] Wikstrom. Standard ML. Prentice-Hall, 1988.
[Wis87] D. Wise. Matrix algebra and applicative programming. In Proceedings of
1987 Functional Programming Languages and Computer Architecture Con-
ference, pages 134153, Springer Verlag LNCS 274, September 1987.
[WM88] P. Wadler and Q. Miller. An Introduction to Orwell. Technical Report, Pro-
gramming Research Group, Oxford University, 1988. (First version, 1985.).
[WW87] P. Watson and I. Watson. Evaluating functional programs on the FLAGSHIP
machine. In Proceedings of 1987 Functional Programming Languages and
Computer Architecture Conference, pages 8097, Springer-Verlag LNCS 274,
September 1987.
86
[You88] J. Young. The Semantic Analysis of Functional Programs: Theory and Prac-
tice. PhD thesis, Yale University, Department of Computer Science, 1988.
87