Conference PaperPDF Available

The history of Standard ML

Authors:

Abstract and Figures

The ML family of strict functional languages, which includes F#, OCaml, and Standard ML, evolved from the Meta Language of the LCF theorem proving system developed by Robin Milner and his research group at the University of Edinburgh in the 1970s. This paper focuses on the history of Standard ML, which plays a central rôle in this family of languages, as it was the first to include the complete set of features that we now associate with the name “ML” (i.e., polymorphic type inference, datatypes with pattern matching, modules, exceptions, and mutable state). Standard ML, and the ML family of languages, have had enormous influence on the world of programming language design and theory. ML is the foremost exemplar of a functional programming language with strict evaluation (call-by-value) and static typing. The use of parametric polymorphism in its type system, together with the automatic inference of such types, has influenced a wide variety of modern languages (where polymorphism is often referred to as generics). It has popularized the idea of datatypes with associated case analysis by pattern matching. The module system of Standard ML extends the notion of type-level parameterization to large-scale programming with the notion of parametric modules, or functors. Standard ML also set a precedent by being a language whose design included a formal definition with an associated metatheory of mathematical proofs (such as soundness of the type system). A formal definition was one of the explicit goals from the beginning of the project. While some previous languages had rigorous definitions, these definitions were not integral to the design process, and the formal part was limited to the language syntax and possibly dynamic semantics or static semantics, but not both. The paper covers the early history of ML, the subsequent efforts to define a standard ML language, and the development of its major features and its formal definition. We also review the impact that the language had on programming-language research.
Content may be subject to copyright.
86
The History of Standard ML
DAVID MACQUEEN, University of Chicago, USA
ROBERT HARPER, Carnegie Mellon University, USA
JOHN REPPY, University of Chicago, USA
Shepherd: Kim Bruce, Pomona College, USA
The ML family of strict functional languages, which includes F#, OCaml, and Standard ML, evolved from the
Meta Language of the LCF theorem proving system developed by Robin Milner and his research group at the
University of Edinburgh in the 1970s. This paper focuses on the history of Standard ML, which plays a central
rôle in this family of languages, as it was the rst to include the complete set of features that we now associate
with the name “ML” (i.e., polymorphic type inference, datatypes with pattern matching, modules, exceptions,
and mutable state).
Standard ML, and the ML family of languages, have had enormous inuence on the world of programming
language design and theory. ML is the foremost exemplar of a functional programming language with strict
evaluation (call-by-value) and static typing. The use of parametric polymorphism in its type system, together
with the automatic inference of such types, has inuenced a wide variety of modern languages (where
polymorphism is often referred to as generics). It has popularized the idea of datatypes with associated
case analysis by pattern matching. The module system of Standard ML extends the notion of type-level
parameterization to large-scale programming with the notion of parametric modules, or functors.
Standard ML also set a precedent by being a language whose design included a formal denition with an
associated metatheory of mathematical proofs (such as soundness of the type system). A formal denition
was one of the explicit goals from the beginning of the project. While some previous languages had rigorous
denitions, these denitions were not integral to the design process, and the formal part was limited to the
language syntax and possibly dynamic semantics or static semantics, but not both.
The paper covers the early history of ML, the subsequent eorts to dene a standard ML language, and the
development of its major features and its formal denition. We also review the impact that the language had
on programming-language research.
CCS Concepts:
Software and its engineering Functional languages
;
Formal language denitions
;
Polymorphism;Data types and structures;Modules / packages;Abstract data types.
Additional Key Words and Phrases: Standard ML, Language design, Operational semantics, Type checking
ACM Reference Format:
David MacQueen, Robert Harper, and John Reppy. 2020. The History of Standard ML. Proc. ACM Program.
Lang. 4, HOPL, Article 86 (June 2020), 100 pages. https://doi.org/10.1145/3386336
Authors’ addresses: David MacQueen, Computer Science, University of Chicago, 5730 S. Ellis Avenue, Chicago, IL, 60637,
USA, dmacqueen@mac.com; Robert Harper, Computer Science, Carnegie Mellon University, 5000 Forbes Avenue, Pittsburgh,
PA, 15213, USA, rwh@cs.cmu.edu; John Reppy, Computer Science, University of Chicago, 5730 S. Ellis Avenue, Chicago, IL,
60637, USA, jhr@cs.uchicago.edu.
Permission to make digital or hard copies of part or all of this work for personal or classroom use is granted without fee
provided that copies are not made or distributed for prot or commercial advantage and that copies bear this notice and
the full citation on the rst page. Copyrights for third-party components of this work must be honored. For all other uses,
contact the owner/author(s).
©2020 Copyright held by the owner/author(s).
2475-1421/2020/6-ART86
https://doi.org/10.1145/3386336
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:2 David Maceen, Robert Harper, and John Reppy
Contents
Abstract 1
Contents 2
1 Introduction: Why Is Standard ML important? 4
2 Background 6
2.1 LCF/ML — The Original ML Embedded in the LCF Theorem Prover 7
2.1.1 Control Structures 8
2.1.2 Polymorphism and Assignment 8
2.1.3 Failure and Failure Trapping 8
2.1.4 Types and Type Operators 9
2.2 HOPE 9
2.3 Cardelli ML 9
2.3.1 Labelled Records and Unions 11
2.3.2 The ref Type 12
2.3.3 Declaration Combinators 12
2.3.4 Modules 13
3 An Overview of the History of Standard ML 13
3.1 Early Meetings 13
3.1.1 November 1982 13
3.1.2 April 1983 14
3.1.3 June 1983 14
3.1.4 June 1984 15
3.1.5 May 1985 15
3.2 The Formal Denition 15
3.3 Standard ML Evolution 16
3.3.1 ML 2000 16
3.3.2 The Revised Denition (Standard ML ’97) 17
4 The SML Type System 18
4.1 Early History of Type Theory 18
4.2 Milner’s Type Inference Algorithm 22
4.3 Type Constructors 25
4.3.1 Primitive Type Constructors 25
4.3.2 Type Abbreviations 26
4.3.3 Datatypes 27
4.4 The Problem of Eects 29
4.5 Equality Types 32
4.6 Overloading 33
4.7 Understanding Type Errors 33
5 Modules 34
5.1 The Basic Design 36
5.2 The Evolution of the Design of Modules 41
5.3 Modules and the Denition 46
5.4 Module Innovations in Version 0.93 of SML/NJ 46
5.5 Module Changes in Denition (Revised) 46
5.6 Some Problems and Anomalies in the Module System 47
5.7 Modules and Separate Compilation 48
6 The Denition of Standard ML 49
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:3
6.1 Early Work on Language Formalization 49
6.2 Overview of the Denition 51
6.3 Early Work on the Semantics 52
6.4 The Denition 54
6.5 The Revised Denition 58
7 Type Theory and New Denitions of Standard ML 59
7.1 Reformulating the Denition Using Types 61
7.2 A Mechanized Denition 62
7.3 Lessons of Mechanization 64
8 The SML Basis Library 64
8.1 A Brief History of the Basis Library 64
8.2 Design Philosophy 66
8.3 Interaction with the REPL 67
8.4 Numeric Types 67
8.4.1 Supporting Multiple Precisions 67
8.4.2 Conversions Between Types 68
8.4.3 Real Numbers 69
8.5 Input/Output 69
8.6 Sockets 70
8.7 Slices 72
8.8 Internationalization and Unicode 72
8.9 Iterators 73
8.10 Publication and Future Evolution 73
8.11 Retrospective 74
8.12 Participants 74
9 Conclusion 75
9.1 Mistakes in the Design of Standard ML 75
9.2 The Impact of Standard ML 75
9.2.1 Applications of SML 75
9.2.2 Language Design 76
9.2.3 Language Implementation 77
9.2.4 Concurrency and Parallelism 78
9.3 The Non-evolution of SML 79
9.4 Summary 80
Acknowledgments 80
A Standard ML Implementations 80
A.1 Alice 80
A.2 Edinburgh ML 81
A.3 HaMLeT 81
A.4 The ML Kit 81
A.5 MLton 81
A.6 MLWorks 81
A.7 Moscow ML 81
A.8 Poly ML 82
A.9 Poplog/SML 82
A.10 RML 82
A.11 MLj and SML.NET 82
A.12 Standard ML of New Jersey 82
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:4 David Maceen, Robert Harper, and John Reppy
A.13 SML# 83
A.14 TIL and TILT 83
References 83
1 INTRODUCTION: WHY IS STANDARD ML IMPORTANT?
The ML family of languages started with the meta language (hence ML) of the LCF theorem proving
system developed by Robin Milner and his research group at Edinburgh University from 1973
through 1978 [Gordon et al
.
1978b].
1
Its design was based on experience with an earlier version of
the LCF system that Milner had worked on at Stanford University [Milner 1972] .
Here is how Milner succinctly describes ML, the LCF metalanguage, in the introduction to the
book Edinburgh LCF [Gordon et al. 1979]:
. . .
ML is a general purpose programming language. It is derived in dierent
aspects from ISWIM, POP2 and GEDANKEN, and contains perhaps two new
features. First, it has an escape and escape trapping mechanism, well-adapted to
programming strategies which may be (in fact usually are) inapplicable to certain
goals. Second, it has a polymorphic type discipline which combines the exibility
of programming in a typeless language with the security of compile-time type
checking (as in other languages, you may also dene your own types, which may
be abstract and/or recursive); this is what ensures that a well-typed program
cannot perform faulty proofs.
This original ML was an embedded language within the interactive theorem proving system LCF,
serving as a logically-secure scripting language. Its type system enforced the logical validity of
proved theorems and it enabled the partial automation of the proof process through the denition
of proof tactics. It also served as the interactive command language of LCF.
The basic framework of the language design was inspired by Peter Landin’s ISWIM language
from the mid 1960s [1966b], which was, in turn, a sugared syntax for Church’s lambda calculus. The
ISWIM framework provided higher-order functions, which were used to express proof tactics and
their composition. To ISWIM were added a static type system that could guarantee that programs
that purport to produce theorems in the object language actually produce logically valid theorems
and an exception mechanism for working with proof tactics that could fail. ML followed ISWIM in
using strict evaluation and allowing impure features, such as assignment and exceptions.
Initially the ML language was only available to users of the LCF system, but its features soon
evoked interest in a wider community as a potentially general-purpose programming language.
Cardelli, then a graduate student at Edinburgh, decided to build a compiler for a free-standing
version of ML, initially called “ML under VMS” (also “VAX ML” in contrast to “DEC10 ML,” which
was used to refer to the LCF version), that allowed people to start using ML outside of LCF. A bit
earlier in Edinburgh, in parallel with later stages of the development of LCF, Rod Burstall, David
MacQueen, and Don Sannella designed and implemented a related functional language called
HOPE [1980]2that shared ML’s polymorphic types but added datatypes3to the type system.
1
Milner’s LCF group initially included postdocs Malcolm Newey and Lockwood Morris, followed in 1975 by Michael Gordon
and Christopher Wadsworth, plus a number of graduate students.
2
“HOPE,” despite being all-caps, is not an acronym, but refers to the name of the building where the language was born:
Hope Park Square.
3
We use the term “datatypes,” instead of the often used term “algebraic data types” because it is more concise and to avoid
confusion with algebraically specied types, which are usually restricted to rst-order types.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:5
What makes Standard ML (SML) an interesting and important programming language? For one
thing, it is an exemplar of a strict, statically typed functional language. In this rôle, it has had
a substantial inuence on the design of many modern programming languages, including other
statically-typed functional languages (e.g., OCaml, F#, Haskell, and Scala). It also gave rise to a
very substantial research community and literature devoted to investigation of the foundations
of its type and module system. The focus of this research community broadened in the 1990s to
encompass the foundations of (typed) object-oriented programming languages, as represented by
the work of Luca Cardelli, John Mitchell, Kim Bruce, Benjamin Pierce, and others.
More particularly, it is worth understanding how Standard ML pioneered and integrated its most
characteristic features.
Static typing with type inference and polymorphic types, now commonly known as the
Hindley-Milner type system.
Datatypes with the corresponding use of pattern matching for case analysis and destructuring
over values of those types.
A module sub-language that itself is a functional language with a substantial extension of
the basic type system of the core language.
A type safe exception mechanism providing more exible control structures.
Imperative (i.e., state mutating) programming embodied in the updatable reference construct.
This complex of features evolved through the design of the language and its early formal denition
in the 1980s [Harper et al
.
1987a], the rst published denition [Milner and Tofte 1991;Milner et al
.
1990], and were further rened in the revised denition published in 1997 [Milner et al
.
1997] and
the Basis library standard [Gansner and Reppy 2004]. Theoretical foundations were investigated
and extensions and variants were explored in multiple implementations of the language. Extensions
have included rst-class continuations [Harper et al
.
1993b] primitives to support concurrent
programming [Reppy 1991], and polymorphic record selection [Ohori 1992].
Another contribution of Standard ML was the emphasis on safety in programming, meaning that
the behavior of all programs, including buggy programs, would be fully determined by a dened
semantics of the language. Another way of characterizing safety is that programs cannot “crash”
and fail in unpredictable ways. In practice, safety was achieved by a combination of static type
checking and a run-time system that implements a safe memory model. Earlier languages, such as
Lisp, achieved a weaker form of safety with respect to primitive data structures of the language, but
the safety guarantees could not be extended to non-primitive abstractions such as binary search
trees.
Standard ML has also driven advances in compiler technology. Standard ML implementations
have exploited new techniques such as the use of continuation passing style and A-normal forms in
intermediate languages [Appel 1992;Tarditi 1996]. Another new technology involved type-driven
compilation and optimization, as represented by the TIL compiler at CMU [Tarditi et al
.
1996] and
the FLINT project at Yale [Shao 1997;Shao and Appel 1995] that translate type information in the
source language into typed intermediate languages and in extreme cases all the way to executable
code.
Finally, a distinctive attribute of Standard ML is that the language was intended to be formally
dened from the outset. Most languages (including other languages in the ML family such as OCaml
and F#) are dened by their implementation, but it was a major theme in the British programming
research culture that this was not good enough and that one should strive for mathematical rigor
in our understanding of what a programming language is and how programs behave. While there
were previous examples of languages with semi-rigorous denitions (e.g., McCarthy’s Lisp [1960]
and Algol 68 [Lindsey 1996;Mailloux et al
.
1969;van Wijngaarden et al. 1969]), these eorts were
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:6 David Maceen, Robert Harper, and John Reppy
post hoc. The formal denition of Standard ML diers from these earlier eorts in several respects;
most importantly, in that it was a primary goal of the designers of the language.
The remainder of the paper is organized as follows. We start with a discussion of the developments
that led up to the idea of dening a “Standard ML” in 1983, including a brief look at the history and
character of British programming language research, followed by a discussion of the immediate
precursors of Standard ML, namely LCF/ML, HOPE, and Cardelli ML (or “ML under VMS”).
We then give an overview of the history of design process and the personalities involved in
Section 3. This section explores the problems and solutions that arose during the evolution of the
design from 1983 through 1986; i.e., what were the key design decisions, and how were they made?
The next four sections focus on specic aspects of the language. First, Section 4describes the
type system for the core language. Then, Section 5focuses on the particular problems of design and
denition involved in the module system, which was, in some ways, the most novel and challenging
aspect of the language, and one which has produced a substantial research literature in its own right.
Section 6deals with the history of how the formal Denition of Standard ML was created from
1985 through 1989, leading to the publication of the 1990 Denition [Milner et al
.
1990].
4
After the
completion of the Denition, and even before, reseach continued on nding better type-theoretical
foundations for the static semantics, and also toward a more feasable and ideally mechanized
metatheory (proofs of safety and soundness). These developments are covered in Section 7. An
important part of the revised denition of SML was the standardization of a library of essential
modules for serious application development and interaction with the computing environment
(operating system, networking, etc..). This eort is described in Section 8.
Section 9concludes the paper with a discussion of several topics. These include mistakes in
the design of the language, a survey of its impact on programming language research, and its
application in the eld. We also discuss the debate over whether the language design should be
xed by the 1997 Revised Denition or be allowed to evolve. Lastly we describe the current state of
aairs.
2 BACKGROUND
In this section, we describe the technical and cultural context in which Standard ML was created.
Part of this context was formed by the culture and personalities of British programming language
research in the 1960s and 1970s. An important part is played by the set of precursor programming
languages developed at the University of Edinburgh, which directly informed and contributed to
the design of SML; namely the ML “Meta Language” of the LCF theorem proving system, the HOPE
language, and Cardelli’s dialect of ML.
ML was designed in a British culture of programming language theory and practice, and more
specically an Edinburgh culture, that prevailed during the 1970s and 80s. There was a large and
active articial intelligence community at Edinburgh that had a strong association with early work
on programming languages. This community was familiar with Lisp and its applications in AI, but
was also a center of logic programming and with an active Prolog user community. POP-2 [Burstall
et al. 1977] was a home-grown AI and symbolic programming language.
The British programming language scene at the time was also strongly inuenced by the work of
Christopher Strachey and Peter Landin. Strachey had developed the CPL conceptual language [1963;
1966] and had collaborated with Dana Scott on the development of denotational semantics [Scott and
Strachey 1971], whose metalanguage was essentially a variety of typed lambda calculus. Strachey
also coined the term parametric polymorphism [1967] for situations where a denition or construct
is parameterized relative to a type. Landin had promoted the use of the lambda calculus as a basis for
4Hereafter we refer to the 1990 denition as the “Denition.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:7
programming via the ISWIM conceptual language with an operational evaluation semantics [1964;
1966b], and as the target language for a translational semantics of Algol 60 [1965a;1965b]. Also
worth noting is Tony Hoare’s early work on abstract data types and language design, which
was well known to the Edinburgh community. A common thread of most of the British language
research at the time was an eort to formalize the basic concepts in programming languages and use
these formalizations for analysis, proof of metalinguistic properties, and as a guide to “principled”
language design.5
2.1 LCF/ML — The Original ML Embedded in the LCF Theorem Prover
We start with a brief look at some programming languages that inuenced the design of LCF/ML.
Lisp
6
was the language used in the Stanford LCF project [Milner 1972] that Milner had developed
in collaboration with Richard Weyhrauch and Malcolm Newey before coming to Edinburgh, and it
was familiar to all the ML design participants as the then de facto standard language for symbolic
computing. Lisp was a functional language that could manipulate functions as data, although its
behavior as a functional language was awed by the use of dynamic binding. It provided lists
as a general, primitive type and had an escape mechanism (catch and throw). Besides the fact
that it lacked proper function closures (true rst-class functions), its main drawback as a proof
assistant metalanguage was that it was dynamically typed and had no support for abstract types.
Lisp was the language in which the Stanford LCF system was implemented and also served as its
“meta-language.” The design of ML for Edinburgh LCF was informed by Milner’s experience at
Stanford; in fact, LCF/ML was implemented by translation into Stanford Lisp code.
ISWIM
7
was a conceptual language created by Landin as a general framework for future pro-
gramming languages [1966b] and as a tool for studying models of evaluation [1964]. ISWIM was
basically an applied, call-by-value lambda calculus with syntactic sugar (e.g., “
where
” and “
let
declaration constructs) for convenience. Landin also proposed an escaping control construct called
the J operator that was originally designed to model gotos in Algol 60 [1998]. Although ISWIM was
not statically typed, Landin used an informal, but systematic, way of describing data structures
such as the abstract syntax of ISWIM itself. This informal style of data description was a precursor
of the datatype mechanism introduced in HOPE and inherited by Standard ML.
Landin mentions a prototype implementation of ISWIM in [1966b], and he and James Morris
(at MIT) implemented an extension of ISWIM as the language PAL (Pedagogical Algorithmic
Language) [Evans, Jr. 1968a,b,1970;Wozencraft and Evans, Jr. 1970].8
POP-2 was designed by Robin Popplestone and Burstall in the late 1960s [1977;1968;1968a],
following on Popplestone’s earlier POP-1 language [1968b]. Like Lisp, POP-2 was a functional
language with dynamic binding and dynamic (i.e., runtime) typing, but its syntax was in the
ALGOL-style, and its features were heavily inuenced by the work of Strachey and Landin. It had
a number of notable features such as a form of function closure via partial application and an
escape mechanism (the jumpout function) inspired by Landin’s J operator that plays a rôle similar
to catch/throw in Lisp. The design of ML was also inuenced by POP-2’s record structures and by
5
It is interesting to note that most of the central personalities rst met through an unocial reading group formed by an
enthusiastic amateur named Mervin Pragnell, who recruited people he found reading about topics like logic at bookstores or
libraries. The group included Strachey, Landin, Rod Burstall, and Milner, and they would read about topics like combinatory
logic, category theory, and Markov algorithms at a borrowed seminar room at Birkbeck College, London. All were self-taught
amateurs, although Burstall would later get a PhD in operations research at Birmingham University. Rod Burstall was
introduced to the lambda calculus by Landin and would work for Strachey briey before moving to Edinburgh in 1965.
Milner had a position at Swansea University before spending time at Stanford and taking a position in Edinburgh in 1973.
6Originally spelled “LISP,” for LISt Processing.
7A quasi-acronym for “If you see what I mean.
8PAL development was continued by Arthur Evans and John M. Wozencraft at MIT.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:8 David Maceen, Robert Harper, and John Reppy
its sections, a simple mechanism for name-space management that provided rudimentary support
for modularity.
GEDANKEN [Reynolds 1970] was another conceptual language dened by John Reynolds based
on the principle of completeness (meaning all forms of values expressible in the language were
“rst-class,” in the sense that they could be returned by functions and assigned to variables), and the
concept of references as values that could contain other values and be the targets of assignments.
GEDANKEN also included labels as rst-class values, a feature that enabled unconventional control
constructs.
We now proceed to ML as it existed in the interactive LCF theorem proving system, where it
served as the metalanguage (i.e., proof scripting language) and command language. We refer to this
original version of the language as LCF/ML. The most important feature of LCF/ML was its static
type system, with (parametric) polymorphic types and automatic type inference [Milner 1978].
This type system is explored in detail in Section 4.
A major benet of this typing algorithm was that it did not require the programmer to explicitly
specify types (with rare exceptions), but instead inferred the types implicit in the code. This feature
preserves some of the simplicity and uidity of dynamically typed languages like Lisp and POP-2,
and was particularly important for LCF/ML’s use as a scripting and command language.
2.1.1 Control Structures. Like ISWIM, LCF/ML was at its core a call-by-value applicative language
with basic expressions composed using function application. On top of this base functional language
were added imperative features in the form of assignable variables, with a primitive assignment
operator to update their contents, iterative control structures (loops), and failure (i.e., exception)
raising and trapping constructs.
The language oered an interesting combined form for conditionals and loops, where either the
then
or
else
keywords of a standard conditional expression could be replaced by
loop
to turn the
conditional into an iterative statement. These conditional/loop expressions could also be nested. As
an example, here is the code for the gcd function:
let gcd (x,y) =
letref x,y=x,y
in i f x>yloop x: = xy
if x<yloop y: = yx
else x; ;
In this example, control returns to the top of the whole conditional after executing either of the
loop
branches. The expected
else
keyword following the rst loop can be elided, as it is here. This
example also illustrates the letref declaration form for introducing assignable variables.
2.1.2 Polymorphism and Assignment. A notable complication in the type inference algorithm for
ML arose from the interaction between polymorphic types and assignable variables introduced by
letref
declarations. It was soon discovered that without some sort of restriction on polymorphic
typing of such variables, the type system would be unsound. One rather ad hoc and unintuitive
solution was devised for LCF/ML, but the search for better solutions continued for many years as
described in some detail in Section 4.4.
2.1.3 Failure and Failure Trapping. Another feature of LCF/ML that was motivated by the needs
of programming proof tactics was an exception or failure mechanism. Failures were identied by
tokens, which were strings of characters enclosed in backquotes. To signal failure, one used the
syntax “
failwith t
,” which would signal a failure/exception labeled by the token
t
, and failures could
be trapped using the syntax “
e1?e2
,” where a failure in
e1
would result in the expression
e2
being
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:9
evaluated. One could also specify a list of failure tokens to trap using the syntax “
e1?? tokens e2
,
where
tokens
was a space-separated sequence of strings enclosed in double backquotes. A novel
aspect of the failure mechanism was the iterative form of failure trapping (using “
!
” and “
!!
” instead
of “
?
” and “
??
”) These forms would repeat execution of the expression
e1
after the handler
e2
had
run, until
e1
evaluated without failure. Proof tactics can fail for various reasons, and failure trapping
is useful for dening higher-order tactics (tacticals) that can detect failures and try an alternate
tactic (or the same tactic after some adjustment).
2.1.4 Types and Type Operators. The basic types of LCF/ML are
int
,
bool
,
token
(i.e., string), and a
trivial type denoted by “
.
” that has a single element, the null-tuple “
()
.” There are also built-in types
term
,
form
,
type
, and
thm
representing the terms, formulae, types, and theorems of the PPLAMBDA
logic (the object language of the LCF system). The built-in type constructors include binary products
ty1#ty2,” binary disjoint sums “ty1+ty2,” function types “ty1>ty2,” and list types “ty list.”
The
list
type constructor could be considered a predened abstract type except that special
syntax was provided for list expressions.
Another aspect of the type system of LCF/ML was the ability to dene new types. These could
be either simple abbreviations for type expressions composed of existing types and type operators,
or they could be new abstract types, dened using the
abstype
declaration keyword. Abstract
type denitions specied a hidden concrete representation type and a set of exported values and
functions that provided an interface for creating and manipulating abstract values. Abstract types
could be parameterized and they could be recursive (i.e., an abstract type could appear in its own
concrete representation). Abstract types played an important rôle in the LCF theorem proving
system, where
thm
was an abstract type and the functions it provided for building elements of the
type corresponded to valid inference rules in the logic of LCF, thus ensuring by construction that a
value of type
thm
must be proveable. As noted by Gordon et al. [1978b], the
abstype
construct is
similar to the notion of “clusters” in the CLU language [Liskov et al. 1981].
2.2 HOPE
The HOPE programming language [Burstall et al
.
1980] was developed by Rod Burstall’s research
group in the School of Articial Intelligence at Edinburgh just after LCF/ML from 1977 to 1980.
This language was inspired by the simple rst-order equational language that was the subject of
Burstall and Darlington’s work on program transformation [1977]. Burstall had designed a toy
language called NPL [1977] that added a simple form of datatype [1969] and pattern matching
to the earlier equational language. David MacQueen and Burstall (later joined by Don Sannella),
added LCF/ML’s polymorphic types and rst-class functions (lambda abstractions) to create the
HOPE language. From the perspective of the later evolution of ML, the notable features of HOPE
were its datatypes, pattern matching over datatypes, clausal (or equational) function denitions,
and the combination of polymorphic type inference with overloading of identiers.
2.3 Cardelli ML
We start by giving a brief history of Cardelli’s dialect of ML, also known as “ML under VMS” (the
title of the manual), before considering the language innovations it introduced [Cardelli 1982d].9
Cardelli began working on his ML compiler sometime in 1980. The compiler was developed
on the Edinburgh Department of Computer Science VAX/VMS system, so Cardelli sometimes
informally called his version “VAX ML” to distinguish it from LCF/ML, which was also known
9
Note that Cardelli was building his VAX ML at the same time as he was doing research for his PhD thesis [Cardelli 1982a].
He also developed his own text formatting software, inspired by Scribe, that he used to produce his thesis and the compiler
documentation, and a simulation of the solar system!
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:10 David Maceen, Robert Harper, and John Reppy
as “DEC-10 ML,” because it ran under Stanford Lisp on the DEC-10/TOPS-10 mainframe [Cardelli
1981].10
Cardelli’s preferred working language at the time was Pascal, so both the compiler and the
runtime system were written in Pascal, using Pascal’s unsafe union type to do coercions for
low-level programming (e.g., for the garbage collector).
11
Cardelli’s ML compiler for VAX/VMS
generated VAX machine code, and was much faster than the LCF/ML (DEC-10) version, which
translated ML to Lisp code, which was interpreted by the host Lisp system. The compiler was
working and was made available to users in the summer of 1981 (version 12-6-81, distributed on
June 12, 1981), although a working garbage collector was not added until version 13-10-81.
The earliest surviving documents relating to the compiler date to late 1980: “The ML Abstract
Machine,” a description of the abstract machine AM [Cardelli 1980a] (which would develop into the
FAM [Cardelli 1983a]), and “A Module Exchange Format,” a description of an external string format
for exporting ML runtime data structures [Cardelli 1980b]. There is a README le titled “Edinburgh
ML” from March 1982 that describes how to install and run the system [Cardelli 1982b], and a
partial manual titled “ML under VMS” providing a tutorial introduction to the language [Cardelli
1982d], corresponding to Section 2.1 of “Edinburgh ML” [Gordon et al. 1979].
In early 1982, Nobuo Saito, then a postdoc at CMU, ported Cardelli’s ML compiler to Unix, using
Berkeley Pascal [1982]. In April 1982, Cardelli completed his PhD at Edinburgh [1982a] and moved
to the Computing Science Research Center (the birthplace of Unix) at Bell Labs, and immediately
began his own Unix port, which was available for distribution in August 1982. The runtime system
for the Unix port was rewritten in C, but most of the compiler itself remained in Pascal. The rst
edition of the Polymorphism newsletter
12
contained a list of known distribution sites [Cardelli
1982c]. At that time, there were at least 23 sites spread around the world, several using the new Unix
port. The Unix port had three releases during 1982 (13-8-82, 24-8-82, and 5-11-82), accompanied
with some shifts in language design and system features, notably a new type checker for ref types
and an early version of le I/O primitives. The manuals for these releases were all titled “ML under
Unix,” instead of “ML under VMS.
The next major milestone was the rst Standard ML meeting in Edinburgh in April 1983 (see
Section 3.1). Cardelli agreed to a request from Milner to suspend work on the manual for his ML
dialect (now titled “ML under Unix”) pending developments in response to Milner’s initial proposal
for Standard ML [Milner 1983d]. Following the meeting Cardelli began to change his compiler to
include new features of the emerging Standard ML design, resulting in Pose 2 (August 1983),
13
Pose 3 (November 1983), and nally Pose 4 (April 1984). This last version is described in the paper
“Compiling a Functional Language” [Cardelli 1984a].
The rst description of the Cardelli’s version of ML was a le mlchanges.doc [Cardelli 1981] that
was part of his system distribution. This le describes the language by listing the changes made
relative to LCF/ML (DEC-10 ML). The changes include a number of minor notational shifts. For
instance, LCF/ML used “
.
” (period) for list cons, while Cardelli ML initially used “
_
,” later shifting
to the notation “
::
” used in POP-2 [Burstall and Popplestone 1968]. The trivial type (called “
unit
in Standard ML) was denoted by “
.
” in LCF/ML and by “
triv
” in Cardelli ML. A number of features
10
Cardelli sometimes referred to these implementations as two varieties of “Edinburgh ML,” but that name was used later
for the self-bootstrapping port of Cardelli ML implemented by Kevin Mitchell and Alan Mycroft.
11
Since the entire system was written in Pascal, there was no sharp distinction between the compiler and the runtime,
which was simply the part of the system responsible for executing the abstract machine instructions (FAM code).
12
Polymorphism – The ML/LCF/Hope Newsletter was self published by Cardelli and MacQueen at Bell Laboratories and
distributed by physical mail to interested parties. Electronic copies are available at either http://lucacardelli.name/indexPapers.
html or http://sml-family.org/polymorphism.
13Cardelli called his compiler versions “Poses” adopting a terminology from dance.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:11
of LCF/ML were omitted from Cardelli ML, e.g., the “
do
” operator, sections, and the “
!
” and “
!!
looping failure traps [Gordon et al. 1979, Chapter 2].
But the really interesting changes in Cardelli ML involved new labelled record and union types,
the ref type for mutable values, declaration combinators for building compound declarations, and
modules. We describe these in the following subsections.
2.3.1 Labelled Records and Unions. Cardelli was of course familiar with the conventional record
construct provided in languages such as Pascal [Jensen and Wirth 1978, Chapter 7]. But inspired by
Gordon Plotkin’s lectures on domain theory, Cardelli looked for a purer and more abstract notion,
where records and discriminated union types were an expression of pure structure, representing
themselves without the need of being declared and named. The notation for records used decorated
parentheses:
( | a1=e1; . . . ; an=en| ) : ( | a1:t1; . . . ; an:tn| )
where
ei
is an expression of type
ti
. The order of the labelled elds in a record type did not matter
— any permutation represented the same type.
Accessing the value of a eld of a record was done using the conventional dot notation:
r.a
,
where
r
is a record and
a
is a label. Records could also be deconstructed in declarations and function
arguments by pattern-matching with a record varstruct (pattern), as in the declaration:
let ( | a=x;b=y| ) = r
From the beginning, Cardelli included an abbreviation feature for record patterns where a eld
name could double as a default eld variable, so
let ( | a;b| ) = r
would implicitly introduce and bind variables named
a
and
b
to the respective eld values in
r
. All
these features of the VAX ML record construct eventually carried over to Standard ML, but with a
change in the bracket notation to use {...} .
Labelled unions were expressed as follows, using decorated square brackets:
[ | a1=e1| ] : [ | a1:t1; . . . ; an:tn| ]
The union type to which a given variant expression belonged had to be determined by the context
or given explicitly by a type ascription. Variant varstructs could be used in varstructs for declaration
and argument bindings, with their own defaulting abbreviation where a
[| a|]
stood for
[| a= ()|]
,
both in varstructs and expressions, which supported an enumeration type style.
14
A case expression
based on matching variant varstructs was used to discriminate on and deconstruct variant values,
with the syntax
case e
of [ | a1=v1.e1;
· · ·
an=vn.en
| ]
where eis of type [| a1:t1; ... ; an:tn|] and the vihave type tiin ei.
14
Initially, the record and variant abbreviation conventions were also applied to types, but this was not found useful and
was quickly dropped.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:12 David Maceen, Robert Harper, and John Reppy
2.3.2 The
ref
Type. In LCF/ML, mutable variables could be declared using the
letref
declaration.
Cardelli replaced this declaration form with the
ref
type operator with its interface consisting of
the operations
ref
,
:=
, and
!
. This approach was carried over unchanged into Standard ML, though
the issue of how
ref
behaved relative to polymorphism took many years to resolve, as is discussed
below in Section 4.4.
2.3.3 Declaration Combinators. Another innovation in Cardelli ML was a set of (more or less)
independent and orthogonal declaration combinators for building compound declarations [Cardelli
1982d, Section 1.2]. These combinatorsare
enc
, for sequential composition, equivalent to nesting
let
-bindings: “
d1 enc d2
” yields the
bindings of d1 augmented with or overridden by the bindings of d2.
and, for simultaneous or parallel composition, usually used with recursion.
ins
, for localized declarations: “
d1 ins d2
” yields the bindings of
d2
, which are evaluated in
an environment containing the bindings of d1.
with
, a kind of hybrid providing the eect of
enc
for type bindings and
ins
for value bindings;
usually used with the special “<=>” type declaration to implement abstract types.
rec, for recursion
There were also reverse forms of
enc
and
ins
called
ext
and
own
for use in
where
expressions, thus
let d1 enc d2 in e” is equivalent to “ewhere d2 ext d1.
This “algebra” of declarations (possibly inspired by ideas in Robert Milne’s PhD thesis [1974])
was interesting, but in programming, the combinators would normally be used in a few limited
patterns that did not take advantage of the generality of the idea. Indeed, certain combinations
seemed redundant or problematic, such as “
rec rec d
,” or “
rec d1 ins d2
” (
rec
syntactically binds
weaker than the inx combinators).
Cardelli factored the
abstype
and
absrectype
of LCF/ML using
with
(and possibly
rec
) in combina-
tion with a special type declaration form “
tname <=> texp
” that produced an opaque type binding
15
of tname to the type expression texp together with value bindings of two isomorphism functions:
abstname :texp >tname
reptname :tname >texp
A compound declaration “
tname <=> texp with decl
” would compose the type binding of
tname
with
decl
while localizing the bindings of
abstname
and
reptname
to
decl
. Thus
with
acted like
enc
at the type level and
ins
at the value level. This mechanism was, in principle, more general
than
abstype
/
absrectype
in LCF/ML, in that the declaration
d1
in
d1 with d2
was arbitrary and not
restricted to a possibly recursive simultaneous set of isomorphism (
<=>
) type bindings, but it was
not clear how to exploit increased generality.
In the end, the
and
combinator was used in Standard ML, but at the level of value and type bindings,
not declarations (just as it was used in LCF/ML), the
ins
combinator became the “
local din eend
declaration form, the
rec
combinator was adopted, but at the level of bindings,
16
and the
<=>
,
with
combination were replaced by a variant of the old
abstype
declaration, but using the datatype form
for the type part and restricting the scope of the associated data constructors.
In later versions, starting with “ML under Unix,” Pose 2 [Cardelli 1983b], another declaration
form using the export keyword was added. A declaration of the form
export exportlist from decl end
15Meaning that tname was not equivalent to texp.
16
Arguably, the syntax was still too general, since it allowed an arbitrary number of
rec
annotations to be applied to a
binding.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:13
produced the bindings of decl, but restricted to the type and value names listed in the exportlist.
Exported type names could be specied as abstract (in ML under Unix, Pose 4) meaning that
constructors associated with the type were not exported. Thus both
local
and
abstype
declarations
could be translated into export declarations.
2.3.4 Modules. LCF/ML had no module system; the closest approximation was a
section
directive
that could delimit scope in the interactive top-level. Since Cardelli ML aimed to support general
purpose programming, Cardelli provided a basic module system. A module declaration was a named
collection of declarations. Modules were independent of the environment in the interactive system,
and explicit import declarations were required to access the contents of other modules (other
than the standard library of primitive types and operations, which was pervasively accessible, i.e.,
implicitly imported into every module). Cardelli called this feature a module hierarchy. Compiling
a module denition produced an external le that could be loaded into the interactive system or
accessed by other modules using the import declaration. Importing (loading) a module multiple
times would only create one copy, so two modules
B
and
C
that both imported a module
A
would
share a single copy of
A
. The export declaration was commonly used to restrict the interface
provided by a module. There was no way to separately specify interfaces (thus no equivalent of
signatures in Standard ML).
3 AN OVERVIEW OF THE HISTORY OF STANDARD ML
In this section, we survey the history of the design and evolution of Standard ML (which we will
free to abbreviate as SML from here on). This section provides a historical framework for the more
technically focused sections that follow.
3.1 Early Meetings
3.1.1 November 1982. By the fall of 1982, interest in the functional-programming systems developed
in Edinburgh (LCF/ML, Cardelli ML, and HOPE) had spread through the British programming
research community and beyond (to INRIA in France in particular). This interest led to the convening
of a meeting titled “ML, LCF, and HOPE” at the Rutherford Appleton Laboratory (RAL) in mid-
November of 1982 under the sponsorship of the SERC
17
Software Technology Initiative, chaired
by R. W. Witty [Wadsworth 1983]. The 20 attendees included a large contingent from Edinburgh
(Robin Milner, Rod Burstall, etc.) and representatives from RAL, Oxford, Imperial College, and
Manchester.
The discussions at this meeting focused on strategy for further development of ML, LCF, and
HOPE, including new implementations and ports of existing implementations. Because there were
already multiple implementations (forks) of both ML (the LCF/ML embedded metalanguage and
Cardelli ML), LCF (new implementations with variations at Cambridge/INRIA and Chalmers), and
HOPE (the original POP-2 implementation, MacQueen’s Lisp port, and work on compilers for the
Alice machine in Darlington’s group at Imperial College), there was concern about duplication and
dispersion of eort among dierent design and implementation forks.
The question of merging the ML and HOPE languages to reduce duplication came up, and,
interestingly, Milner’s response was reported by Chistopher Wadsworth as follows:
There was some discussion about the merits of propagating two languages (HOPE
and ML). R. Milner summarized the general view that HOPE and ML represent
two points in a spectrum of possible (typed) functional languages. Diering aims
had led to dierent compromises in the design of the two languages and both
17Science and Engineering Research Council, the British equivalent of the National Science Foundation in the US.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:14 David Maceen, Robert Harper, and John Reppy
should continue to be developed. The ultimate functional language could not be
envisaged yet — variety and experimentation, rather than standardization, were
needed at this stage.
Thus it is clear that Milner was not contemplating a merged language at this point. Bernard Sufrin
(who attended the RAL meeting), reports, however, that he had further discussions with Milner
at another meeting (possibly in York) that fall where he urged Milner to consider a new, unied
version of ML that might subsume Cardelli’s dialect and even HOPE. Milner refers to Sufrin’s
suggestion in the second draft proposal [1983c].
The notes for the November 1982 meeting, along with a response from Michael Gordon and
Larry Paulson (Cambridge University), who were not able to attend the meeting, were published in
Polymorphism [Wadsworth 1983].
3.1.2 April 1983. By April 1983, Milner had written a manuscript proposal for a new language
that he called “Standard ML” [1983d].
18
Milner emphasised that he was thinking of a conservative
revision of the original ML design that was not intended to introduce novel features, but rather
to consolidate ideas that had developed around ML. The language was presented as a minimal
“Bare ML” that was then eshed out with standard abbreviations and alternate forms that could be
dened in terms of Bare ML. In the Bare ML language, notable changes to LCF/ML included the
introduction of HOPE-style algebraic datatypes, pattern matching over datatype constructors, and
clausal (or equational) function denitions.
Cardelli’s labeled records were not adopted, leaving the binary product types and pairing operator
of LCF/ML in place. Similarly, Cardelli’s labeled variants were regarded as redundant given that
datatypes provided tagged unions, with the datatype constructors serving as tags, and datatypes
could also be used to dene the equivalent of LCF/ML’s binary tagged union operator on types.
The elaborate conditional/iterative control constructs of LCF/ML were dropped and case expres-
sions and simple conditionals were derived using application of a clausal function. Similarly, the
elaborate conditional/iterative failure trapping construct was replaced by simple
escape
and
trap
expression forms, with escape transmitting a token (that is, a string) as before.
Serendipitously, it happened that in the Spring of 1983 there was a signicant group of PL
researchers gathered in Edinburgh,
19
who met in the parlor of Milner’s home for three days in
April to discuss Milner’s proposal.
3.1.3 June 1983. Following the meetings at Milner’s home, there was a period of further discussion,
mostly via the post,
20
and the circle of discussion widened somewhat to include Gérard Huet at
INRIA and Stefan Sokolowski at the Polish Academy of Science in Gdansk.
The discussions at the meeting and after resulted in Milner’s producing and distributing a longer,
revised version of his proposal in June [1983a;1983c], which was followed by further discussion.
This second proposal added equality types and equality polymorphism to the language, using a
restricted form of bound type variables.
Further discussion lead to the rst typewritten design proposal in November of 1983; the rst
two drafts had been handwritten. The introduction of this draft notes that the issues of input/output
18
The name “Standard ML” was not viewed as permanent, and it did not imply a formal standardization process, but simply
an attempt to re-unify the dialects that had come to exist, namely LCF/ML and Cardelli ML. The fact that the name stuck
was unfortunate, since in the end a new language was produced, and there was no sanctioned “standard.
19
Participating: Rod Burstall, Luca Cardelli (Bell Labs), Guy Cousineau (INRIA), Michael Gordon (Cambridge), David
MacQueen (Bell Labs), Robin Milner, Kevin Mitchell, Brian Monahan, Alan Mycroft, Larry Paulson (Cambridge), David
Rydeheard, Don Sannella, David Schmidt, John Scott; unless otherwise noted, all from Edinburgh.
20
None of the institutions involved were connected to the Arpanet, and email was not yet a routine means of communication.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:15
and separate compilation were contentious and, thus, the proposal focuses on dening a “core”
language.
3.1.4 June 1984. In June 1984, Milner organized a slightly more formal second meeting in Edinburgh
to rene and extended the design. This second meeting was more planned than the rst one, with
Milner distributing an invitation and timetable [1984a] in advance of the meeting. A meeting report
was written by MacQueen and Milner, and published in the Polymorphism newsletter the following
January [1985]. The meeting also resulted in the publication of two papers — one on the Core
language [Milner 1984b] and a second on the module system [MacQueen 1984] — at the 1984 Lisp
and Functional Programming Conference in Austin, Texas.
3.1.5 May 1985. The third design meeting was a three-day aair held in Edinburgh in May 1985.
This meeting was more elaborate, with a number of position papers being presented [Milner 1985b].
A meeting report was prepared by Robert Harper [1985].
A major topic of discussion at the third meeting, which consumed the whole rst day, was
a proposal by MacQueen for a Standard ML module system. This initial proposal was based on
MacQueen’s earlier conceptual work on a module system for HOPE [1981]. The design was inspired
partly by work on parametric algebraic specications, particularly Burstall and Goguen’s Clear
specication language [1977], and partly by research in type theory and type-based logics (e.g.,
by Per Martin Löf [1982;1984]). The main elements of the design were signatures (interface
specications), modules (implementations of interfaces), and parametric modules, called functors.
Modules could be nested inside other modules and functors.
The design issues involved in the module system proved to be surprisingly subtle, and much
discussion went on during the period from 1984 through 1988 on the details of the design and the
methods for its formal denition. Major issues were coherence (sharing) among functor parameters,
the nature of signature matching (whether it was coercive or not), and the static identity of modules
(i.e., are module declarations generative). These issues are discussed in detail in Section 5.
While modules were the dominant topic of discussion at this meeting, an afternoon was given
over to discussing other topics. These included early proposals for some form of labeled tuples
(what would eventually become the record type of SML), and various syntax issues.
The 1985 meeting was also the rst meeting that did not include participation from the FORMEL
group at INRIA. By this time, they had decided to develop a separate dialect of ML that would be
free from the need to adhere to a standard. This version of ML became the Caml (and eventually
OCaml) branch of the language; the rst release was in 1987 and compiled down to the virtual
machine of the LeLisp system (also developed at INRIA) [OCaml-history 2019].
3.2 The Formal Definition
Up to this point, the design proposals and discussion were carried out on an informal basis, with
considerations of formal semantics in the background. But with the arrival of Robert Harper in
Edinburgh in the Summer of 1985, work began on the semantics of the language and subsequent
design work was generally integrated with that eort.
The development of the formal denition of Standard ML began after the May 1985 meeting.
It was primarily developed by Milner, Mads Tofte, and Harper, with input from Dave MacQueen
and other participants in the design process, including Burstall, Kevin Mitchell, Alan Mycroft,
Lawrence Paulson, Don Sannella, John Scott, and Christopher Wadsworth. By April 1985, Milner
had produced a third draft of “The Dynamic Operational Semantics of Standard ML” [1985a].
This was an evaluation semantics (“big step”) in the same operational style eventually used in
the Denition. Don Sannella had also developed a denotational semantics of the module system
[Sannella 1985], which revealed some issues and ambiguities in the informal description [Milner et al
.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:16 David Maceen, Robert Harper, and John Reppy
1990, Appendix E]. Harper wrote the rst version of a static semantics in the summer of 1985, but
the rst published version of the static semantics appeared in 1987, including the characterization
of principal signatures for modules [Harper et al
.
1987b]. The development of the semantics was
undertaken as Tofte’s Ph.D. dissertation research [1988], which examined many issues in the
language itself and in the semantic methodology needed to dene it. Milner and Tofte extensively
developed these ideas into The Denition of Standard ML, which was published in 1990 [Milner
et al
.
1990]. A companion volume, Commentary on Standard ML [Milner and Tofte 1991], was
published shortly thereafter by Milner and Tofte. The Commentary provided an analysis of design
alternatives and fundamental properties of the denition, particularly the static semantics. These
two volumes served as the foundation for some subsequent implementations, most notably the
ML Kit Compiler [Birkedal et al
.
1993], and spurred a large body of research on the language’s
metatheory and on the methodology of formal denition. These topics are explored further in
Section 6and Section 7.
3.3 Standard ML Evolution
By the early 1990s, a great deal of work had gone into the design, formal denition, and imple-
mentation of Standard ML, and substantial applications had been built (including Standard ML
compilers written in Standard ML). Some technical bugs, gaps, and ambiguities had been discovered
in the Denition, and some problems or weaknesses in the design itself had become evident. Thus
people in the community were beginning to feel that at least a minor revision was warranted to
correct acknowledged mistakes.
3.3.1 ML 2000. In January 1992, at the POPL conference in Albuquerque, New Mexico, a small
group consisting of Robert Harper, John Mitchell, Dave MacQueen, Luca Cardelli, and John Reppy
21
had lunch together and began to discuss whether the time was right to think about a next generation
of ML, based on the accumulated experience with design, implementation, use, and theoretical
developments related to ML (both SML and Caml).
The idea was more ambitious than to just make minor corrections or modications to Standard
ML. The group was open to a more radical redesign that might even re-integrate the Caml fork, by
trying for a language design that would be seen as an improvement over both Standard ML and
Caml.
This initial meeting lead to a series of meetings (roughly two per year, usually connected with
either POPL, ICFP, or IFIP Working Group 2.8, but with a couple ad hoc meetings at a cabin at Lake
Tahoe) with a gradually expanding group of interested researchers, including Xavier Leroy and
Didier Rémy from the Caml community.
A number of relatively minor technical issues were discussed, but it turned out that the central
question was whether ML would be improved by adding object-oriented features. At this time,
Cardelli and Mitchell were both involved in developing type-theoretic foundations for object-
oriented languages, and Didier Rémy and his student Jérôme Vouillon were beginning work that
would result in Objective Caml (rst released in 1996) [OCaml-history 2019]. Kathleen Fisher
and Reppy would also explore the idea extending ML with object-oriented features in the Moby
language [1999;2002]. But another point of view, espoused by Harper and MacQueen among others,
was that the ideas underlying object-oriented programming were both excessively complex and
also unsound from a methodological point of view [MacQueen 2002].
The dispute between the advocates and the opponents of object-oriented features was never
resolved, leading to a failure of the group to produce a new language design. Jon Riecke, however,
21The exact cast of characters is uncertain — memories dier.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:17
was able to write up a report on the ideas on which there was general agreement [ML2000 Working
Group 1999]. In this report, ML2000 was envisioned as having the following properties:
A module system based on SML modules, but with a well-dened semantics of separate
compilation and support for higher-order functors. There were, however, competing views
on how to realize the latter mechanism, with some arguing for the approach taken in SM-
L/NJ [MacQueen and Tofte 1994], whereas others favored the “applicative functor” approach
taken in OCaml [Leroy 1995].
Support for object-oriented programming, including a notion of object types and a subtyping
relation. Type specications in a signature could specify subtyping relationships (i.e., that an
abstract exported type was a subtype of some other type).
Concurrency would be included using a design based on the
event
type constructor and
supporting combinators found in Concurrent ML [Reppy 1999]. There was also an awareness
that the semantics of ML2000 would have to take into account relaxed memory-consistency
models; an idea that has become standard in recent years.
Instead of the single extensible datatype (
exn
) in SML, there would be a general mechanism
for dening “open” datatypes based on a design by Reppy and Riecke [1996].
In addition, there were various minor changes, such as adopting modulo arithmetic and
eliminating polymorphic (or generic) equality.
3.3.2 The Revised Definition (Standard ML ’97). Within the core SML community, the ML 2000
discussions did bear concrete fruit.
By the mid 1990s, signicant experience with the Standard ML design had been accumulated
through the work on the formal denition, multiple implementation eorts, and the development
of reasonably large software systems written in SML. A core group of designers, Milner, Tofte,
Harper, and MacQueen,
22
felt it would be valuable to attempt a modest revision of the language
design, leading to the creation of Standard ML ’97 and the publication of the Denition of Stan-
dard ML (Revised). This revision simplied the Core type system in signicant ways, notably by
introducing the value restriction rule to control the interaction of polymorphism and mutable data,
eliminating the need for “imperative” type variables that were used to express a weakened form of
polymorphism [Wright 1995].
It also both simplied and enriched the module system. Signatures were made more expressive
by introducing denitional (or translucent) type specications, which had been implemented
in Standard ML of New Jersey in 1993 (Version 0.93) and proposed in papers by Harper and
Lillibridge [1994] and Leroy [1994] at POPL 1994. Opaque signature matching (using the delimiter
:>
”) was added, and the notion of structure sharing was weakened to implied type sharing. These
issues will be discussed more fully below in Section 5.
Along with changes to the language, the Basis Library was substantially revised and expanded
to match common practice. New built-in types were added for characters, unsigned integers, and
multiple precisions of numeric types were supported. These changes required modications to the
syntax of the language, as well as extending the overloading mechanism to numeric literals.
The discussion of these revisions took place over a period of about a year (1995-1996) and Tofte
carried out the detailed revision of the semantic rules. The revised Denition [Milner et al
.
1997]
was published in 1997 — hence the revised language is commonly known as SML ’97.
22Brought together in Cambridge in the fall of 1995 by the Semantics of Computation program at the Newton Institute.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:18 David Maceen, Robert Harper, and John Reppy
4 THE SML TYPE SYSTEM
This section covers the background, design, theory, and implementation(s) of the ML type system.
We start with a look at the early history of the concept of types as it arose in research in the
foundations of mathematics. We then consider the issue of implicit typing and type inference, and
the related notion of parametric polymorphism.
The polymorphic type inference system used in ML (and related languages) is owed to Robin
Milner, who independently rediscovered the ideas that had been previously explored by logicians
(Haskell Curry, Max Newman, and Roger Hindley). But Robin Milner added the essential idea of
let-polymorphism, or introducing polymorphism at local declarations, along with his now-classic
“Algorithm W” for performing type inference [1978].
The two theoretical properties that the type system strives to achieve are soundness and princi-
pality. Soundness roughly means that a statically well-typed program will not fail with a certain
class of errors called type errors. Principality roughly means that the type inferred for a program or
expression is as general as possible – any other valid typing would be a specialization of this most
general type. We also discuss the interaction of polymorphism and eects. Handling eects in a
sound fashion disrupts the theoretical simplicity of the type system and conicts with principality.
Pragmatically the problem of eects was initially addressed by complications in the type system
such as imperative type variables. Later, in the Denition (Revised), the decision was made to simplify
the type system by introducing the value restriction, which is described below.
We then move on to consider the structure of types (products, sums, etc.). Here the major inno-
vation in ML is the notion of datatypes. These were implicit in Peter Landin’s informal conventions
for dening data types in ISWIM [1966a;1966b] and were developed further by Rod Burstall in his
language NPL [1977], and nally in a fully developed form in HOPE [Burstall et al. 1980].
Finally we address some implementation issues and techniques related to the type system.
4.1 Early History of Type Theory
The development of the theory of types that eventually led to the Standard ML type system goes
back more than a hundred years. It is covered in considerable detail and depth in references such as
“A Modern Perspective on Type Theory, From its Origins until Today” [Kamareddine et al
.
2004],
“Lambda-Calculus and Combinators in the 20th Century” [Cardone, Jr. and Hindley 2009], and
“The Logic of Church and Curry” [Seldin 2009].
23
The following is a summary of some of the most
relevant ideas from this history.
In the late 19th century, Georg Cantor, Gottlob Frege, Giuseppe Peano, and others endeavored
to provide rigorous logical foundations for mathematics, particularly arithmetic, but also real
analysis (with Richard Dedekind and Augustin-Louis Cauchy). Cantor created his set theory [1874]
in pursuit of a proof of the existence of transcendental (irrational) real numbers. Frege devised
a logical calculus, called Begrisschrift [1879], and used it to construct a formal foundation for
arithmetic in “Grundgesetze der Arithmetik” [1893]. And, of course, Peano developed his well-
known axioms for arithmetic [1889] and invented something like the modern notation used in
logic.
In 1901, Bertrand Russell discovered a paradox in Frege’s logic [1967]. This paradox (and others)
threatened the program to build logical foundations for mathematics. Russell, with the collaboration
of Alfred North Whitehead, launched an eort to repair the fault that led to the publication of the
three volumes of Principia Mathematica between 1910 and 1913 [1910,1912,1913]. Their approach
to avoiding the paradoxes was to introduce a ramied theory of types into a formalism derived
from Frege’s Begrisschrift. This formalism was based, like Frege’s, on the idea of propositional
23For general introductions to typed λ-calculus, see texts by Hindley [1997] or Henk Barendregt [1992].
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:19
functions rather than sets. The types characterized the arguments of such propositional functions:
their number and “levels” (what we would today call their “order”). The ramication took the
form of orders (natural numbers) that specied the universes quantied over in the denition of
a propositional function: order 0 if there was no (universal) quantication, order 1 if there were
quantiers over “individuals” (basic values like numbers), 2 for quantiers over order 1 propositional
functions, and so on. These notions of types and orders were treated somewhat informally, and in
fact there was no denition of or notation for types, nor for what it meant for a term to have a
type. There was only an incomplete denition of when two terms “have the same type.24
This ramied theory of types was both complex and not fully dened in Principia Mathematica.
During the 1920s, F.P. Ramsey [1926], and David Hilbert and Wilhelm Ackermann [1928] showed
that the complexity of ramied types was not actually necessary to avoid the paradoxes and that
asimple type theory would suce [Carnap 1929]. Their versions of a simple theory of types was
dened in terms of propositional functions, where the types characterized only the arguments of
functions, since all such functions returned propositions (or, roughly, Boolean values). In 1940,
Alonzo Church published his paper “A formulation of the Simple Theory of Types” [1940]. This
paper still focused on using types (in conjunction with an applied
λ
-calculus enriched with logical
operators like “NOT,” “AND,” and universal quantication) to express a logic, but it supported a
more general notion of functions that could return values other than propositions (or truth values).
Church’s type theory became the standard in the following decades.25
Meanwhile, in the 1930s Haskell Curry was working on Combinatory Logic, another “logical”
calculus [Schönnkel 1924] [Curry 1930,1932]. Curry introduced what he called functionality in
Combinatory Logic, which consisted essentially of types for combinators and combinations [1934].
Although his notation was archaic and types were not notationally distinguished from general
combinators, his notion of functionality for combinators amounts to what we now call parametric
polymorphic types. For instance, his axiom for the typing of the
K
combinator (corresponding to
the λ-expression λx.λy.x) is
(x,y)Fy(Fxy)K(FK)
where the notation
Fαβ
translates into
αβ
(assuming
α
and
β
are terms representing types),
and
Fαβx
represents the typing assertion
x
:
αβ
. So the above axiom for typing
K
translates
into the modern form
K:∀(x,y).y→ (xy) (FK)
Another important point about Curry’s type theory is evident here: a function (e.g.,
K
) can simulta-
neously have many dierent types. The meaning of a typing assertion such as
Fxyz
(i.e.,
z
:
xy
)
is given by the following axiom:
(x,y,z)(Fxyz⇒ (u)(xu y(zu))) (Axiom F)
or in modern notation:
∀(x,y,z)(z:xy⇒ (∀u)(u:xzu :y)) (Axiom F)
in other words,
z
:
xy
means that
z
maps members of type
x
to members of type
y
. Thus having
a type is a property of a term that does not exclude that term having other types; i.e.; the type is
not intrinsic to the term. This approach is called Curry-style typing in contrast with the types in
24
Modern reformulations of the paradoxes and the theory of ramied types are given by Thierry Coquand [2018] and
Fairouz Kamareddine [2004].
25
But Church’s notation for types did not survive. For instance, in his notation, the type of functions with domain
α
and
range βwas (β α ), which we now would express as αβ.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:20 David Maceen, Robert Harper, and John Reppy
Church’s simply typed
λ
-calculus, where the type of a term is uniquely determined by the syntax
of the term and the type is an intrinsic property of the term.26
Church’s simply typed
λ
-calculus introduced the problem of determining whether a term in the
calculus was well-formed, which is essentially the problem of type checking. Curry’s formulation
entailed the more challenging problem of discovering a type assignment for a given untyped
term. The rst solution of this type assignment problem (i.e., an algorithm for discovering a type
assignment for a term) for the
λ
-calculus was provided by Max Newman
27
in 1943 [Hindley 2008;
Newman 1943].28
Curry returned to the problem of discovering (or inferring) type assignments more than 30 years
later [1969]. His approach was to use equations to express the constraints that unknown types had
to satisfy. For instance, given an application
F A
, the types
tF
and
tA
of
F
and
A
have to satisfy the
equation
tF=tAt
for some type
t
. Other equations are generated for occurrences of primitive combinators whose
types are presented as type schemes for the basic combinators like K and S, which, in modern
notation, are:
K:uvu S :(u→ (vw)) → ((uv)→(uw))
where the type variables in the schemes are implicitly universally quantied. So the assignment
problem for a compound term generates a set of rst-order order equations involving a set of type
variables representing the types to be determined. Curry presented an algorithm to solve such
equations that is equivalent to Robinson’s Unication algorithm [Robinson 1965].
29
Curry claimed
that his algorithm produced a principal functional character (or what we now would call a principal
type scheme), meaning that any correct type assignment satisfying the constraints can be obtained
by applying a substitution to the scheme produced by the algorithm. Curry did not prove that the
algorithm produces the most general (i.e., principal) typing, however [1969]. Nevertheless, since
the algorithm is equivalent to Robinson’s, it does indeed have this principality property.
At about the same time, Hindley [1969] discovered the same type assignment algorithm, but in
his case he explicitly invoked Robinson’s unication algorithm to solve the typing equations, so
his proof of principality followed from Robinson’s result.
Note that in these three solutions of the type assignment problem (Newman 1943, Curry 1966/1969,
and Hindley 1969), the assignment problem is posed for a complete, closed, term, with no context
required. There is a good exposition of this Curry-style type assignment problem, in its purest
form, in Hindley’s Basic Simple Type Theory [1997], but it is worth explaining the intuitive idea
behind the algorithm and some of the terminology and notation here, using the modern notation
of natural semantics.30
26
Curry returned to the issue of functionality of types in Combinatory logic in “Combinatory Logic, Volume 1” [1958,
Chapter 9], published 24 years later.
27
Newman was Turing’s mentor at Cambridge and the head of the “Newmanry” group at Bletchley Park that created the
Colossus code-breaking computer.
28MacQueen [2015] gives an explanation of how Newman’s algorithm worked – it was not based on type equations.
29
Robinson’s algorithm solves a set of equations between type terms involving type metavariables. The solution takes the
form of a substitution (a mapping from type metavariables to types), which can be applied across the entire derivation.
Robinson is not cited by Curry, so he appears to have independently discovered the algorithm.
30
The term “natural semantics” was coined by Gilles Kahn [Kahn 1987] to describe an inference-rule based style of semantics
that mimicked the sequent style of natural deduction [Ryan and Sadler 1992, Section 4.3].
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:21
We start with typing judgements. In their simplest form these are assertions that a given term
M
has a given type
T
, written “
MT
,
31
where
M
is an term in some syntax for value
expressions and
T
is an expression in some type syntax. Let us assume that the term language is a
λ
-calculus with some basic constants and primitive operators, and type terms are formed from type
variables (e.g.,
t
) and primitive type constants (e.g.,
int
and
bool
) combined with a right-associative,
binary operator representing function types (
). We start with some assumed judgements such as
0int true bool
+int int int not bool bool
We can infer additional typing judgements using inference rules, one of the most basic being the
rule for application:
M1T1T2M2T1
M1M2T2
(App)
which simply says that if a function expression of type
T1T2
is applied to an argument expression
of type
T1
, the result of the application has type
T2
. This simple form of typing judgements suces
if we are only dealing with closed terms (with no free variables), but the inference rule for typing
λ
abstractions requires a richer notion of judgement that works for terms with free variables. Thus a
context must be provided to give the types of any free variables, yielding the following form of
judgement:
ΓMT
where
Γ
is a typing environment — a nite map from term variables to type expressions. We can
also think of
Γ
as a nite sequence of typing assertions like
x
:
int,y
:
bool
. It is assumed that the
domain of
Γ
contains all the free variables in the term
M
. Then the inference rule for
λ
abstraction
is as follows:
Γ,x:T1MT2
Γλx.MT1T2
(Abs)
This rules says that if, when we assume that
x
has type
T1
,
M
has the type
T2
, then
λx.M
can be
assigned the type
T1T2
, and we can then drop the variable
x
from the context because it is not
a free variable of the abstraction
λx.M
. In these rules, the symbols
T1
and
T2
are metavariables
ranging over arbitrary type expressions, so in fact the inference rules are rule schemas, and there
are innitely many instance rules obtained by substituting particular type expressions for the type
metavariables. But we can also substitute generalized type terms containing type metavariables for
the metavariables!
These rule instances plug together like Lego bricks to form type derivations (i.e., “proofs” of
typing judgements), but getting the interfaces between the constituent rule instances to match up
requires that certain constraints be satised. For instance, let us construct a derivation of a typing
for the term (λx.x)true.
x:TxT
λx.xTTtrue bool
⊢ (λx.x)true T
31
There are many dierent syntaxes for these judgements; we follow the notation used in the Denition of Standard
ML [Milner et al. 1990].
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:22 David Maceen, Robert Harper, and John Reppy
In order for these rule instances to t together, it is necessary that
T=bool
, which can be solved
by the substitution
{T7→ bool}
. Applying this substitution to the entire derivation (including
contexts) gives
x:bool xbool
λx.xbool bool true bool
⊢ (λx.x)true bool
Also note that, as illustrated by this example, the rules are syntax-driven, in the sense that there
is one rule governing each form of term construction (abstraction, application, etc.). So for any
term we can construct the template of its typing derivation, which will be analogous in structure
to the term itself, and to make the rules “match,” we have to solve a set of equations between type
terms involving type metavariables. As mentioned above, Robinson’s unication algorithm [1965]
provides a way to solve such sets of equations producing the necessary substitutions.
Sometimes after solving the equational constraints in a derivation and applying the resulting
substitution, there remain some residual type metavariables that were not eliminated. An example
would be
x:T1xT1
λx.xT1T1
y:T2yT2
λy.yT2T2
⊢ (λx.x) (λy.y) ⇒ T1
with the constraint that
T1=T2T2
, yielding the substitution
{T17→ T2T2}
. Applying the
substitution to the derivation yields
x:T2T2xT2T2
λx.x⇒ (T2T2)→(T2T2)
y:T2yT2
λy.yT2T2
⊢ (λx.x) (λy.y) T2T2
The residual metavariable
T2
that was not eliminated can be replaced by any type at all in the
derivation, so the term
(λx.x)(λy.y)
can have any instantiation of the type scheme
T2T2
as its
type (e.g.,
int int
). We capture this multiplicity of types by saying the term has the polymorphic
type (or polytype)α.αα.
If the equational constraints arising from a schematic derivation are not solvable (e.g., they
include an equation like int =bool or T=TT), then a type error has been detected.
4.2 Milner’s Type Inference Algorithm
When Milner began his project to develop the Edinburgh LCF system, his plan was to replace Lisp,
the metalanguage in the rst generation Stanford LCF system, with a logically secure metalanguage,
where a bug in the metalanguage code could not lead to an invalid proof. He chose Landin’s
ISWIM language [1966b] as the basic framework for the metalanguage, but because ISWIM was
dynamically typed it would not provide the desired security. So Milner added a type system to
ISWIM, among other features, to guarantee that logical formulas would only be blessed as theorems
if they were valid in the LCF logic (i.e., were produced as the conclusions of proofs). The simplest
solution would have been to use a simply typed variant of ISWIM, since ISWIM was a syntactically
sugared version of the untyped
λ
-calculus, but that choice would have sacriced much of the
exibility and expressive power that he was accustomed to with Lisp. At that time, Milner was
not aware of the previous work on type inference by Newman, Curry, and Hindley, so he invented
his own version of type inference with one very signicant twist. Instead of a whole-program
(or whole-expression) approach to the type assignment problem, he added a special treatment
for let bindings (local denitions), such that the deniens subexpression in a let expression could
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:23
be assigned a typescheme (a polymorphic type) and the dened variable(s) could then be used
“generically” in the body of the let-expression (i.e., dierent occurrences of the variable could be
assigned dierent instances of its polymorphic type).
Milner implemented this typing algorithm in the ML type checker in the Edinburgh LCF system
and later published a theoretical development of what he called Algorithm W in the paper “A theory
of type polymorphism in programming” [1978]. The term polymorphism, used in this sense, had
been in common use since Christopher Strachey introduced the term parametric polymorphism in
his 1967 lectures on “Fundamental Concepts of Programming Languages” [1967].
Let us review the type checking regime that Milner set out in his 1978 paper. He worked with a
simple variant of ISWIM with
let
and
letrec
expressions for local denitions of ordinary variables
and recursive functions, respectively. The language also provides some primitive types like
int
and
bool
and some basic data structures like pairs and lists with their associated polymorphic
operations (e.g., fst :∀(α,β).α×βα).
Let-bindings are treated specially by potentially assigning the dened variable a polymorphic
type. In an expression of the form “
let x=e1in e2
,” the deniens
e1
is typed as above in a context
derived from any enclosing denitions or
λ
-bindings that determine the types of the free variables
that appear in
e1
. The type derived for
e1
is assigned to the bound variable
x
, and any remaining
type metavariables in that type are converted to generic (i.e., quantied) type variables – but only if
they do not appear in the types of any
λ
-bound variables in the context. For example, consider the
following two function denitions:
let g=λx.let f=λy. ( y,x)in (f2 , f true )
let h=λx.let f=λy.cons (y,x)in (f2 , f true )
In the rst denition, the
λ
-bound parameter
x
will be assigned a type metavariable
Tx
, representing
its initially unknown type, and the local let binding of
f
will be assigned a polymorphic type
αα×Tx
, where
α
is generic (i.e.,
f
:
α.αα×Tx
). Thus the body will type check because
the type of f can be instantiated to both
f
:
int int ×Tx
in
f2
and
f
:
bool bool ×Tx
in
f true
. The type of deniens for
g
will be
Tx→ (int ×Tx)×(bool ×Tx)
, and since the context
is empty,
Tx
can be “generalized” (turned into a generic, or quantied, type variable), giving
g:α.α→ (int ×α)×(bool ×α).
In the second denition, the
λ
-bound parameter
x
will again initially be assigned a type
Tx
, and
the inner parameter
y
will be assigned type
Ty
. Then, to type check the body
cons(y,x)
, we are
forced to equate
Tx
with
Tylist
, which means that
Tylist
replaces
Tx
everywhere, including in
the type of
x
in the context. So now the type of
x
is
Tylist
, which means that
Ty
occurs the type
of the
λ
-bound variable
x
in the context of the let binding of
f
and thus it is not generic (i.e., not
replaced by a quantied type variable) in the type of
f
:
TyTylist
. Since
Ty
is not generic,
it cannot be instantiated dierently in the two expressions
f2
and
f true
. Assuming
f2
is typed
rst,
Ty
will be replaced (everywhere) by
int
, making the type of
f
:
int int list
and then
the typing of f true will fail.32
Note that the types of
λ
-bound variables are never generic. In other words, the argument of a
function cannot have a polymorphic type. As a consequence of this, polymorphic types assigned to
variables are always in prenex form, with all type variable quantiers at the outermost level; thus a
type such as (∀α.αlist int) → int with inner quantiers will never occur.
32
Type metavariables like
Tx
are not valid forms of type expressions; they are temporary placeholders (representing
unknown types) used in the type inference algorithm and they must be eliminated either by substitution or by being
“generalized” and turned into universally quantied type variables. Thus they are sometimes called “unication variables.
Milner’s description did not notationally dierentiate type metavariables and quantied type variables — they were both
represented by lower-case Greek letters.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:24 David Maceen, Robert Harper, and John Reppy
In Section 3 of the 1978 paper, Milner provided a denotational semantics for both value terms and
type terms, and a declarative denition of the well-typing of an expression (with a context). He then
proved a Semantic Soundness theorem that intuitively says that the value of a well typed expression
belongs to the denotation of its type. The semantic evaluation function was dened to produce a
special value wrong if a semantic type error is detected (e.g., when applying a non-function to an
argument), where wrong is not a member of the denotation of any type. Thus if an evaluation of a
term produces a member of the denotation of its type, the evaluation cannot have gone “wrong”
(i.e., produced the value wrong).33
The next section of the paper presented two versions of a type checking/inference algorithm.
The rst, called Algorithm W, is purely applicative. It is rather inecient and cumbersome because
it involves a lot of machinery to pass around and compose the type substitutions that result from
local unications of type equations as they appear during a traversal of the term being typed. A
second, imperative, version is called Algorithm J, where a global variable is used to accumulate the
substitutions.
Finally, the paper considered several extensions of the subject language. The rst was adding
some additional primitive data structures, such as Cartesian product, disjoint sum, and lists, with
their associated polymorphic primitive operators. This extension is straightforward.
The second extension was to add assignable variables by introducing a
letref
expression (as in
LCF/ML). Done naïvely, this feature leads to unsoundness, for instance if
y
is assigned the generic
type αlist in:
letref y=nil in (y: = cons ( 2 , y) , y: = cons (tr u e ,y) )
This top-level unsoundness was avoided by requiring the bound variable to have a monotype. We
discuss how the issue is dealt with in Standard ML below in Section 4.4.
The third extension was adding user-dened types (and n-ary type constructors) including
recursive types, dened using the
abstype
and
absrectype
declaration forms in LCF/ML. The
extension of the well-typing algorithms was asserted to be fairly straightforward (as known from
their implementation in LCF/ML).
The last two extensions considered were type coercions and overloaded operators. Milner
argued that adding coercions to correct some type errors (e.g., using an integer where a real is
expected) should be easy to support. The discussion of overloading is even more speculative, with
the conclusion that “there appears to be a good possibility of superposing this feature upon our
discipline.
Although Milner’s paper included a soundness proof for his polymorphic type system for a purely
applicative language, there were some theoretical loose ends left unresolved. Luis Damas addressed
these issues in his PhD thesis [1984] and a paper on “Principal type-schemes for functional programs”
with Milner that appeared in POPL in 1992 [1982]. He proved that the type inference algorithm
in [Milner 1978] was sound and complete, meaning that if an expression had a typing according to
the type inference rules, then algorithm W would produce a type for the expression and that that
type would be principal, i.e., as general as possible. He also considered two extensions of the basic
ML type system: (1) overloaded operators and (2) polymorphic references (see Section 4.4 below).
We next note several techniques that most real implementations of type inference use to improve
eciency or usability of the ML type system.
33
This approach to semantic soundness has the disadvantage that there is no way to know that all of the necessary checks
for “type errors” have been included in the rules dening evaluation. A more modern approach to soundness, now usually
referred to as “safety,” is based on showing that a transition-style dynamic semantics will not get “stuck” for well-typed
terms [Wright and Felleisen 1991] (see Pierce [2002, Section 8.3] for a tutorial explanation).
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:25
Manipulating and composing substitutions is quite expensive. One technique for avoiding them is
to represent type metavariables as reference cells (a dynamically allocated cell that can be assigned
to). Then a substitution is realized by updating that cell with an instantiation; i.e., a link to the type
substituted for the type variable. This can lead to chains of instantiations, which can be compressed
in an amortized way when traversing types representations. In the implementation of LCF/ML,
type metavariables were represented by Lisp symbolic atoms, and substitution was performed by
setting a property (called @INSTANCE) of such an atom.
Determining whether a type variable is generic (polymorphic) in the naïve algorithm involves
an expensive search through all the types in the context type environment. This can be avoided by
annotating type variables with the
λ
-nesting depth at which the type variable was created. Instanti-
ating a variable during unication propagates this
λ
-level to type variables in the instantiating type
and determining whether a type metavariable is generalizable then requires only comparing its
internal
λ
-nesting depth with the current
λ
-nesting depth [Kuan and MacQueen 2007]. An alternate
form of binding-level bookkeeping due to Didier Rémy, and used in the Caml family of compilers,
uses let-binding nesting levels instead of λ-binding nesting levels [Rémy 1992].
Milner’s Algorithm W uses a top-down, left-to-right traversal of the abstract syntax tree of the
term being typed. In fact, there is quite a bit of exibility in the choice of traversal order, and some
traversal orders provide better locality information when a type error is detected. For instance,
Oukseh Lee and Kwangkeun Yi [1998] propose an alternate algorithm M that uses a bottom-up
order. In practice, the type checkers used in compilers often use a mixture of top-down and bottom
up traversal, mainly to improve locality information about type errors (see Section 4.7 below).
There has also been considerable theoretical analysis of the type inference problem as process of
collecting constraints and then solving them. See Pottier and Rémy’s “The Essence of ML Type
Inference” [2005] for a summary of this work.
There is, nally, a fundamental principle underlying parametric polymorphism that virtually
all implementations depend on: the principle of uniform representation. The algorithm invoked
by a polymorphic function is expected to work the same no matter how the polymorphic type
of the function is instantiated. For instance, the list reversal function “
rev :'a list >'a list
performs the same algorithm whether it is reversing a list of integers, booleans, string lists, or
functions. The algorithm works only on the list structure and is insensitive to the nature (or
type) of the elements of the list. In order to support this principle, implementations use a uniform
representation for all values, typically a 32-bit or 64-bit “word.” Small values like booleans or small
integers are represented directly in the content of the word, while larger and more complex values
like a function are represented by pointers to a heap-allocated structure. Often a tag bit is used to
dierentiate between a direct value (“unboxed”) and a pointer value (“boxed”).
4.3 Type Constructors
Types as expressions can be atomic, like the primitive type
int
or a type variable
α
, or they can
be compound, like
int bool
. Compound type expressions are constructed by applying type
constructors to argument type expressions. Type constructors are characterized by having an arity
or kind; thus “
” is a type constructor of arity 2 or kind
Type
#
Type Type
, i.e., it is a binary
operator on types. We can treat primitive types like int as type constructors of arity 0.34
4.3.1 Primitive Type Constructors. The Curry-style type assignment system studied by Curry and
Hindley had just one primitive binary operator on types, “
,” for constructing function types. The
34
In some formal developments, some forms of compound type expressions are also called “constructors” and are distin-
guished from “types.” We are taking a more naïve view here.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:26 David Maceen, Robert Harper, and John Reppy
ML type system is much richer. It has this function type operator and many others, and new type
operators can be dened by programmers.
For product types, LCF/ML had a single binary inx operator “
#
,” with product values (pairs)
constructed by the comma operator. Tuples of length greater than two were approximated by
nested (right associative) pairing. Cardelli ML kept the binary product operator but added a labeled
record construct with selectors. During the Standard ML design, there was a fairly early consensus
on including records, but there were multiple proposals for record syntax and semantics and the
design took a long time to settle. In the end, the nal form of records used a class of eld label
identiers, with types such as
{a:int ,b:bo o l ,c:string }
and corresponding record expressions such as
val r= { c="x y z " ,a=z2 , b=n ot p }
The order of elds in the type was not important (canonically the elds would be ordered lexico-
graphically), but the order that the elds were written in a record expression determined the order
in which they would be evaluated. Selector operations would be automatically generated from eld
labels; e.g.,
#a r
. Record labels can be reused in dierent record types, so the meaning and type
of a selector operator like
#a
will depend on the type of its argument, and it is an error if its type
cannot be statically resolved.
Record patterns were also supported, with the ability to specify partial record patterns using
...
” to represent the unspecied part of the record (e.g.,
{a = x, ...}
). The exact type of the
record, however, must be inferable from the context.
The binary pairing operator of LCF/ML was dropped in SML, but ordered tuples could be
represented by records with natural number labels (successive from 1to
n
, for some
n
, with no
gaps). The normal, abbreviated syntax for a tuple expression (in this case a 3-tuple) is
(e1,e2,e3)
.
The tuple expression is evaluated by evaluating the constituent element expressions from left to
right and returning the tuple of the resulting values. The type of a tuple expression is an n-ary
product type, written
t1t2t3
, where
ti
is the type of the corresponding element expression
ei
.
Tuples are heterogeneous in the sense that dierent element types can be mixed and they are “at”
in the sense that a 3-tuple is not formed by nested pairing. The selector function for the
n
th element
of a tuple twhere 1nlength(t), is #n. For example, the expression #2(1,3, true)returns 3.35
4.3.2 Type Abbreviations. The simplest way for a programmer to dene a new type or type operator
is to declare a so-called type abbreviation.36 Here are two examples:
type point =real real
type 'a pair ='a ' a
In the scope of the rst declaration,
point
is simply an abbreviation for the type expression
real real
. And
pair
is a simple type function such that
int pair
expands to (reduces to)
int int
.
So in this example, the types
point
and
real pair
are the same. Type abbreviations cannot be
recursive or mutually recursive, so it is always possible to eliminate type abbreviations by expanding
their denition, which is, in principle, what happens during type checking. Type abbreviations do
not add complexity to the type checking algorithm since they could, in principle, be eliminated
before type checking.
35By the way, the generic length function over tuples is not expressible in SML. There is no type containing all tuples.
36Type abbreviations were supported in LCF/ML by the lettype declaration.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:27
4.3.3 Datatypes. Cardelli ML had a notion of “variants” or labeled unions, but since it was decided
more or less from the beginning to adopt some form of the datatypes from the HOPE language,
Cardelli’s labeled unions would have been redundant. Instead, the
datatype
declaration was adopted.
Datatypes are a form of type (or type operator) denition that combine the following features in a
single mechanism:
Discriminated union types, with data constructors serving as tags to discriminate between
variants.
Recursive types could only be dened as datatypes. Here the constructors serve as wind-
ing/unwinding coercions in an iso-recursive form of recursive type.37
Datatypes can have parameters, and so can dene type operators (such as
list
) as well as
simple types.
The data constructors of a datatype can both construct values of the type, injecting variant values
into the union, and they can also discriminate on and deconstruct datatype values when used in
pattern matching.
Landin’s ISWIM had an informal style of data type specication that was not part of the language
itself, and was therefore not statically checkable [1966b].
38
Here is an example of one of these
informal type denitions, for an abstract syntax for applicative expressions (λexpressions):
An e x p r es s i o n i s e i ther simple
and is e i t h e r a n identier
or alambda expression
and ha s a bv which i s a b o u n d va r i a b l e
and abody which is a n e x p r e s sion ,
or compound
and ha s a rator
and arand ,bo t h o f w h i c h a r e e x p r e s si o n s
This stylized prose description is made of unions and products, with the union variants identied
by words like simple and compound and products (or records) having elds named bv and body
(for a lambda expression) or rator and rand for the elds of a compound expression, while identier
stands for some previously dened type. The convention is that the names simple and compound
designate recognizer functions that can distinguish between the two variants of the union type
expression
, and similarly for the two variants of simple expressions. Meanwhile, bv,body,rator,
and rand serve as selectors from their respective record types. There would also be a convention
for variant names like simple and compound to be used (perhaps with a standard prex like “cons”
or “mk”) as constructors for the corresponding versions of expression, so the union types were
implicitly discriminated unions.
In Standard ML, we can express this abstract syntax using a pair of mutually recursive datatypes.
datatype simple =
=MkIdentifier of identifier
|MkLambdaExpression of {b v :variable ,body :expression }
37
The term iso-recursive indicates a form of type recursion that is mediated by coercive operations relating the recursive type
to its unfolded form. An alternative is equi-recursive types, where the recursive type is directly equivalent to its unfolding.
See Pierce [2002, Section 20.2] for further discussion.
38
Landin’s style of denition was probably inspired by McCarthy’s earlier system for dening the abstract syntax of
languages [McCarthy 1963,1966].
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:28 David Maceen, Robert Harper, and John Reppy
and expression =
=MkSimple of simple
|MkCompound of {rator :expression ,rand :expression }
and the denition in HOPE would be a minor notational variant of this.
A bridge between Landin’s informal method of dening structured data and the datatypes of
HOPE and Standard ML was provided by a simple language, named NPL, that Burstall described
in a 1977 paper [1977]. NPL had a form of types with data constructors where the type and its
constructors could be declared independently, as with Standard ML’s primitive
exn
type. This
language had clausal, rst-order, function denitions and patterns built with data constructors.
One important change that HOPE introduced relative to NPL was to require “closed” datatype
declarations, where a datatype and all its constructors were declared together. The point of this
change was to allow optimized runtime representations of constructors and optimized pattern
matching, where a set of patterns (for a clausal function or case expressions) could be processed
to produce very ecient matching code with no backtracking [Baudinet and MacQueen 1985;
Maranget 2008;Pettersson 1992;Sestoft 1996]. The second of these optimizations was part of the
rst HOPE compiler and was rened in the SML/NJ compiler.
Datatype declarations are generative, in the sense that whenever a datatype is statically processed
(we say “elaborated”) a fresh type constructor is created that is distinct from all existing type
constructors. The rationales for this generative form of type declaration were:
It is consistent with the way that abstract types (declared with
abstype
and
absrectype
)
worked in LCF/ML and datatypes in HOPE. Datatypes in part replaced the
abstype
mech-
anism of LCF/ML, where they were the only way to dene recursive structured types like
trees.
It made comparison of types in the type checker simple, since
datatype
(and
abstype
)
constructors behaved as atomic, uninterpreted operators in type expressions.
It avoided a serious technical problem that would have arisen if type equivalence treated
recursive types transparently; i.e., adopted equi-recursive types instead of iso-recursive types
(which datatypes embody). Marvin Solomon [1978] had shown that the type equivalence
problem with
n
-ary (
n>
0) recursive type operators was the same as the equivalence of
deterministic push-down automata (which was not known to be decidable at that time). Much
later this problem was determined to be decidable, but the decision algorithm was not simple
or ecient [Pottier 2011].
Datatypes are allowed to occur within a dynamic context, such as an expression or even a
function body, but such a datatype still has a unique static identity.
39
But if a datatype occurs within
a functor body, it creates a new static type constructor for each application of the functor. Thus
both datatype declarations and functor applications (if the functor body dened datatypes) have a
static eect of generating new types, or rather, new unique type constructors.
SML has a couple of peculiar pseudo-datatypes:
ref
and
exn
. The primitive
ref
type is treated as
if it was dened by
datatype 'a ref =ref of 'a
in the sense that the
ref
data constructor may be used like a normal constructor to both construct
values in expressions and destruct values in patterns, but
ref
is not really a datatype, since its
values are mutable, with special equality semantics.
39
This design decision was rather odd, and it is not considered good programming practice to dene types within expressions.
On the other hand, some have advocated for module declarations within expressions, which would entail the same thing.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:29
The
exn
constructor, on the other hand, is a very convenient way of simplifying the handling of
multiple potential exceptions by a form of pattern-matching similar to a case expression. The
exn
type is eectively a special, open-ended datatype whose constructors (exception constructors) can
be declared separately, with no bound on the number of constructors. Thus the
exn
type can be
used (or abused?) in cases where we need an “open-ended” union, although this feature was not
the original intent, and there is no way to create new open datatypes like exn.
4.4 The Problem of Eects
The type inference schemes of Newman, Curry, Hindley, and nally Milner with Algorithm W
had the beautiful property of producing the principal or most general typing for a term. For ML,
however, there was a problem: the problem of side-eects, which, if not handled with great care,
could make the type system unsound.
This problem was manifested in LCF/ML in terms of the
letref
declaration, which introduced
an initialized assignable variable. This construct could potentially allow unsound examples such as:
letref x= [ ] i n
(x: = [ 1 ] ;
not (hd x ) ) ; ;
If
x
is assigned the polymorphic type
α.αlist
then the two occurrences of
x
in the body of the
let-expression could be assigned two independent instances of that polymorphic type, namely
int list
and
bool list
, and the expression would type check with type
bool
, thus transforming
the value
1
into a boolean! To avoid this problem, LCF/ML introduced the following rather obscure
and ad hoc constraints on the typing of variable occurrences (where “
\
” represents
λ
) [Gordon et al
.
1979, Page 49]:
(i)
If x is bound by
\
or
letref
, then x is ascribed the same type as its binding occurrence. In
the case of
letref
, this must be monotype if (a) the letref is top-level or (b) an assignment to
x occurs within a \-expression within its scope.
(ii)
If x is bound by
let
or
letrec
, then x has type ty, where ty is an instance of the type of the
binding occurrence of x (i.e. the generic type of x), in which type variables occurring in the
types of current \-bound or letref bound identiers are not instantiated.
In the example above, the
letref
-bound
x
could not be assigned a polymorphic type because the
expression was (assumed to be) at top-level. Thus the assignment statement
x:= [1]
would force
x to have the monotype
int list
and the expression
not(hd x)
would cause a type error because
(hd x) : int.
The next step toward a sound treatment of assignment and polymorphism occurred in the early
1980s. In 1979, Gordon wrote a brief note “Locations as rst class objects in ML” [1980] proposing
the
ref
type for ML, with a restricted type discipline he called weak polymorphism based on weak
type variables. Gordon suggested this as a research topic to Luis Damas, who eventually addressed
the problem in his PhD thesis [Damas 1984, Chapter 3] using a rather complex method where typing
judgements were decorated by sets of types involved in refs. Cardelli got the idea to use the ref type
either from Gordon’s note or via discussions with Damas, with whom he shared a grad-student
oce for a time. At the end of Chapter 3 of his thesis, Damas returned to a simpler approach to the
problem of refs and polymorphism similar to the weak polymorphism suggested by Gordon, and
he mentioned that both he and Cardelli implemented this approach.
40
Weak polymorphism is not
40
At one point, Cardelli had Damas write down a rough, half-page sketch of his algorithm that was circulated for several
years and served as the starting point of multiple implementations of the idea. For instance, MacQueen adopted the design
in the original type checker for the Standard ML of New Jersey compiler [MacQueen 1983a].
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:30 David Maceen, Robert Harper, and John Reppy
mentioned in the various versions of Cardelli’s ML manuals [1982d;1983b;1984b], however, where
the ref operator is described as having a normal polymorphic type.41
Now let us illustrate again the need for a careful treatment of polymorphism and assigment when
using
ref
types. Assume, as in Cardelli ML and Standard ML, assignment side eects are introduced
through the ref primitive type and its associated operators. Assume also, that we, naïvely, assign
the following ordinary polymorphic types to those operators:
val ref :'a>'a re f
val : = : 'a ref ' a>unit
val ! : 'a r e f >'a
Now with these types, the above LCF/ML example can be coded as follows:
let val x=ref [ ]
in
x: = [ 1 ] ;
not (hd ( ! x) )
end
The problem is that at the binding introducing variable
x
, if
x
is assigned the polymorphic type
'a list ref
”, then the two occurrences of
x
in the body of the let expression can have their types
be independently instantiated to “
int list ref
” and “
bool list ref
”, which again means that the
result would have type bool and the integer value 1will have been converted to a boolean.
The question is how to modify polymorphic type inference for pure expressions to prevent this
unsound outcome? The original solution in Standard ML was to introduce a special variety of type
variable, called an imperative type variables, which are restricted on when they can be generalized.
In this example, the primitive function
ref
is dened to have a restricted form of polymorphic type:
val ref :'_a >'_ a r e f
where “
'_a
” is an imperative type variable. Such a type variable can be instantiated to any type
τ
as before, but the property of being imperative is inherited by any type variables in
τ
. In our
example, the type inferred for “
ref []
” will be “
_Tlist ref
,” where the type metavariable
_T
has
inherited the “weak” attribute from the type of
ref
. Now a special restriction controls whether the
type of
x
can be generalized to make it polymorphic. Generalization is only allowed if the deniens
ref []
of the declaration of
x
is non-expansive, meaning that it is of a restricted class of expressions
that do not invoke any “serious” computation. But in this case, the application
ref []
is expansive,
because it involves the allocation and initialization of a new ref cell. Hence the type of
x
will be
ungeneralized
_Tlist
. The type metavariable
_T
will be instantiated to
int
when type checking
the assignment “
x:= [1]
,” causing the type of
x
to become “
int list ref
.” Then the expression
not (hd (!x)) ” will fail to type check.
The theory for imperative type variables is developed in Tofte’s paper “Type Inference for
Polymorphic References” [1990]. Harper developed a much simpler proof of soundness in [Harper
1994].
This solution using imperative type variables works, but it has some unpleasant side eects. For
instance, an alternate denition of the identity function of the form
val id =f n x=> ! ( r ef x )
which temporarily creates a ref cell will be assigned an imperative polymorphic type
id :'_ a >'_ a
41
The notes at the end of [Cardelli 1982c], mention that the 5-11-82 Unix version has a “New typechecker for ref types.” It is
not known whether this type checker used weak polymorphism, but it is likely.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:31
even though its use of a reference cell is benign because it is encapsulated and the ephemeral
reference cell cannot “escape” to cause problems.
Another issue is that an imperative polymorphic type cannot be allowed to match a pure (that is,
ordinary) polymorphic type in a signature specication, so purely internal and contained use of
references can contaminate an implementation so that it cannot be used to implement a signature
with pure polymorphic value specications. For example, the function
id
dened above could
not be used to satisfy a signature specication of the form “
type id :'a>'a
,” which is a kind of
violation of abstraction.
In the SML/NJ compiler [Appel and MacQueen 1991;Appel and MacQueen 1987], a slightly
liberalized variant of this scheme was implemented whereby imperative type variables had an
associated numeric weakness and only type variables of weakness 0 were prevented from being
generalized. This approach moderately increased the exibility of the type system, but did not
really eliminate the basic problems with imperative polymorphism.
Many people worked on the problem of polymorphic references over a period of several
years [Harper 1994;Leroy 1993;MacQueen 1983a;Mitchell and Viswanathan 1996;Tofte 1987,1988,
1990;Wright 1995,1992,1993]. Ultimately, Andrew Wright cut the Gordian knot by proposing
to get rid of imperative type variables and replace them with a stronger, but simpler, restriction
on the generalization of types in
let
expressions [1995]. The key insight is that polymorphic
generalization in a pure, call-by-name language is justied by the interpretation of
let
-expressions
as
β
-redexes that can be reduced to an equivalent expression by substituting the deniens for all
occurrences of the dened variable in the scope of the binding.
let x=e1in e2→ [e2/x]e1
Multiple occurrences of
x
in
e2
are replaced by multiple copies of the expression
e1
. These multiple
copies can then be typed in their individual contexts, producing specialized versions of the “generic”
or polymorphic type of
e1
that correspond to the dierent instantiations of that polymorphic type
that are produced during the type inference algorithm.
But in an call-by-value language, the dynamic semantics of
let
expressions evaluates the
deniens once, before substitution. The call-by-name and call-by-value interpretations are not
equivalent in the presence of eects, except in the special case that the deniens is a syntactic value
(e.g., a variable or
λ
-expression), which is guaranteed not to produce eects when it is (trivially)
evaluated. The failure of type safety resulting from treating references as polymorphic is just one
of several manifestation of this discrepancy, as evidenced by the discovery of the incompatibility of
unrestricted generalization with rst-class continuations [Lillibridge and Harper 1991].
The restriction of polymorphic generalization to syntactic value expressions came to be known
as the value restriction
42
and was adopted in the Denition (Revised) because it was found not to be
a signicant limitation in practice.
43
For example, in the case of a function dened by the partial
application of a function
val f=g x
one can restore polymorphism by η-expansion:
fun f y =gxy or val f=fn y=> gxy
42
In the Denition the restriction is called “non-expansiveness” because of the historical, but inaccurate, association of the
problem with the allocation of references.
43
Wright examined over 200,000 lines of existing SML code and found only 31
η
-expansions were required, along with a
small number of other minor edits [1995].
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:32 David Maceen, Robert Harper, and John Reppy
which makes
f
a value and, thus, its type will be generalized. The value restriction had some disad-
vantages, however. For example,
η
-expansion may delay computations that could be performed
once, rather than once on each call, aecting eciency. Another problem is that
η
-expansion
does not apply to abstract types, such as a type of parsing combinators, in which the underlying
representation as functions is not exposed to the programmer. For in that case even simple compu-
tations such as the composition of parsers violates the value restriction, precluding polymorphic
generalization, and there is no natural work-around. In practice, however, the vast majority of
existing SML code was not aected by the switch to the value restriction.
4.5 Equality Types
Equality is normally considered an intuitively “polymorphic” relation. But it technically does not
t the pattern of parametric polymorphic functions, which obey the Uniformity Principle where
the underlying algorithm is insensitive to the instantiation of the polymorphic type. In the case of
equality, the algorithm for computing equality varies depending on the type of the elements being
compared and for some types (e.g., function types) it is not possible to compute equality at all. But
it is also the case that equality does not seem to t the pattern of ad hoc overloaded operators like
addition, where the algorithms for dierent types of arguments may be entirely unrelated.
There is a natural subset of all types consisting of types that admit equality. For these types, the
computation of equality of values is driven systematically by the structure of the type. Although
not parametric polymorphic, equality seems to t a notion of uniform type-driven polymorphism.
LCF/ML had a rather ill-dened notion of equality [Gordon et al. 1979, Appendix 3]:
[
=
] is bound to the expected predicate for an equality test at non-function types, but is
necessarily rather weak, and may give surprising results, at function types.
In Standard ML, however, it was decided that a more exible pseudo-polymorphic treatment of
equality should be provided. This was achieved by introducing another special form of polymor-
phism with another special form of bound type variable, namely equality type variables. These type
variables arise from uses of the generic equality operation and can be generalized. But when these
variables are instantiated, they can only be instantiated to types in a restricted class, namely the
types that admit equality based on their known structure. One way of thinking about this class
of types is that they are the hereditarily concrete types. In particular, function types and abstract
types are excluded from this class, as are all types that are built up from function types or abstract
types. Recursive datatypes (such as
list
) are included, because they support an inductively dened
equality (given that the arguments of the type constructor, if any, support equality).
There are several ways to implement the polymorphic equality operation:
(1)
One can use object descriptors that allow a runtime-system function to determine how to
compare values for equality. Since this information largely overlaps the requirements for
tracing garbage collection algorithms, it may involve little additional cost.
(2)
One can implicitly pass an equality operation for the specic instantiation of an equality type
variable at runtime to the place where it is needed. This approach is similar to the dictionary
passing method used to implement Haskell type classes [Wadler and Blott 1989].
(3)
One can monomorphize the program, which turns polymorphic equality into monomorphic
equality.
For Standard ML, the rst implementation strategy is most common, but there have also been
examples of the second [Elsman 1998] and third approaches [Cejtin et al
.
2000]. Also in cases where
an occurrence of equality is assigned a ground type, compilers can produce specialized inline code
to implement equality for that particular type, avoiding the need for runtime tracing of structure
(dictionary-passing can also be optimized in such cases).
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
The History of Standard ML 86:33
There are a couple of signicant problems with polymorphic equality (beyond the additional
complexity in the type system). The rst of these is that there is a hidden performance (or complexity)
trap in polymorphic equality if it is used naïvely. Generic equality of data structures, such as lists
and trees, takes time that is proportional to the size of the data structures. In general, when testing
equality on complex structures, exhaustive traversals of the structures being compared is not an
ecient approach. Often there is some designated component of the complex structure (say, a
unique identier, or reference cell), that can serve as a proxy for the structure with regard to
equality and which can be compared eciently. Another problem is that structural equality is
often the wrong notion when comparing values of a data abstraction. For example, a red-black-tree
implementation of nite sets admits multiple concrete representations of the same abstract set
value. Experienced programmers learn to avoid the use of polymorphic equality because of these
problems and instead dene an ecient ad hoc equality operation for their nontrivial types. Such a
specialized equality operation is typically made available as part of the signature of a module, but it
has to be invoked via a specialized name because the operator “
=
” denotes only the generic version
of equality. For this reason some Standard ML designers (Harper and MacQueen) suggested that
polymorphic equality be removed from the language during the work on the Denition (Revised),
or at least that it be deprecated.
4.6 Overloading
LCF/ML had no overloading. There was only one numeric type,
int
, and the arithmetic and order
operations were dened only for
int
. On the other hand, HOPE had a very general scheme for
overloading, where functions, constants, and datatype constructors could all be overloaded. In HOPE,
overloading was resolved against the full contextual type, which allowed overloaded constants and
overloaded operations that were resolved by their return type. The general overloading scheme
coexisted fairly well with polymorphism (borrowed from LCF/ML), but the overloading resolution
algorithm was non-trivial, being based on Waltz’s relaxation algorithm for computer vision [Waltz
1975].
The complexity of overloading in Hope led some of the SML designers to oppose adding over-
loading to the language, but, in the end, the decision was to support “conventional” overloading of
arithmetic and order operators on numeric types, and order operations on character and string
types. Since all the instances of overloaded operators are of ground type, it is fairly easy to re-
solve overloading, but it must be resolved in a second phase after type inference, because type
inference provides the contextual type information needed to resolve overloading. The Denition
(Revised) extended this mechanism to also support overloading of literals (e.g., integer literals can be
overloaded in terms of precision) and added the notion of default types for overloaded constants.
44
4.7 Understanding Type Errors
Type inference imposes a pragmatic cost: determining the cause of type errors can sometimes be
dicult, because type information ows from one place in the program text to another, possibly
distant, point by way of a series of unications and their resulting type variable substitutions. Most
type checkers detect an error (depending of course on the details of the algorithm used) at a point
where two pieces of type information come into conict. Because of the implicit type ow, the
program point where a conict is detected may not be the source of the type error. The cause of the
error may also not be one of the points where the conicting pieces of type information originate,
44
Before default types, the function “
fn x=> x+x
” could not be assigned a type, which was quite mysterious to novice
users, but under the Denition (Revised), the type of this function would be “
int >int
,” since “
+
” defaults to integer
addition.
Proc. ACM Program. Lang., Vol. 4, No. HOPL, Article 86. Publication date: June 2020.
86:34 David Maceen, Robert Harper, and John Reppy
but some intermediate point where the type ow is misdirected (e.g., by projecting the wrong
component of a pair).
In practice, the problem of understanding the cause of type error messages is mainly an issue
for relatively novice ML programmers. Experienced programmers develop a facility for mentally
tracing the ow of types, and they can easily nd the cause of most type errors. The long-distance
ow of type information that can lead to mysterious type-error messages can be interrupted by
module boundaries and by selective addition of explicit type specications in declarations, which
also help make code more readable. In those rare cases where the cause of a type error is not evident
after a few minutes thought, the source of an error can generally be determined by judicious
addition of some explicit type specications.
As mentioned above (Section 4.2), the type checking algorithms used in a compilers have a
great deal of freedom to choose the order in which the abstract syntax tree is traversed. Type
checkers typically do incremental unications to solve typing constraints at each node of the
abstract syntax tree, but they can use dierent strategies to control the “timing” of unications
that may produce type errors. This exibility can be used to improve the locality of error messages
by careful choice of the order of traversing the abstract syntax, using a combination of top-down
and bottom-up strategies [Lee and Yi 1998], or by choosing the points where unications are
performed [McAdam 2002,1998]. The default traversal orders used in theoretical descriptions, such
as Milner’s Algorithm W, were not designed with the goal of good type error messages and are
therefore not optimal for this purpose.
Nevertheless, deciphering type error messages is clearly a challenge for students learning ML.
Facilities that help explain type errors can also be useful for teaching programmers how type
inference and polymorphic typing work, even in the absence of type errors. Thus, over the years,