ML97 EDITION
ELEMENTS OF
ML PROGRAMMING
JEFFREY D, ULLMANElements of ML
Programming
ML97 Edition
Jeffrey D. Ullman
== An Alan R. Apt Book
ast
Prentice Hall
Upper Saddle River, New Jersey 07458Library of Congress Cataloging
Ullman, Jeffrey D.
Elements of ML programming/ Jeffrey D. Ullman.
ML97 Edition
Pp. cm
“An Alan R. Apt. Book”
Includes bibliological references and index.
ISBN: 0-13-790387-1
1. ML (Computer program language). 1. Title.
CIP DATA AVAILABLE
Acquisitions editor: ALAN R. APT
Editor-in-chief: MARCIA HORTON
Production editor: IRWIN ZUCKER
Managing editor: BAYANI MENDOZA DE LEON
Director of production and manufacturing: DAVID W. RICCARDI
Cover director: JAYNE CONTE
Manufacturing buyer: JULIA MEEHAN
Editorial assistant: TONT CHAVEZ
© 1998, 1994 by Prentice-Hall Inc.
El Upper Saddle River, New Jersey 07458
reproduced, in any form or by any means,
without permission in writing from the publisher.
‘The author and publisher of this book have used their best efforts in preparing this book. These efforts include the
development. research. and testing of the theories and programs to determine their effectiveness. The author and
Printed in the United States of America
098765
ISBN QO-13-790387-1
Prentice-Hall International (UK) Limited, London
Prentice-Hall of Australia Pty. Limited, Sydney
Prentice-Hall Canada Inc.. Toronto
Prentice-Hall Hispanoamericana. S.A.. Mexico City
Prentice-Hall of India Private Limited. New Delhi
Prentice-Hall of Japan. Inc... Tokyo
Pearson Education Asia Pte. Ltd.. Singapore
Editora Prentice-Hall do Brasil, Ltda.. Rio de JaneiroPreface
I became interested in ML programming when I taught CS1U9, the introduc-
tory Compnter Science Foundations course at Stanford, starting in 1991. MT.
was used by several of the instructors of this course, including Stu Reges and
Mike Cleron, to introduce concepts such as functional programming and type
systems. It was also used for the practical purpose of introducing a second
programming paradigm, other than the Pascal or C that students learned in
the introductory programming course. Reimplementing algorithms and data
structures in a significantly different language often is an aid to understanding
of basic data structure and algorithm concepts.
i firsi learned ML from the notes thai Reges and Cierou had written for
their students. Initially, I was intrigued by the rule system, which gave me
much of the power of Prolog, a language with which I had worked for several
years. Yet ML did not introduce the semantic complexity that comes from the
use of unification and backtracking in Prolog. However, I soon discovered other
charms of ML: the type system, the use of exceptions, and the module system
for creating abstract datatypes, among others. From the Reges and Cleron
notes I also picked up the utility of giving the student a fast overview, stressing
the most commonly used constructs rather than the complete syntax.
In writing this guide to ML programming, I have thus departed from the
approach found in many books on the language. As an outsider, I had the
opportunity to learn the language from the standpoint of the typical program-
mer. I have tried to remember how things struck me at first, the analogies I
diew with conventional languages, and
© coucepis that I found most useful
in getting started. I hope that my selection is accurate, and that the book will
facilitate the reader's transition from conventional languages to ML.
The Second Edition
You are reading the second edition of the book. The primary change between
the first and second editions is that the second conforms to the new language
standard called ML97. All major implementations of ML either have converted,
or are in the process of converting, to this standard. For the few matters that
are implementation-dependent, such as the choice of diagnostics, the second
iiiiv PREFACE
edition, like the first, follows the Standard ML of New Jersey (SML/NJ) im-
plementation. SML/N4J is the work of Andrew Appel of Princeton University,
David MacQueen of Lucent/Bell Labs, and their colleagues.
The fo! by
The following is b:
y
correspondence between the first and second editions.
« Chapter 1 corresponds to the old Chapter 0.
« Chapter 2 lays the groundwork for ML programming. Sections 2.1 through
2.4 correspond to the old Chapters 1 through 4, respectively.
© Chapter 3 introduces functions in ML. The old Chapter 5 is now Sections
3.1 and 3.2, while the old Chapter 6 appears in Sections 3.3 and 3.6. Old
Chapter 7 has become Section 3.4, while Sections 3.5 and much of Section
3.6 are new.
© Chapter 4 covers ML mput and output. ‘Lhe old Chapter Y has been split
among Sections 4.1, 4.2, and 4.4, while the new Section 4.3 comes from
the old Chapter 22.
© in Chapier 5 we reiurn io ide subject of funciious in ML, presenting a
number of advanced topics. Section 5.1 covers matches and patterns like
the old Chapter 19. Section 5.2 covers exceptions, from the old Chapters
8 and 20. Section 5.3 covers polymorphism as in the old Chapter 10.
Material on higher-order functions from the old Chapters 11 and 21 is
now split among Sections 5.4 throngh 5.6. ‘The case stndy in Section 5.7
was originally part of the old Chapter 22.
© Chapter 6 introduces datatypes. The old Chapter 12 is split between
Sections 6.1 and 6.2, while old Chapter 13 is now Sections 6.3 and 6.4.
© Chapter 7 presents a number of advanced topics about data structures,
Section 7.1 covers record structures, the old Chapter 18. Material on
arrays, the old Chapter 16, is now in Sections 7.2 and 7.4. Old Chapter
17, about references, is in Sections 7.3 and 7.5.
© Chapter 8 covers the ML module system. Old Chapter 14 is split among
Sections 8.1 through 8.3, and old Chapter 15 is now in Sections 8.4 and
8.5. The case study in Section 8.6 is new.
© Finally, Chapter 9 attempts to summarize the entire language. Some
concepts not appearing elsewhere in the book are introduced as well.
Section 9.i on infix operators, corresponds to the oid Chapter 24. Sections
9.2 and 9.3 summarize what is called the “top-level environment,” the set.
of features one has in ML without asking for them explicitly. Then Section
9.4 summarizes the “standard basis,” or capabilities one can obtain if one
calls for them explicitly. Some of the old Chapter 25 is spread among
Sections 9.2 through 9.4, but much of these sections is new for ML97.PREFACE v
Section 9.5 corresponds to the old Chapter 23 and covers some important.
features found only in SML/N4, involving the creation of executable fil
Section 9.6 concludes with syntax diagrams for the entire language and is
Features of the Book
The test of a language is not the hest or most. succinct. examples of its use.
Rather, a language will only be adopted widely if it can handle everyday pro-
gramming chores well. Thus, I have considered in this book many of the most
common data structures, such as trees and hash tables, and many of the most
common algorithms, such as sorting or Gaussian climination. I think the reader
will be impressed by how well ML handles these standard tasks that were se-
lected because of their ubiquity, not because they exhibit special features of the
language.
To focus the reader's attention, I have inserted boxes at various places in
the text. These boxes are interruptions from the main focus of the text, but
the other hand, footnotes are also interruptions to the main thread, but they
are there only “for the record” rather than as an aid to understanding.
Exe:
re.
Most of the sections have exercises at the end. In the text, we indicate that an
exercise or part of an exercise has a published solution by preceding the exercise
or part of an exercise by a star. You can find solutions to exercises with stars
at URL http://www-db.stanford.edu/“ullman/emlpsols/sols.html.
Exercises are graded by difficulty. Harder exercises are indicated by an
exclamation point in the margin, and a few of the hardest exercises have two
exclamation points.
Use of the Book
The book as a whole is a tutorial and reference for the person who wants
to program productively in ML. Although there are occasional references to
4
conventional languages like C ur Pascal, I believe the buok is suffici
contained that it could be used to teach ML as a first programming language.
When we teach students ML in the CS109 course at Stanford, the material
covered corresponds closely to what is in Chapters 2, 3, 4.1, 5, 6, and 7. The
book can be used as a supplement to a programming language concepts course,
in which case Chapter 8 would surely be included.at TABLE OF CONTENTS
Acknowledgments
I would like to thank Andrew Appel and David MacQueen, both of whom
filly critiqued the original edition. Matthias Blume was equally a boon
for the second edition. They are all three tied for the title of “world’s greatest
referee.”
I value a number of important pointers on ML from John Mitchell. Also,
Henry Bauer, Richard LeBlanc, Peter Robinson, and Jean Scholtz have my
appreciation for their work as referees of the first-edition manuscript.
Errata from the first edition were found and pointed out to me by Baoquan
Chen, Franklin Chen, Martin Brwig, Mark Girod, Naomichi Komuro, Hugh
Finally, I appreciate the help from several members of the core ML commu-
nity that kept me informed of changes and encouraged me to keep this book on
track. I even got help on the design of the cover for the first edition (see the
following nove), which has Deen carried over to the second edition as well.
Cover Art
Special thanks go to Luca Cardelli, who volunteered to create original art for
this book. The result is on the cover.
Supplementary Material on the Web
The book’s home page is ww-db.stanford.edu/“ullman/emlp.html. There
you can find:
1. Solutions to starred exercises.
. Code for the major programs in the book.
. Errata.
Fen
Notes and exams from Stanford’s CS109 involving ML.
es
. Links to ML documentation and resources.
1.D.U.
Stanford CA
September, 1997Table of Contents
1 A Perspective on ML and SML/NJ 1
11 Why ML? ... 0... 2 ee eee bene sees
1,2 Standard ML of New Jersey
1.3. Prerequisites for the Reader
1.4. References and Web Resources
1,5 Features of ML97............
2 Getting Started in ML
2.1 Expressions ........
2.1.1 Constants :
2.1.2 Arithmetic Operators
2.1.3 String Operators
2.14 Comparison Operators
2.1.5 Combining Logical Values
2.1.6 IfThen-Elsc Expressions... .. . . bene
2.17 Exercises for Section 21...........
2.2 Type Consistency
22.1 Type Errors... 2... ee eee
2.2.2 Coercion Between Integers and Reals . . . .
2.2.2 Coercions Retween Characters and Integers .
2.2.4 Coercions Between Strings and Characters
2.2.5 Exercises for Section 2.2... .
2.3. Variables and Environments .
Identifiers... . . tees
‘Lhe Top-Level Environment . .
An Assignment-Like Statement. .
A View of ML Programming
2.3.9 Exercises for Section 2.3
2.4 Tuples and Lists sees
a
2.4.2 Accessing Components of Tuples
243 Lists. ..........00% :
2.4.4 List Notation and Operatorsviii TABLE OF CONTENTS
2.4.5 Converting Between Character Strings aud Lists... . . 40
2.4.6 Introduction to the ML Type System - 41
24.7 Exercises for Section 2.4..........4 42
3 Defining Functions 45
3.1 It’s Easy; It’s fun. : 45
3.1.1 Function Types . . see 46
3.1.2 Declaring Function Types .: 7
3.1.3. Function Application... . . 49
3.14 Functions With More Than One Parameter... 50
3.1.5 Functions that Reference External Variables . . 52
3.1.6 Exercises for Seciion 3.
3.2. Recursive Functions
3.2.1 Function Execution
3.2.2 Nonlinear Recursion .
3.2.3 Mutual Recursion
3.2.4 How ML Deduces Types -
3.2.5 Exercises for Section 3.2 .
3.3 Patterns in Function Definitions
3.3.1 Patterns as Function Parameters
“As” You Like it: Having it Both Ways ‘
Anonymous Variables... . +
What Is and What Isn’t a Pattern?
How ML Matches Patterns
A Subtle Pattern Bug ... .
Exercises for Section 3.3
3.4 Local Environments Using let... 0.0 eee eee
3.4.1 Defining Common Subexpressions ......-.-.
34.2. Effect on Environments of let
3.4.3 Splitting Apart the Valuc Returned by a Function .... 80
3.44 Mergesort: An Efficient, Recursive Sorter... .. 2...
3.4.5 Exercises for Section 3.4
3.5. Case Study: Lincar-Time Reverse... 0. eee eee
3.5.1 Analysis of Simple Reverse... 0... «
3.5.2. ML’s Representation of Lists Ls
3.5.3 A Reversal Function Using Difference Lists . . -
3.5.4 Analysis of Fast Reverse .
3.5.5 Exercises for Section 3.5. . .
3.6 Case Study: Polynomial Multiplication
i Representing Poiynomiais by List =
A Simple Polynomial-Multiplication Algorithm . .
Analysis of Simple Multiplication . . . .
Auxiliary Functions for a Faster Multiplication . . .
‘The Karatsuba-Ofman Algorithm... .
Analysis of the Karatsuba-Ofman AlgorithmTABLE OF CONTENTS ix
3.6.7 Exercises for Section 3.6... 6.2.0 eee 98
4 Input and Output
41 Simple Ontpnt
4.1.1 The Print Function... ....
4.1.2 Printing Nonstring Values . . .
4.1.3 “Statement” Lists ....... =
4.14 Statement Lists Versus Let Expres ions
4.1.5 Exercises for Section 4.1...
4.2 Reading Input Froma File ......
Instreams
Reading Characters From a File... 2.2
Reading Lines of aFile ...........
Reading Complete Files... .
Reading a Single Character . .
.2.6 Lookahead on the Input .. . .
4.2.7 Closing Instreams
4.2.8 Exercises for Section 4.2... .
4.3 Output to Files .
43.1 Outstreams
4.3.2 Closing Outstreams 2...
4.3.3 The output Command
43.4 Exercises for Section 4.3
44 Case Study: Sununing Integers... eee
44.1 The Function startInt .............-.00005
44.2 The Function finishInt
44.3 The Function getInt 2... 2... ee ee
444 The Function sumInts. 0.2.00... eve eee
4.4.5 Eager Evaluation
4.4.6 Exercises for Section 4.4... 0... 0. eee ees
5 More About Functions
5.1 Matches and Patterns 2... 0... 0.0000 eee
BLL Matches... oe cee eee
5.1.2 Using Matches to Define Functions . . . .
5.1.3 Anonymous Functions ..... 0.2...
5.14 Case Expressions 2.0.0... 0200 -
5.1.5 If-Then-Else Expressions Revisited
5.1.6 Exercises for Section 5.1.......-.
Exceptions
5.21 User-Defined Exceptions .
5.2.2 Expressions With Parameters
5.2.3 Handling Exceptions... . .
5.2.4 Exceptions as Elements of an Em eee .
5.2.5 Local Exceptions
o
R5.3
5.5
56
5.7
Defining Your Own Types
61
6.2
6.3
TABLE OF CONTENTS
5.2.6 Exercises for Section 5.2... . .
Polymorphic Functions... . .
5.3.1 A Limitation on the Use of Polymorphic Funetions . . . . 145
5.3.2 Operators that Restrict Px wee —=
5.3.3 Operators that Allow Polymorphism .
5.34 The Equality Operators .. . .
5.3.5 Exercises for Section 5.3... .
Highes-Order Functious oo 1
5.4.1 Some Common Higher-Order Functions. .
5.4.2 A Simple Map Function
5.4.3 The Function reduce 2.0... 0... e eee eee
5.4.4 Converting Intix Uperators to Function Names ...... 165
5.4.5 The Function Filter
5.4.6 Exercises for Section 5.4... . «
Curried Functions 2... 2.0.00.
5.5.1 Partially Instantiated Functions
5.5.2 The ML Style of Function Appli
5.5.3 Exercises for Section 5.5... 0.2.0... .
Built-In Higher-Order Functions
5.6.1 Composition of Functions ..........
5.6.2 The ML Operator o For Composition . . .
5.6.3. The “Real” Version of Map . :
5.6.4 Folding Lists .
.6.5 Exercises for Section 5.6 .
Case Study: Parsing Expressions . . . :
5.7.1 The Grammatical Structure of Arithmetic EBxpresons . 184
.7.2 Structure of the Parsing Program . . . .
5.7.3 Detailed Explanation of the Parser Code. =
5.7.4 Exercises for Section 5.7 . .
Defining New Types
6.1.1 Review of the ML Type System. 6.2... eee
6.1.2 New Names for Old Types. . . :
6.1.3. Parametrized Type Definitions
6.14 Exercises for Section 6.1... .
Datatypes... 2.2... =
6.2.1 A Simple Form of Datatype Declaration . .
6.2.2 Using Constructor Expressions in Datatype Definitions
6.2.3 Recursively Defined Datatypes
6.2.4 Mutually Recursive Datatypes
6.2.5 Exercises for Section 6.2... .
Case Study: Binary Trees
6.3.1 Binary Search Trees .
6.32 Lookup in Binary Search ‘TreesTABLE OF CONTENTS xi
6.3.3
6.3.4
6.3.5
6.3.7
6.3.8
64 Case Study: General Rooted Trees
GAL
6.4.2
6.4.3
6.44
7 More About ML Data Structures
7.1 Record Structures
TL
T12
713
714A
71S
716
x
ey
7.21
7.22
7.2.3
Arrays
Insertion into Binary Search Trees 2... 0 eee 214
Deletion from Binary Search Tre
Some Comments About Running Time .
Prowler ‘Traversals . wee
Exercises for Section 6.3... .
A Datatype for Trees
Summing the Labels of a General ‘Iree . . . =o
Computing Sums Using Higher-Order Functions 227
Exercises for Section 64... 0.000000. e eee ee 227
Records and Their Types . . .
Extracting Field Values .. . .
Tuples as a Special Case of Record Structures
Patterns That Match Records .
Shorthands in Record Patterns
Exercises for Section 7.1
Why Do We Need Arrays’. . .
Array Operations
Exercises for Section 72.0... 00 cee eee ee
(ebm RetereCeS a mee meee omem memes Seems eee ees Se 242
731
7.3.2
7.3.3
7.34
7.3.5
74 Case Study: Hash Tables
7A1
TAD
74.3
744
7.5 Case Study: Triangularization of a Matrix
T51
75.2
7.5.3
8.11
8.1.
8.2 Structures... . 2... 0.000.
Encapsulation and the ML Module System
8.1 Why Modules?
The ref Type Constructor
Obtaining the Value of a Ref-Variable .. 2.6... 21.
Modifying Ref-Variables..........0.002.005
The While-Do Statement
Exercises for Section 7.3.0.0... 0.00 c eee eee
The Dictionary Ope:
How a Hash Table Works sae see
‘An Example of Hash ‘Table Implementation... .
Exercises for Section 7.4
Creating and Initializing the Matrix... .
‘Triangularization by Row Operations
Exercises for Section 7.5... 0. eee eee
Information Hiding... ... .
Clustering Connected ElementsTABLE OF CONTENTS
8.2.1 Signatures... ee eee eee ees 262
8.2.2 Restricting Structures Through ‘Their Signatures... . . 264
8.23 Accessing Names Defined Within Structures
8.24 Opening Structures . sees
82.5 Exercises for Section 8.2 .
8.3 Functors..... ee
8.3.1 Motivation for Functors . 5. . =
83.2 Using Fuucturs w Import Information . . «
8.3.3 More General Forms for Functor Parameters and “Argu-
ments
8.34 Exercises for Section 83... «
84 Sharings.... 0.2... eee
84.1 Sharing Specifications
8.4.2 Substructures .
8.4.3 Sharing of Types .
844 Sharing of Substructures |
8.4.5 Exercises for Section 8.4 .
8.5 ML Techniques for Hiding Information
8.5.1 An Information-Hiding Problem
8.5.2 Using Signatures to Hide Information
8.5.3 Abstract Types . .
8.5.4 Local Definitions .
8.5.5 Opaque Signatures =
8.5.6 Exercises for Section 8.5 .
8.6 Case Study: Feedback Shift Registers :
8.6.1 Operation of a Feedback Shift Register . .
8.6.2 A Functor to Create Random Number Generators. .
8.6.3 Generating a Feedback Shift Register
8.6.4 Exercises for Section 8.6... ......-
Summary of the ML Standard Basis
9.1 The Infix Operators
9.1.1 Precedence»... 6. eee eee eee
9.1.2 Precedence Levels in ML
9.1.3 Associativity of Operators . .
9.14 Creating New Infix Operators
9.1.5 Infix Data Constructors . . .
9.1.6 Exercises for Section 9.1. . . eee
9.2 Functions in the Top-Level Environment . . . . .
9.2.1 Functions on Integers
9.2.2 Functions on Reals... .. .
9.2.3 Functions on Booleans . . . .
9.2.4 Functions on Characters . . .
9.2.5 Functions on Strings... . .
9.26 Functions on OptionsTABLE OF CONTENTS xiii
9.3
9.5,
96
9.2.7 Functions on References... .
9.2.8 Functions on Lists ae
.9 Functions on Exceptions
R
9.2.11
‘Top-Level ‘
9.3.1 Primitive Types
9.3.2 Primitive Type Constructors.
9.3.3 Primitive Datatypes
9.3.4 Top-Level Exceptions
9.3.5 Exercises for Section 9.3... . .
pes and Exceptions... .
Structures of the Standard Basis...
9.4.1 The Structure Int
942 The Structure Word
9.4.3 The Structures Real and Math .
¥.4.4 ‘Lhe Structure Char
9.4.5 The Structure String... . .
9.4.6 The Structure Substring . .
9.4.7 The Structure List... . .
9.4.8 The Structure Array . .
9.4.9 The Structure Vector .
9.4.10 The Structure 0S os
9.4.11 The Structures Time and Timer .
9.4.12 What If I Lose a Name? . . .
9.4.13 Exercises for Section 9.4...
Additional Features of SML/NJ
9.5.1 Exporting Functions
9.5.2 Exporting the ML Environment
9.5.3 Exercises for Section 9.5.0... 0.0... eee eee
Summary of MI Syntax
9.6.1 Lexical Categories... eee
9.6.2 Some Simplifications to the Grammatical Structure .
9.6.3 Expressions... 2.2... an .
9.6.4 Matches and Patterns .. . .
9.6.5 Types ......
9.6.6 Declarations. . .
9.6.7 Signatures... .
9.6.8 Structures... .
9.6.9 Functors.... .Chapter 1
A Perspective on ML and
OQnaAT /NIT
SIVEL/ ING
In this preliminary chapter we shall introduce the reader to the history of ML
and the reasons for its existence and popularity. We shall also present the
mechanics of using a particular implementation of ML, called Standard ML
of New Jersey, which is the implementation used for examples in this book.
Finally, some general references on the language are given, including URL's for
obtaining ML compilers aud on-line documentation.
1.1. Why ML?
ML is a relatively new language that has some extremely interesting features.
Its designers incorporated many modern programming-language ideas, yet the
language is surprisingly easy to learn and use. In this section we shall enumerate
the most important of these features.
A Functional Language
MLis primarily a functional language, meaning that the basic mode of compu-
tation is the definition and application of functions. Functions can he defined
by the user as in conventional languages, by writing code for the function. But
it is also possible in ML to treat functions as values and compute new functions
from them with operators like function composition.
Side-Effect Freedam
‘A consequence of the functional style is that computation proceeds by eval-
uating expressions, not by making assignments to variables. There are ways
to give expressions side-effects, which are operations that permanently change
12 CHAPTER I. A PERSPECTIVE ON ML AND SML/NJ
the value of a variable or other observable object (e.g., by printing output).
However, side-effects are treated as necessary aberrations on the basic theme.
Tn contrast, languages like Pascal or C use statements with side-effects as a
effect, since the value of variable a is changed after the assignment is executed.
In contrast, when ML evaluates an expression like bt, it typically creates an
entirely new element with which to associate the result.
Higher-Order Functions
ML supports higher-order functions — functions that take functions as argu-
ments — routinely and with great generality. In comparison, languages like
Pascal or C support functions as arguments only in limited ways.
Polymorphism
ML supports polymorphism, which is the ability of a function to take arguments
of various types. For example, in Pascal or C we may have to create different
mg 9 hg
of pairs of integers,” and so on. We would then have to define operations like
“push” and “pop” for each different type of stack. In ML, we can define one
notion of a stack, one push function, and one pop function, each of which works
no matter what type of elements our stacks have.
Abstract Data Types
ML supports abstract data types through:
1. An elegant type system,
2. The ability to construct new types, and
3. Constructs that restrict access to objects of a given type so all access is
through a fixed set of operations defined for that type.
‘An example is a type like “stack,” for which we might define the push and pop
operations and a few other operations as the only way the contents of a stack
could be read or modified.
These abstract data types, called structures, offer the power of “classes”
used in object-oriented programming ianguages like C‘*, Java, or Smailitalk.
‘They are considered very important for such programming goals as modularity,
encapsulation of concepts, and reusc of software. However, the ML notion of
a structure also includes and generalizes several other important ideas, such as
the libraries of functions provided in many languages and “friend” classes in
C++1.1. WHY ML? 3
Recursion
ML strongly encourages recursion in preference to iterators like for-loops or
whileloops that are need commonly in Pascal or C. Reenrsion generally pro-
vides a cleancr expression for computational ideas, especially when coupled
with ML’s functional programming style. We shall learn a natural, recursive
style of programming similar to that used in Lisp or Scheme. However, iterative
constructs are available in ML for the times when that style is most appropriate.
Rule-Based Programming
There is in ML an easy way to do rule-based programming, where actions are
based on if-then rules. The core idea is a pattern-action construct, where a value
is compared with several patterns in turn. The first pattern to match causes
an associated action to be executed. In this way, ML has much of the power
of Prolog and other languages that are thought of as “artificial intelligence
languages.”
Strong Typing
ML is a strongly typed language, meaning that all values and variables have a
type that can be determined at “compile time” (ie., by examining the program
but not running it). A value of one type cannot be given to a variable ot
another type. For example, the integer value 4 cannot be the value of a real-
valued variable, even hough the real 4.0 could be the value of that variable.
Many other languages allow confusion of types. For example, C allows a value
to change its type arbitrarily, through the “cast” mechanism, while Lisp and
Prolog do not try to constrain types in general.
Strong typing is a valuable debugging aid, since it allows many errors to
he by th her th hen the
program is run. Interestingly, although most other strongly typed languages
require a declaration of the type of every variable, ML tries hard to figure out
the unique type that each variable may have, and only expects a declaration
for a variable when it is impossible for ML wo deduce its type.
ML is not the only language to possess these features. For example, Lisp is
principaily functional, supports higher-order functions, and promotes the use
of recursion. Prolog also promotes recursion and supports rule-based program-
ming naturally. Smalltalk and C++ offer powerful abstract-data-type facilities,
and so on. However, the combination of features found in ML offers the user &
great deal of programming ease. At the same time, ML allows one to use a full
palette of modern programming language concepts4 CHAPTER 1. A PERSPECTIVE ON ML AND SML/NJ
1.2 Standard ML of New Jersey
In this book, we shall assume that the SML/NJ, or “Standard ML of New
of ML is used SML, INT wae ted hy Daw
NacGucen of Lucent Bell Laboratories, Andrew Appel of Princeton University,
and their colleagues. It is available for most UNIX workstations, for PC’s
running LINUX, and in an experimental version (at the time of this book’s
writing) for PC's running Microsoft Windows. There is a Web site at Bell
Laboratories from which software and documentation may be downloaded; see
the references in Section 1.4. SML/NJ version 109.30, upon which this book is
based, has been implemented to conform to the recent ML97 standard.
Interactive Mode
To run SML/N4J in interactive mode, in response to the UNIX prompt type
snl
SML/NJ will respond with:
Standard ML of New Jersey ---
Here, as throughout this book, we shall use italic font to indicate ML’s re-
sponses, while text typed by the user will be in the “teletype” font, as sml
ahove.
The dash on the second line is ML’s prompt. The prompt invites us to type
an expression, and ML will respond with the value of that expression. We can
make definitions and enter expressions indefinitely, and SML/NJ will respond
to cach with the resulting value.
¢ To terminate an SML/NJ session, type
d.
Direct Program Execution
It is also possible to get SML/NJ to execute a program in a conventional way.
For example, if your ML program is in file foo, give the sm1 command with
that file as standard input:
sul < foo
Another option is to issue the sml command to UNIX, which gets us started
in interactive mode. Then, in response to the prompt, read and cxecute a file
foo that coutains an ML program. We do so by typing to ML the expression
use "foo";1.3. PREREQUISITES FOR THE READER 5
Any quoted UNIX path name can appear in place of "foo". ‘his mode is handy
when we are debugging a program and want to read in its definitions and then
try them in interactive mode,
There is a third way to get an Mi program to run using SML/NJ. One
can compile an ML program source file into a file that includes the SML/NJ
runtime system and thus can be executed directly without. invoking command
smi. This mode of operation is discussed in Section 9.5.
What ML Gives You
When we invoke ML. we are given access to several resources. In MI., all
available capabilities — operators like + for addition, functions like sin, and
some very complex operators not present in other languages — are organized
into structures, such as Int and Real, as suggested in Fig. 1.1. The entire
collection of capabilities is called the standard basis
Top-level
SS) =] Environment }
Structures
Figure 1.1: Organization of resources in ML
A structure in ML is akin to a library in most other languages. For example,
the structure called Int contains many functions useful for dealing with inte-
includes some
less typical operators, Thus, ML selects the most important operators from the
various structures and puts them in the top-level environment. ‘These capabil-
ities are available when we invoke ML. The additional capabilities, those that
are found in the various structures but that are not part of the top-level envi-
ronment, are also accessible if we make a small amount of additional effort. We
shall describe how to access those capabilities not in the top-level environment
starting in Section 4.1.2.
1.3 Prerequisites for the Reader
We assume the reader is familiar with programming in some conventional lan-
guage such as Pascal or C. Occasionally, as a matter of interest, we shall6 CHAPTER 1. A PERSPECTIVE ON ML AND SML/NJ
compare ML constructs with those of Pascal or C, but familiarity with one or
both of these languages is not essential.
It is also assumed the reader is familiar with the process of writing and
debugging progr
has written at least a few recursive programs and has some comfort with that
style of programming. However, our first recursive examples will be covered
in sufficient detail that the style may be learned here. In addition, we assume
the ivader is faiuilias with simple data structures and data structure concepts
such as records, pointers, lists, and trees. ‘he author immodestly recommends
Foundations of Computer Science: C Edition by A. V. Aho and J. D. Ullman,
Computer Science Press, New York, 1995 for the reader who desires further
background on these subjects.
1.4 References and Web Resources
The original definition of Standard ML is from [3], which evolved into the book
[5]. An elaboration of this work is [4]. This version of ML is now given the
retronym MLO0.
The recent revision, called MLST, is described in ie buuk [6]. Au iapuriani
part of the definition of ML97 is the standard basis, which is obtainable on-line
in [1]
The original paper on the Standard ML of New Jersey implementation as-
sumed in this book is [2]. There is an extensive resource library available on-line.
The root. URI. is:
http: //cem. bell-labs . com/em/cs/vhat/smlnj
A uscful on-line document is
http: //cm. bel1-1abs.. com/cn/cs/what/smnj/top-level-compar ison html
which explains the difference between earlier versions of SML/NJ and the cur-
rent, ML97-based versions starting with Version 109.24.
To obtain software to run SML/NJ on various hosts, start at
http: //cm.bell-labs.com/cm/cs/what/smlnj/software. html
An important source for non-UNIX implementations of ML is
http: //www.dina.kvl.dk/~sestoft/mosml.html
which is the Keldysh Institute of Applied Mathematics in Moscow. Their im-
plementation runs on PC’s and MAC’s, as well as workstations, and largely
conforms to ML97 at the time this book was written.
1. Appel, A. W., N. Barnes, D. Berry, E. R. Gansner, L. George, L. Huels-
bergen, D. MacQueen, B. Monahan, C. Miller, J. H. Reppy, J. Thackray,
and P. Sestoft, The Standard ML Basis Library. Its URL is:15.
N
+
°
FEATURES OF ML97
http://cm.bel1-Labs.com/cm/cs/what/sminj/sm197.htm1
Appel, A. W. and D. B. MacQueen, “Standard ML of New Jersey,” Inter-
national Symposium on Programming Languages, Implementation
Logic, pp. 1-13, Springer-Verlag, 1991 is a technical article describing the
SML/NGJ system.
Harper, R. M., D. B. MacQueen, and R. Milner, “Standard ML,” ECS-
LFCS-86-2, Laboratory for Foundations of Computer Science, Edinburgh
University, Dept. of CS, 1986.
Milner, R. and M. Tofte, Commentary on Standard ML, MIT Press, Cam-
bridge MA, 1991.
Milner, R., M. Tofte, and R. M. Harper, The Definition of Standard ML,
MIT Press, Cambridge MA, 1990.
. Milner, R., M. Tofte, R. M. Harper, and D. B. MacQueen, The Definition
of Standard ML (Revised), MIT Press, Cambridge, MA, 1997.
1.5 Features of ML97
If you are familiar with the earlier version of ML called ML90, then you will
notice certain differences between ML90 and the version ML97 covered in this
book. If you are not familiar with ML90, then skip this section, The complete
list of changes is found in the ML97 source book, reference [6] above. However,
for the reader with ML experience, the following is an incomplete list of the
changes that are most likely to affect your programming.
e
x
. There is a cleaner organization to features that are available in the top-
level basis and features that are available through a library structure.
. Certain values with unknown type, such as nil, are no longer legal ex-
pressions. However, polymorphic functions remain a feature ot ML.
Input /ontput operators are now defined as part of the standard, rather
than being implementation-dependent.
. Characters are now a separate type, different from strings of length 1.
. Reals are no longer an equality type; i.e, you cannot test r = s for reals
rand s
. There is an unsigned integer type called word. Both words and ordinary
integers may be represented in hexadecimal, if we wish.
. A datatype called option is provided to represent elements that are Op-
tionally missing.CHAPTER 1. A PERSPECTIVE ON ML AND SML/NJ
8. Requirements to specify types are reduced because there is a default type
(integer) for overloaded operators such as + or <.Chapter 2
Getting Started in ML
In this chapter we shall introduce the reader to the simplest form of program-
ming in ML, where one types expressions to the ML system and receives back
values for these expressions. We shall learn how to construct expressions using
atomic types such as integers and strings. We shall also discuss expressions in-
volving lists and a simple form of record structure called tuples; lists and tuples
are both basic ML constructs. The reader will also see an important difference
between ME and most other languages: the ML rules regarding types of ex-
pressions allow the ML compiler to check at compile time for type errors that
in other languages can lead to mysterious run-time bugs
2.1 Expressions
When we are in interactive mode, the simplest thing we can do is type an
expression in response to the ML prompt (-). ML will respond with the value
and its type.
ML response.
142435
val it = 7: int
Recall from Scction 1.2 our convention that we use “teletype” font for things
we type and italic font for the response of the ML system. Here, we have typed
the expression 1 + 2¥ 3, and ML responds that the value of variable it is 7,
and that the type of this value is integer. The variable it plays a special role
in ML, It receives the value of auy expression that we type in interactive mode.
a
Two useful points to observe from Example 2.1 are:10 CHAPTER 2, GETTING STARTED IN ML
© An expression must be followed by a semicolon to tell Une ML system that
the instruction is finished. If ML expects more input when a
is typed, it will respond with the prompt ‘The = sign is a
warning that we have not Snished our
© The response of ML to an expression is:
1. The word val standing for “value,”
2. The variable name it, which stands for the previous expression,
3. An equal sign,
¢
i
*
5. A colon, which in ML is the symbol that associates a value with its
type, and
6. An expression that denotes the type of the value In our example,
the value of the expression is an integer, so Uhe type int follows the
colon.
2.1.1 Constants
As in any other langnage, expressions in MI, are composed of operators and
operands, and operands may be cither variables or constants. At this point,
we have not yet discussed the way values may be assigned to variables, so it
does not make sense to use variables in expressions. However. syntactically.
variables present no surprises. You may think of Pascal identifiers (letters
followed by letters or digits) or the identifiers in your favorite language as names
for ML variables, although as we shall see in Section 2.3.1, ML identifiers differ
somewhat from identifiers in these languages.
ML provides as part of its top-level environment (see Section 1.2) a number
of types that are similar to those found in most languages. There is also a way
to ert from the system some additional types. In this preliminary discussion,
iuoduce only the most commonly used atomic types aud
allowable values. The complete set of types is discussed in Section 9.3.
Integers
Integers are represented in ML as in other languages, with one exception involv-
ing the minus sign. A positive integer is a string of one or more digits, such as
0, 1234, or 11111111. A negative integer is formed by placing the unary minus
sign, which is the tilde (~), not a dash, in front of the digits, such as ~1234.
Integers may also be represented in heradecimal notation, where the char-
acters Ox or OX arc followed by a string of hexadecimal digits. Recall that the
hexadecimal digits are 0 through 9 and A through F, with the letters standing for
“digits” with values 10 through 15 (in decimal), respectively. The hexadecimal
digits that are letters may be written in either upper or lower case.2.1. EXPRESSIONS u
Example 2.2: Here are the responses of the ML system to some expressions
that are hexadecimal integers.
Ox1234:
val it = 4660 : int
Here, 1234 in hexadecimal, whose decimal value is
1x 172842 x 14443 x 1244 = 4660
is converted to decimal in ML/s response. Notice that ML gives you a deci-
mal representation, regardless of whether you write the integer in decimal or
hexadecimal.
“Oxah;
val it =
170 : int
Here, we notice that either a or A stands for the hexadecimal digit “10,” and
upper and lower case can be mixed. We also see that the negation symbol ~
may be used in hexadecimal integers. O
Reals
Reals are also represented conventionally, with the exception that minus signs
within reals are represented by ~. An ML constant of type real thus consists
of
1. An optional ~,
2. A string of one or more digits, and
3. One or both of the following elements:
(a) A decimal point and one or more digits.
(b) The letter E or e, an optional ~, and one or more digits.
As in other langudges, the value of a real number is determined by taking the
number that appears before the E or e and multiplying it by 10 raised to the
power that is the integer that follows.
Example 2.3: Here are some examples of real numbers:
i. 123.0 is the negative reai that happens tu have au i
2. 3E73 has value .003.
3. 3.14e12 has value 3.14 x 10".
a12 CHAPTER 2. GETTING STARTED IN ML
Booleans
There are two boolean values: true and false. ML is case-sensitive (unlike
some other languages such as Pascal or SQL that also use true and false as
boolean constants), so these constants must be written in lower case, never as
‘TRUE, False, or any other combination involving capitals.
Example 2.4: Here is what happens when we type a boolean value.
true;
val it = true : bool
Notice that the type of buvieaus is bel in tite ML respuuse. Go
Strings
Valucs of type otring arc double quoted character strings like "£00" or "ROI
Certain special characters are represented by sequences of characters, as in
the language C, where the backslash (\) serves as an escape character. The
principal ways to represent characters that cannot be typed on the keyboard,
or characters with a speciai meaning thai would confuse ihe inierpreiaiiun of
strings, are:
1. The two-character sequence \n is used for the “newline” character
2. \t is used for the tab character.
3. \\is used for the backslash character.
4.
. \" stands for the double-quote character, which otherwise would be in-
terpreted as the string ender.
5. A backslash followed by three decimal digits stands for the character
whose ASCII code is the number represented by those three digits, in
base 10. This convention allows us to type characters for which there is
no key on the keyboard. For example \007 is the “character” that rings
the bell on the console.
6. Those characters that are control characters can also be written by the
three character sequence consisting of a backslash, the caret or uparrow
symbol *, and a character whose ASCII code is in the range 64-95 (dec-
imal), i.e., the capital letters and the five characters [\]*_. The actual
character represented is determined by subtracting 64 from the ASCIL
code for the character typed. For example, \"G stands for G
and is the same bell-ringing charactcr that is represented by \007.
‘There are certain other escape sequences that are less commonly used or that
may not be supported by a given ML implementation. See the box on “Other
Character Codes.”2.1, EXPRESSIONS 13
Example 2.5; The string "A\tB\tC\n1\t2\t3\n" is printed as
A B Cc
1 2 3
Here we see uses of the tab sequence \t and the newline sequence \n.
If a string is too long to be written conveniently on a single line, we may
continue it over several lines. We make all but the last line end with a backslash,
and all but the first line begin with a backslash.
over three lines as follows:
"\\\" stands for the double-quote character, \
\which vthe:wise would be inlespreted \
\as the string ender."
Tn the first line, the first quote is not part of the string but indicates that
a string foliows. The first wo backsiashes represent the character \. The
third backslash and the quote represent the character " (the second character
of item 4 above). The backslash at the end of the first line indicates that the
string continues on the next line. Note that the space after the comma is shown
explicitly on the first line. If that space were missing, the represented string
In general,
© Any sequence of characters beginning and ending with the backslash and
containing between the backslashes only “whitespace” characters such as
blank, tab, and newline, is ignored in interpretation of strings.
another backslash to make a string break over several lines without the newlines
becoming part of the string
Characters
As in C, there is a distinction between a character string of length one and a
single character. ML provides a type char for characters. The representation
of character values in ML is somewhat unusual: he character # followed by a
character string of length one. That is, #' represents the character ©.
Example 2.7: Character a is represented by #"a", The tab character is rep-
resented by #"\t". O4 CHAPTER 2. GETTING STARTED IN ML
Other Character Codes
MI. also provides the following escape sequences: \a. \b. \v. \f. and \r
for the ASCII characters 7, 8, 11, 12, and 13, which are the bell-ringing
character, backspace, vertical tab, form feed, and carriage return, respec-
tively. In addition, the ML97 standard permits, but does not require, that
an implementation support an extended ASCTT character set of np to 16
its, such as the 16-bit character code used in the language Java. If an im-
plementation supports such an extended set (SML/NJ version 109.30 does
not), then one can represent such characters hy the sequence \u followed
2.1.2 Arithmetic Operators
The arithmetic operators of ML are similar to those of Pascal or C. There are:
1, The low-precedence “additive” operators: +, -.
2. The high-precedence “multiplicative” operators: *, / (division of reals),
div (division of integers, rounding down toward minus infinity), and mod
(the remainder of integer division).
3. The highest precedence unary minus operator, ~.
However, note the following.
¢ A unary minus sign is always denoted by a tilde (~), never by a dash:
Thus, we write ~3#4 and 3-4, but never 374 or -344.
© ML is case-sensitive, so the operators mod and div must be written in
lower case.
« Associativity and precedence is like Pascal or C; higher precedence op-
crators arc grouped with their operands first, and among operators of
equal precedence, grouping proceeds from the left. Grouping order can
be altered by parentheses in the usual manner.
Example 2.8: Here are some expressions and their responses from the ML
interpreter.
3.0 - 4.5 + 6.7;
val it = 5.2. real
Note that grouping of equal precedence operators is from the left. This expres-
sion is interpreted as (3.0 — 4.5) + 6.7, not 3.0 - (4.5 + 6.7), which has value
~8.22.1, EXPRESSIONS 15
43 div (8 mod 3) * 5;
val it = 105 : int
All throe operators div, mod, and * are of the same precedence, hut the
parentheses force us to use the mod first, then group from the left. Since mod
calls for the remainder when its left argument is divided by the right, the value
of 8 mod 3 is 2. We thus evaluate (43 div 2)*5, or 105. O
2.1.3 String Operators
We may not apply the arithmetic operators to string operands. There is, ho
The operator
ge and only to sti tor
stands for concatenation of strings; it has the precedence of an additive opera-
tor. When we concatenate two strings s, and s2, we get the string 5152. That
is, the resulting string is a copy of string s, followed by a copy of s2
Example 2.9; Here are sume examples of string concatenation.
"house" * "cat";
"Linoleum" >";
val it = “linoleum” : string
Notice in the second example that "" represents the empty string, the string
with no characters. When we concatenate the empty string with any other
string, cither on the left or right of the ~ operator, we get the other string as a
result. O
2.1.4 Comparison Operators
‘he six comparison operators that we find in Pascal are also part of the ML
repertoire. These are =, <, >, <=, >=, and <, representing, respectively, the
comparisons =, <, >, <, >, and #. They can be used to compare integers,
reals, characters, or strings, with one exception:
Reals may not. be compared using = or <>. The other four comparisons of
reals, such as <, are permitted, however.
In the case of characters, c, < cp means “lexicographically precedes”; that
is, the character code for ci is less than the character code for c2. Similarly, <=
means “cquals or lexicographically precedes,” and so on.
For strings, < is lexicographic order, just as < in Pascal or strcmp in C.
That is, if #; and s» are strings, then 8; < 52 if either
1. sy is a proper prefix of s2, or16 CHAPTER 2. GETTING STARTED IN ML
Why Can’t We Test Reals for Equality?
The policy that forbids testing r = s in ML, when r and s are real quanti-
ties, is motivated by the fact that all machines perform real arithmetic only
approximately, Thus, in some circumstances, wo real-valued expressions
that are theoretically equal could turn out, because of rounding error, to
be unequal in the machine. Tf yon definitely want to test whether r = s,
you can test both r < s and s 4;
ML does not evaluate the second condition (3 > 4), since the first being true
is sufficient to guarantee that the whole expression is true. Remember that the
result of a comparison is a boolean, so it makes sense to connect two comparisons
by a logical operation such as orelse.
In the following expression:
1<2 andalso 3>4;
val it = false : bool
it is necessary to evaluate both conditions. Had the first condition been false,
then there would have been no need to check the second, because the whole
expression could only be false. O
Recanse the symbol not. has such high precedence, we must. be careful to
group its argument properly. Here is an cxample.
Example 2.12: ‘he expression not 1<2 is grouped as (not 1)<2, which
makes no sense and is a type error in ML. We would have to write not (1<2),
although the simpler expression 1>=2 would do as well. ©
Incidentally, one might wonder why it matters whether or not the second
operand of a logical operation is evaluated, if the result of the entire expression
cannot depend on that operand. The reason is that in some special cases,
an ML expression can have a side-effect, which is an action whose effect does
not disappear after the expression is evaluated. The most common example of
a side-effect is when something inside an expression causes information to be18 CHAPTER 2. GETTING STARTED IN ML
printed or read. We have not yet seen any ML operator that has a side-effect,
and indeed it is in the ML style to avoid side-effects normally. However, side-
effects are possible, as we shall see in Section 4.1 and elsewhere. When they
nder which part of an
it is casential that we understand the conditions
expression will not be evaluated aud its side-effects consequently not performed.
Remember to use andalso and orelse, never and and or, for the logical
operations. There is no special meaning for or in ML, but and has another
meaning cntircly, having nothing to do with logical operations.
2.1.6 If-Then-Elsc Expressions
ML lets us usc conditional expressions of the form if E then F else G. We
compute the value of this expression by first evaluating expression E, which
must have a boolean value. If that value is true, then we evaluate expression
F (and never evaluate G); the value of F becomes the value of the entire if.
then-else expression. If the value of E is false, then we evaluate ouly G, which
becomes the value of the entire expression.
Example 2.12: Consider the following conditional expression:
if 4¢2 then 344 else 5+6;
val it = 7; int
We begin by evaluating the expression hetween the if and then. In this case,
valuate the sccond 7
we evaluate the sccond expres-
¢ expression 1 < 2 evaluates to true. Thus,
sion, 3+4. The result, 7, is the value of the entire expression. We do not
evaluate the expression 5 + 6, and if in its place there were an expression with
side-effects, those side-effects would not be executed.
Here are a few important points about conditional expressions.
© The conditional, or if-then-else operation, is one of the rare operations
that takes mare than twa operands. There is, hawever, a similar three.
operand (ternary) operator in C, using the characters ? and : in place
of then and else (nothing in place of if).
© Ifthen-else forms an expression. It is not a control-flow construct that
groups statements together, as we find in most languages
© There is no if -+- then construct in ML. Such an expression does not
have a value when the condition is false. This point emphasizes the dif-
ference between if-then-else as an expression form and as a control-flow
construct. There is no harm in having a control-flow construct if-then,
since it simply executes no statements if the condition is false. However,
an if-then expression might return no value at all and thus could not be
used inside larger expressions2.1. EXPRESSIONS 19
Case Sensitivity in ML
ML is case-sensitive, and operators whose names are composed of letters
are written with lower-case letters only. For example, we must be careful
to wrile not, andalso, if, mod, and so on.
‘There might appear to be an exception concerning letters used in the
expression of certain constants. For instance. we saw that either For e
may be used in real constants, and hexadecimal integers can be introduced
with either Ox or OX. In fact, the hexadecimal digits themselves can be
written in either upper or lower case. However, this phenomenon is not an
nply allows
forms of expression of certain constants.
2.1.7 Exercises for Section 2.1
Exercise 2.1.1: What is the response of ML to the following expressions?
* a) 14243
b) 5.0-4.2/1.4
*c) 11 div 2 mod 3
d) "too"™™bar"
* e) 3>4 orelse 5<6 andalso not (7<>8)
f) if 6<10 then 6.0 else 10.0
* 5) OXAB+123
h) Oxab<123
Exercise 2.1.2: The following ML “expressions” have errors in them. Explain
what is wrong with each
*a) 8/4
b) if 2¢3 then 4
*c) is2 and 573
d) 647 DIV 2
* 0) 4.43.5
f) 1.0€2.0 or 3>420 CHAPTER 2. GETTING STARTED IN ML
* g) rane
h) 123.
#11) 1.0 = 2.0
Exercise 2.1.3: Write a string that when printed creates the displayed text on
lines (3)~-(5) of Example 2.6. You may assume that the indentation of the lines
is made by a single tab character. Your string should be written over several
lines so there are no more than 80 characters appearing on any one line.
Exercise 2.1.4: Express:
*a) Eorelse F
b) E andaleo F
as if-then-else expressions. Incidentally, in ML, expressions formed with the
symbols orelse and andalso are actually shorthands for these if-then-else ex-
pressions.
2.2 Type Consistency
Having seen some of the important building blocks of expressions, we must now
learn what can go wrong when we use expressions built from these operators.
ML assigns a unique type to every expression. Operators also have partic-
ular types that they require their operands to have. Certain opcrators take
operands of one particular type only. Examples are /, which requires operands
of type real, div, which requires operands of type integer, and *, which requires
operands of type string. Others, like + or +, can take arguments of different
types, e.g., two integers or two reals. As we shall see shortly, it is not possible
to mix operands of integer and real types, as it is in C or most other languages.
of one type
to an “equivalent” value of another type. We shall also learn a number of these
“coercion” operators in this section.
Let us again remind the reader that there is a purpose to this seeming inflex-
ibility on the part of ML. It enables the ML compiler to type-check programs
completely. ‘hus, no program that can run at all can have a type error. The
advantage to the programmer is that what. could he a run-time bug in another
language's program is caught by the ML compiler.
2.2.1 Type Errors
As we saw in Example 2.1, when au operator is given operands of the proper
type, it responds with the result. However, when one or both operands are of
the wrong type, we get. an error message. The nature of error messages depends21
on the particular implementation. We shall use the responses from SML/NJ
version 109.30 in examples.
Example 2.14: The operator + can take either integer or real arguments
However, both operands must be the same type. When the types of the
operands are the same, ML attributes the same type to the result, for instance:
1+
val it — 3: int
1.0 + 2.0;
vai tt = 3.0 : real
On the other hand, when the operands are of mixed type, we get an error
message, as shown in Fig. 2.1. Let’s see what ML is telling us. The first line of
the response says that the operator expects operands of types othe: than what
it saw. The second line of the response tells us that the operator + expects
an “operand” whose type is a pair of integers. Although + can apply to either
integers or reals, the fact that the left argument 1 is an integer suggests that
integer addition was meant here.
142,
Error: operator and operand don’t agree [literal]
operator domain: int * int
operand: int * real
in expression:
+: overloaded((I : int),2.0)
Figure 2.1: A type error and its diagnostic message
The * operator in the expression int * int is not multiplication, but rather
an operator that applies to types and produces a product type, that is, the type
of a pair, uiple, or so on. In particular, int * int is the type of any pair
of integers, for example, of the pair (1,2). This response makes us aware of
a rather rigid view ML has of operators and operands. Strictly speaking, all
operators in ML are unary, that is, they take @ single argument. A binary
(two-argument) operator like + is perceived by ML as taking a single argument
that is a pair. Tn most situations there is no problem with viewing a binary
1
ces that
address in Section 5.5.
The third line of the response tells us what ML saw as the operand of the
operator, namely a pair whose first component (the left operand) is an integer
THowever, SML/NJ also returns a line and column number locating the point at which it
detected the error. We do not show this response since it is rarely meaningful out of context.22 CHAPTER 2. GETTING STARTED IN ML
but whose second component (the right operand) is a real. The final two lines
indicate the expression in which the error occurred. The only additional nuance
is that the operator and operand are shown in the conventional ML prefix form,
he pair (1,2
In the fifth line of Fig.. 2.1, we note the use of the term “overloaded” in
reference to +. An operator is overloaded if it can apply to two or more different
types, as + can. Notice that the fact + is defined for two integers or two reals
does not mean that it can be applied to one of cach. Similar comments apply
to overloaded operators like -, *, <, and the other comparison operators.
Tf we were to use an operand of the wrong type with an operator that is not
overloaded, we get an error message similar to Fig. 2.1, but without the word
“overloaded.” An example follows.
Example 2.15: The expression
eb
applies the nonoverloaded operator *, which concatenates strings, to a character
and a string. The error message would look like:
Error: operator and operand don't agree literal]
operator domain: string * string
operand: char * string
in expression:
a? Phe?)
a
Another type of error involves applying an operator, overloaded or not, to
operands at least one of which has a type inappropriate for the operator.
Example 2.16: The division operator / applies only to reals, as we learned
in Section 2.1.2. Here is what happens when this operator is misused.
1/2;
Error: overloaded variable not defined at type
symbol: /
type: int
Our first observation is that the error message talks about the symbol / and
its application to the type int, which we know is improper. However, what is
the “overloaded variable” in the first linc of the error message? ML thinks of /
as a variabie. As we shali see in Section 2.3.1, / is a iegitimate identifier for a
variable in ML, unlike most languages, where variable identifiers are restricted
to letters and digits plus perhaps a few other symbols. Although we said that
/ applies only to “reals,” an implementation of ML may support several kinds
of reals, such as single- and double-precision numbers. Thus, / might indeed
be defined for several different types.2.2, TYPE CONSISTENCY 23
Auother place where type mismatches may occur through carelessness is in
an if-then-else expression. The rules regarding types for this expression are:
© The expression foiiowing 11 must have booiean type.
© The expressions following then and else can be of any one type, but they
must be of the same type.
Example 2.17: Figure 2.2 shows what happens when the types of the expres-
sions following then and else disagree. Here, one is a character and one is a
siting.
if 1<2 then #"a" else "
Error: types of rules don't agree [tycon mismatch]
earlier rule(s): bool > char
this rule: bool + string
in rule:
false > "be”
cM
Figure 2.2: A mismatch between the then and else parts
about finding a (lc, "be")
about finding a string {ic., "be")
ing us somethi
when it expected a character to match the character #"a" that followed the
then. But what’s this about “rules”? The explanation lies in the fact that the if-
then-else expression is really a shorthand for a more general kind of expressiot
the case expression. We shall cover the case expression in Section 5.1.4.
For the moment, let. us just note that ML’s view of the if-then-else is that
it involves two “rules,” each of which takes a boolean value and produces a
value of some one type. ‘The first of these rules associates the boolean value
true with the character #"a". This rule expresses the principle that if the
condition is true, we use the value of the expression that follows the then.
The second rule associates the boolean value false with the value following,
the else, namely "bc" in this case. However, ML expects to find another
character-valued expression following else, which it will then associate with
false in the second rule. ML is unhappy that it has found a string-valued
expression, because ML will not tolerate groups of rules that produce values of
di ype. 2
© The word “tycon” in the first line of response in Fig. 2.2 is short for “type
constructor,” that is, a way of constructing types from simpler types.
Rules in the sense used in Fig. 2.2 are actually of a function type, mapping
booleans to some other type. We discuss function types in Section 3.1.1.24 CHAPTER 2. GETTING S1ARTED IN ML
Applying Functions, ML Style
ML offers us a diction for applying a function or operator to an argument
that may be unfamiliar to some: f x means “apply function f to argument
x,” just as f(t) does in C or most other languages. Since there is no
harm in putting parentheses around an argument, we have used the more
conventional style, writing rea1(1) instead of the preferred MT, style:
real 1. By adhering to the more familiar style, with parentheses, we hope
to focus attention on the more significant issues of ML, without adding
to the “newness” of the language. However, as the book progresses. we
I gradually shift to the ML style of omitting
all gradually shift to the MIL style of omitting
the argument of a function whenever appropriate.
2.2.2 Coercion Between Integers and Reals
Sometimes we have a reason to convert (coerce) a value of one type to an
“equivalent” value of another type. ‘hus ML provides certain built-in functions
that do the conversion for us. Perhaps the clearest case is when we want to
convert an integer to a real with the same value. The function real lets us do
just that.
Example 2.18: Applied to an integer, real produces the equivalent real value
real(4);
val it = 4.0: real
As another instance, we can fix Example 2.14, where we tried to add an
integer and a real, if we first apply real to the integer.
reai(i) + 2.0;
val it = 3.0: real
shows a correct version of this addition. Of course, there is no point in writing
real (1) instead of 1.0, but if we replaced 1 by an integer-valued variable, we
would have no choice but to convert the variable by applying the operator real
toit. O
When we try to convert a reai to an integer, it 1s not so clear which integer
we want, since the real may not equal any integer. MT. provides four coercion
operators: floor, ceil (cciling), round, and trun (truncate). Each produces
the integer with the same value when given a real that happens to be an in-
teger; for instance, 4.0 is converted to 4 by each of these four functions. In
general, given a real number r, floor produces the greatest integer that is no2.2. TYPE CONSISTENCY 25
larger than r, and ceil produces the smallest integer no less than r. Function
round produces the closest integer, with 0.5 raised to the next highest integer,
regardless of whether the real is positive or negative. The trunc function drops
Example 2.19: Figure 2.3 shows the effect of these four operators on positive
and negative real numbers. We include the special case of a hall-integer (3.5
and —3.5), noticing that rounding occurs upward. We also include typical cases
where the rounding is to the closest integer. Notice that floor and trune do
the same thing on positive numbers, but trunc agrees with ceil on negative
numbers. Remember that —3 is “larger” than —3.5 and —4 is “smaller.”
x_| floor(x) | cei1(x) | round | trunc(x)
3.5 3 4 4 3
3.5) 4 73 3 os
3 4 3 3
“3.6 “4 “3 “4 “3
Figure 2.3: Effect of real-to-integer coercion operators
2.2.3 Coercions Between Characters and Integers
We convert from characters to integers, just as in Pascal, using the ord function
(which, however, must be lower case in ML). The result of applying ord to a
character is the integer code for that character. Normally, the character will
be one of the ASCII characters, and ord will return the ASCII code for that
character.
Example 2.20:
ora (stam) »
val it = 97: int
ord(#"a") - ord(#"A");
val it = 92 : int
The latter example computes the difference between the ASCII codes for lower-
case a and capital A. This result is no coincidence. Every lower-case letter has
an ASCII code that is 32 more than its corresponding capital letter. O
Similarly, we can convert integers in the range 0 to 255 to characters. The
function chr performs this task as:
chr (97);
val it = "#fa” : char26 CHAPTER 2. GETTING STARTED IN ML
2.2.4 Coercions Between Strings and Characters
If we have a character, we can convert it to a string of length one with the
operator str. That is,
: string
However, conversion from strings to characters is not so straightforward.
Part of the problem is that we have to deal with strings that are not of length
one. ML provides several ways to make the conversion where it makes sense.
For example, we shalll see the explode operator in Section 2.4.5, which converts
a string to a
2.2.5 Exercises for Section 2.2
Exercise 2.2.1; Write cxpressions to make each of the following conversions.
* a) Convert 123.45 to the next lower integer.
b) Convert -123.45 to the next lower integer.
c) Convert 123.45 to the next higher integer.
* d) Convert ~123.45 to the next higher integer.
*e) Convert #"Y" to an integer.
£) Convert 120 to a character.
*1g) Convert #"N" to a real.
'h) Convert 97.0 to a character.
i) Convert #"2" to a string
Exercise 2.2.2: The following expressions contain type errors. What are the
a) ceil (4)
b) if true then 5+6 else 7.0
* c) chr(256)
d) chr(~1)
* 6) ord(3)
f) chr(#"a")
g) if 0 then 1 else 2
* h) ord("a")2.3.
RIABLES AND ENVIRONMENTS 27
2.3 Variables and Environments
In most languages, such as C or Pascal, computing takes place in an environment
consisting of a collection of “boxes,” usually called variables. Variables have
names and hold values. The name of a box is an identifier, which is a string of
characters (typically, letters and digits) that the language allows as the name of
a variable. There is usually a type associated with a variable, and the contents
of a “box” can he any value of the appropriate type. Paseal, C, and most other
Tanguages allow variables of types integer, real, and many other types.
‘At any given time the set of values stored in the variables’ boxes constitute
the store. Tn conventional lanenages, computation oe by sideffors,
A
about ML j
that it is impossible for the store to change, with a few exceptions, such as
arrays and references, that we shall introduce in Chapter 7. Rather, ML does
its computing by adding to the environment new value bindings, which are
assuciatious between identifiers aud values. The above brief overview of this
section is heady material, so let’s start again from the beginning.
2.3.1 Identifiers
Identifiers are character strings with certain restrictions. Most languages allow
identifiers that are letters followed by any number of letters and digits. ML
allows these too, along with many other strings that are not identifiers in most
other languages. In ML, identifiers fall into two classes: alphanumeric and
ables, as described below, which are alphanumeric identifiers beginning with an
apostrophe.
Alphanumeric Identifiers
The alphanumeric class of identifiers consists of strings formed by
1. An upper case ar lower case letter ar the character ? (called apostrophe
or “prime”), followed by
2. Zero or more additional characters from the set given in (1) plus the digits
and the character - (underscore).
However, identifiers beginning with the apostrophe ’ are type variables. They
can only refer to types and cannot be bound to ordinary values.
Example 2.21: The following are examples of alphanumeric identifiers:
abe
x29
Number_of_Hamburgers_Served
arbre28 CHAPTER 2. GETTING STARTED IN ML
The following is a legal alphanumeric identifier: ’a. However, it cannot be
bound to values like 3, 4.5, "six", or any of the values we normally think of
as the values of variables. Tt. can only be bound to a type. In fact, ML often
be
chooses the ide i
of any type. For instance, *a might in some contexts be given the type integer
as its “value.” Note that being bound to the type integer is quite different from
being bound to a particular integer like 3.0
Symbolic Identifiers
Of all the characters we can type with a conventional keyboard, there are only
ten that cannot appear as part of some sort of identifiers. These ten characters
are the three kinds of pairs of parentheses (round, square, and curly), double
quote, period, comma, and semicolon. ‘That is, the only characters that always
stand alone and cannot, he part. of an identifier are
CPCI LIF" 2
Of course the “white space” characters — blank, tab, and newline — also are
not part of identifiers. These do not have a meaning by themselves, but they
serve to separate the elements of a program.
The remaining 20 keyboard characters that cannot appear in alphanumeric
identificrs can be used to form symbolic identifiers. To be precise, the sct of
characters for symbolic identifiers is
tof eC >eP OER KONIG?
Many of these symbols by themselves are names of operators. For example, we
have seen the use of +, *, and several others. ML interprets the identifier + as a
special function that adds either two r i More precisely, ML
binds any other symbolic identifier that stands for an operator to the function
implementing that operator.
We are free in ML to form our own identifiers from strings of the 20 charac-
ters listed above. These identifiers might be used to name new operators that
we define, but they can also be used routinely to name integers, reals, and so
on.
Example 2.22: The following are legal symbolic identifiers: $$$, >>>=, and
!@#%. However, !@a is not a legal identifier because it mixes the characters !
and @ (which may only appear in symbolic identifiers) with the character a
(which can only be part of an alphanumeric identifier), 02.3. VARIABLES AND ENVIRONMENTS 29
Exercise Care Using Symbolic Identifiers
We advise against using symbolic identifiers to represent values of types
such as integers or strings. Besides looking strange, they often cause trou-
ble because they must be surrounded by white space to prevent them
from “attaching” to operators like + and forming unintended identifiers
that confise the MT. system and canse an error That is, although ¢¢ and
a are both legal identifiers, we must write << +a or << + a to add them.
Should we write < =
That is, we use the keyword val, the identifier for which we wish to create a
1 i 1 si d fs 1 ish to
associate with that identifier.
Example 2.23: Here is an example of how the identifier pi shown in Fig. 2.4
might have heen added to the environment.
val pi = 3.14159;
val pi = 3.14159 : real
Notice that in response to the val-declaration of variable pi, ML responds with
the value of pi rather than with the value of it, as was the case in all previous
examples. Otherwise, the response to a val-declaration is the same as the
response to an expression.
¢ In general, responses to val-declarations tell us the identifiers that have
been bound to values and what those values are.
‘e might next define an identifier radius as:
val radius = 4.0;
nal radius = 4.0 : real
3-The val-declaration is actually considerably more general, and in place of a single identifier
we can have arbitrary “patterns.” The matter is discussed further in Section 3.3.4,2.3. VARIABLES AND ENVIRONMENTS 31
Some Points About ML “Assignment”
Remember to use the keyword val to cause a value binding to oc-
cur. Assignment statements like x = y or x := y, familiar from
other languages, are errors in ML (with one exception, discussed in
Section 7.3.3).
© It is tempting to think of the equal-sign in a val-declaration as equiv-
alent to := in Pascal or = in C. However, these assignment operators
from other languages cause side-effects, namely the change in the
value stored in the place named on the left of the assignment opera-
tor. In ML, the val-declaration causes a newentry in the environment
to be created, associating what is to the left of the equal sign with
the value to the right of the equal-sign. Example 2.24 illustrates this,
point.
Now we have some variables, namely pi and radius, that we can use along
with constants to form expressions. For instance, we can write an expression
that is the familiar formula for the area of a circle:
pi * radius + radius;
val it = 50.2544 : real
Similarly, we could introduce another identifier, say area, and use a val-
declaration to give it a value.
val area = pi * radius * radius;
val area = 50.26544 : real
Note that in the above exampie, the expression suppiying the vaiue itself in-
volves variables and operators. In previous examples the “expression” was a
single constant.
2.3.4 A View of ML Programming
We now have a rudimentary view of what ML programs look like. They are
sequences of detinitions, such as the val-declaration that associates values with
identifiers (which are loosely the same as “program variables”). So far, we don’t
have any really interesting assignments to make; we can only bind values of a
basic type (e.g., real or string) to identifiers, and we can ask for the value of an
expression involving these identifiers and constants, by typing that expression.
In Chapter 3 we shall see how to give identifiers values that are functions and32 CHAPTER 2. GETTING STARTED IN ML
how to apply functions to values in order to compute new values. When these
functions are recursive, we shall find ourselves programming in a mode that
gives us all the power of other programming languages, yet has a distinctive
It is natural to think of a val-declaration as an assignment, and often we shall
not go wrong if we do so. However, there is a subtle but important difference
in the way ML views what happens in response to a val-declaration. The next
example ilusuates sume of that difference.
Example 2.24: Suppose that after issuing the val-declarations of Example
2.23 we “redefine” radius to be equal to 5.0 by:
val radius = 5.0;
val radius = 5.0: real
We might imagine that the entry in the environment for radius has had its
value changed from 4.0 to 5.0, However, the proper ML view is suggested in
Fig. 2.5. Below the top entry is the environment that existed before radius
was “assigned” 5.0. We do not show all the identifiers that ML defines for us
(c.g., +), but we concentrate on those we have defined: pi, radius, and area
area 50.26544
4.0
Existing before
3.14159 val radius = 5.0
Figure 2.5: The environment after redefining radius
The topmost entry in Fig. 2.5 is an addition to the environment that re-
sults from the new val-declaration. We have shown in the current. environment.
two entries that are named by the identifier radius, but only the most recent
(upper) one is visible at this time. If we are running ML in interactive mode
and simply entering a sequence of val-declarations, then the earlier declaration
of radius cannot again become accessible through the current environment.
When we discuss functions and their effect on the environment in Section 3.2.1,
we shall see that it is sometimes possible to access a “buried” value binding
such as the lower entry for radius, just as it is in conventional languages such
as C or Pascal. O2.3. VARIABLES AND ENVIRONMENTS 33
Identifiers Do Not Have Fixed Types
Note that when creating an entry with an old name, as we did in Exam-
ple 2.24, there is no restriction that the new value be of the same type as
the old value. We could just as well have defined radius to be an integer
in Example 2.24, for instance:
val radius =
However, we then could not have used this variable radius in expressions
like pi * radius * radius, because of the type mismatch.
2.3.5 Exercises for Section 2.3
Exercise 2.3.1: Tell whether each of the following character strings is (i) an
alphanumeric identifier suitable for ordinary (nontype) values, (ii) a symbolic
identifier, (iii) an identifier that must represent a type as a value, or (iv) not
an identifier of ML.
* a) The7Dwarves
b) 7Dwarves
* c) SevenDwarves ,The
d) ’SnowWhite’
* e) aseb
f) burrahi
*e) HL
h) 7123
Exercise 2.3.2: Show the effect on the environment of making the following
val a= "three";
val c = a*str(chr(floor(b)));34 CHAPTER 2. GETTING S1ARTED IN ML
2.4 Tuples and Lists
So far we have seen five types that ML values may have: integer, real, string,
character, and boolean. Most languages start with a similar collection of types
and build more complex types with a set of operators called type constructors,
which are dictions allowing us to define new types from simpler types. For
example, Pascal has, among other type constructors,
1. The record. . .end notation to build record types, whose fields may be of
any type,
2. The * operator to build a type whose values are pointers to valucs of some
simpier type, and
3. ‘The array constructor that defines an array type, given a type for elements
and an index type.
ML also has a number of ways to define new types, including datatype
constructions discussed in Section 6.2 that go beyond what we find in C, Pascal,
‘or most other languages. However, the simplest and possibly most important
ways of constructing types in MT. are notations for forming tuples, which are
similar to record types in Pascal or C, and for forming lists of elements of a
given type. In this section we shall learn these notations and also cover the
most important operations associated with these types.
2.4.1 Tuples
A tuple is formed by taking a list of two or more expressions of any types, sepa-
rating them by commas, and surrounding them by round parentheses. Thus, a
tuple looks something like a record, but the fields are named by their position
in the tuple rather than by declared field names.
Example 2.25: In the following val-declaration we assign to variable t a tuple
whose first component is the integer 4, whose second component is the real 5.0,
and whose third component is the string "six".
val t = (4, 5.0, "six'
val t = (4, 5.0, "siz”) : int * real * string
Let’s try to understand the ML response. It repeats the fact that the value
of t is the one we just gave it, which should be no surprise. However, it uses
terminology we have not seen before in an ML response, as it describes the
type of t. Recaii from Section 2.2.1 that the type int * real * string is
a product type. Its values are tuples that have three components. The first
component is an integer, the second is a real, and the third component is a
string. The operator * has a different meaning when applied to types than it
does when applied to integer or real values. Here * has nothing to do with
multiplication, but indicates tuple formation. O2.4, TUPLES AND LISTS 35
Tr
In general, a product type is formed from two or more types Ti, Tas ++
by putting *’s between them, as T; * Tz * +++ * Ty. Values of this type are
tuples with k components, the first of which is of type T;, the second of type
and 73 is string.
Example 2.26; Here are some further examples of tuples and their types.
1. (1,2,9,4) is of type int + int + int * int.
2. (1,(2,3.0)) is of type int * (int * real).
3. (4) is of type int. Strictly speaking, it is not a tuple, just a parenthesized
integer.
In (2) the tuple has two components, the first of which is an integer. ‘The second
component is itself a tuple with two components: an integer and a real. This
grouping is reflected in the type description.
‘The « operator applied to types is not an associative operator. For example,
int * (int * real) is not the same type as (int * int) * real. The latter
type describes tuples of two components, the first of which is a pair of integers
and the second of which is a single real. For example, ((1,2),3.0) is a value of
type (int * int) * real. Neither is the same as the type int * int * real,
which describes “flat” tuples like (1,2,3.0).
2.4.2 Accessing Components of Tuples
Given a tuple or a variable whose value is a tuple, we can get any particular
component, say the ith, by applying the function #i.
Example 2.27: In Example 2.25, identificr t was bound to the tuple value
G, 5.0, "six")
Now we can obtain its components. For example:
#1(t) 5
val it = 4: int
#3.(t);
val it = "six” : string
It is an error to apply a function like #4 that designates a component nuinber
higher than the number of components the tuple has. O
Tuples can be likened to records whose field names are the numbers 1,2, .
In truth, tuples as we have defined them are a special, simplified case of a more
general record-structure construct that does allow the programmer to specify
names for fields. However, the tuple is adequate and quite convenient for most
purposes. We defer the more general case of record structures to Section 7.1.36 CHAPTER 2. GETTING STARTED IN ML
2.4.3 Lists
ML provides a simple notation for lists whose elements are all of the same type.
We take a list of elements, separate them by commas, and surround them with
square brackets.
Example 2.28: The list of three integers 1, 2, 3 is represented in ML by
[4,2,31. The response of ML to an expression that is this constant value is
(12,35
val it = [1,23] : int list
The response to our list expression is informative. In addition to the usual
repetition of the value in the expression, it assigns the list the type int list,
which is ML’s way of saying “list of integers.”. O
© In general, “T List” is the type of a list of elements each of which is of
type T.
Bawmple 2.28: Iu ou second eaainple, the list has a single element that is of
type string.
[a];
val it = [’0”]
string list
‘The type attributed to the list expression is string list, or “list of strings.”
‘The fact that there is only one string in the list is irrelevant. The square brackets
differentiate the expression "a", which is of type string, from the expression
C'a"], which is a list of strings that happens to have only one string on the
list. 0
Example 2.30: Finally, here is an example where we erroneously try to mix
the types of elements of a list. We tried to write a list of three characters, but
we forgot the pound sign on the last one, so it became a string of length one,
instead of a character.
Cea", ep",
Error: operator and operand don’t agree [tycon mismatch]
operator domain: char * char list
operand: char * string list
in expression:
BPD” 2s Pe” ss nil
‘We shall explain the error message after we have learned some of the notation
of lists in Section 2.4.4. 02.4. TUPLES AND LISTS 37
2.4.4 List Notation and Operators
In this section we shall learn several operators that involve lists. These include
notation for the empty list, the head and tail of a list, “cons” or construction
of a list from a head and tail, and concatenation of lists.
‘The Empty List
Lhe empty lst, or list of no elements, is represented in ML by either the name
nil or by a pair of brackets, (]
Head and Tail
Any list besides the empty list is composed of a head, which is the first element,
and a tail, which is the list of all elements but the first, in the same order.
Example 2.31: If L ic the list [2,3,4], then the head of L ic 2, and the tail
of L is the list [3,4]. If M is the list [5], then the head of M is 5, and the
tail of M is the empty list, or nil. O
the list, respectively. The following restates Example 2.31 in a sequence of ML
expressions.
Example 2.32: Suppose we define lists Land M by the val-declarations
val L = [9,3,4]
val L = (2,8,4] : int list
val M = [5];
val M = [5] : int list
Now we can get the head and tail of each of these lists as follows.
hd(L) ;
val it =
: int
t1(L);
val it = [3,4] : int list
hd (MD:
val it = 5: int
tL);
val it = [] : int list38 CHAPTER 2. GETTING SVARTED IN ML
In the last of these expressions, ML describes the type of nil as int list. It
is possible for nil to be of any list type. In this case, since it is the tail of an
integer list, it is appropriate to assign it this type. O
Concatenation of Lists
While hd and t1 take apart lists, there are also two operators that construct
lists: concatenation and cons. We consider each in turn.
The concatenation operator for lists, which is @, takes two lists whose cle
ments are the same type and produces one list consisting of the elements of the
first list followed by the elements of the second. Thus
(1,2]0(3,4];
val it = [1,2,3,4] : int list
‘+ Do not interchange the * operator, which is concatenation of strings, with
the @ operator, which is concatenation of lists.
Cons
The cons operator, represented by a pair of colons (::), takes an element (the
head) and a list of elements of the same type as the head, and produces a
single list whose first element is the head and whose remaining elements are the
elements of the tail. Thus
val it = [2.0] : real list
The precedence of the :: and @ operators is below that of the additive
operators such as +, but above that of the comparison operators like <. Most
unusual is that these operators are right-assoctative, meaning that they group
from the right instead of the left as do most operators we have seen
Example 2.33: Especially important about right-associativity of these oper-
alors is the interpretation of a cascade of cons operators, like
1i:2i:3simal
This expression is grouped from the right, as 1::(2::(3::ni1)). Expression
3: 1a represents the list with head 3 and an empty tail, that 1s, Next,
2::(3] is the list whose head is 2 and whose tail is the list whose only element
is 3; this list is 2,3]. Similarly, the entire expression denotes the list [1,2,3].
Notice that when we have a sequence of cons operators, only the last operand
must be a list, such as nil in the example above. The other operands must be
elements. It would not make sense to group an expression like2.4, TUPLES AND LISTS 39
The Types of Heads and Tails
« Remember that the types of the head and tail are different. If the
type of the head is T, then the type of the tail is “list of 7,” or
T list in ML.
© Similarly, the cons operator :: takes a first argument that is of some
type T, and a second argument that is of type T list.
¢ On the other hand, the operator @ takes two arguments of type
T list for some type T.
1is2i 53s inal
from the left, as ((1::2)::3)::nil, because 1::2 is a type mismatch. That
is, when the cons operator sees the left operand 1, it expects that the type of
8 ype of 2 is int, ms
: to this pair of operands. U
possible to apply
Example 2.34: Let us reprise Example 2.30 and consider the meaning of the
error message that we saw there. We repeat the relevant part of Example 2.30
in Fig. 2.6.
tea", #”D", "oD;
1) Error: operator and operand don’t agree [tycon mismatch]
2) operator domain: char * char list
3) — operand: char * string list
4) in expression:
5) PPD es Pe? os nib
Figure 2.6: Error message from Example 2.30
ML parses lists from the back (i.e., the right end). It starts off assuming
a list is empty, ie. nil. When it sees the last element, "c", it “conses” that
clement with the list following, i.c., "c" :: nil to get a list of one clement,
t Evidentiy, the type of this jist is string 11st, since its one eiement is
a string.
Now, ML tries to attach the next-to-last clement, #"b", as the head of a
list whose tail is ("c"]. But there is a type mismatch in the resulting list
#"b" :: ["c"]. That is, since the head is a character, the expected domain
of the operator :: is char * char list, ie., a pair consisting of a character40 CHAPTER 2. GETTING STARTED IN ML
(the head) and a list of characters (the tail). That is what line (2) of the error
message is telling us. However, as line (3) states, the actual type of the pair to
which the :: operator was applied is char * string list, ie., ahead that isa
(4) and (8) of
confirm that the problem occurs in the expression #"b’
2.4.5 Converting Between Character Strings and Lists
In ML, strings and lists are different types. However, there is a great similarity
between a string and a list of characters, and it is possible to convert between
the two representations using the built-in functions explode and implode. The
first Uf Uhese iakes a siting aud couveris ii iv the list of charaviers appearing
in that string, in order.
Example 2.35: Here are two examples of the use of explode.
explode ("abcd"
val it = [#a”,£"b",#"c”,#"a") : char list
explode(""");
val it = [] : char list
Notice in the second example that "" is the empty string, which when exploded
elds an empty list of characters. O
The function implode takes a list whose elements are characters aud con-
catenates all the characters together to form a single string.
Example 2.36: Here are three cxamples of imploding lists.
implode ([#"a",#"b",#"c",#"a"]) ;
val it = "abed” : atring
implode(nil)
val it = "": string
implode (explode ("xyz"));
val it = "ayz” : string
The second example points out that we can implode the empty list and get the
empty string. The third illustrates that implode and explode are inverses of
one another, and the effect of explode followed by implode on any string is to
return the string itself. ©2.4, TUPLES AND LISTS a
The Type of the Empty List
Notice that in the second case of Example 2.35, ML deduced ‘that the
empty list was of type char list, even though there are no elements in
the list. ML knows it is an empty list of characters, because explode
always returns a list of characters.
Tn general, the type of the empty list is ’a list, i-e., a list. of elements
of any one type. Recall from Example 2.21 that identifiers beginning with
a quote mark denote types. Thus, ’a list is ML’s way of saying “any-
type list.”. However, when the empty list appears as a value, the ML
be able to di
sys eo type for
type that the elements would have if there were any elements. We shall
have more to say about the need to resolve types in Section 5.3.1.
A third operator, similar to implode, works on lists of strings instead of
lists of characters. If L is a list of strings, then concat (L) produces the string
37: Here is an example of concat applied to a list of strings.
concat(["ab", "cd", "e"]);
val it = "abede” : string
a
2.4.6 Introduction to the ML Type System
Every programming language has a type system, that is, a collection of types
for its values and variables and a way of expressing those types. We have not
seen nearly all of the ML type system yet. but it is useful to observe the way
types and their representations are constructed. The type system of ML is
constructed from a basis of elementary types by applying certain type construc~
tors recursively. A type constructor is an operator that builds new types from
simpler ones. Here is what we have seen so far of the ML type system
BASIS: We have seen the elementary types int, real, bool, char, and string.
INDUCTION: We have seen two type constructors:
1. The product-type constructor builds the types of tuples. If 7}, T»,..-;Tn
are types, then 7; * 72 * --- * Tn denotes the type of a tuple whose
ith component has type T;, for i = 1,2,...,n.
‘Recall that all hinary operators in ML are perceived as applying to a single pair, rather
than ta two arauments.42 CHAPTER 2. GETTING STARTED IN ML
2. The list-type constructor List builds list types from element types. If T
is a type, then T list is the type of lists each of whose elements is of
type T.
We may apply these type constructors in any order, as many times as we like,
to build new types of increasingly complex structure. In type expressions, list
is of higher precedence than *. Thus, we may need to use parentheses to group
operands properly.
Example 2.38: Here are some examples of constructed types and a typical
value for each.
1. Type expression int ist is a list of integers. It is the appropriate type
for values such as [1,2,3].
2. Type expression string * int list * int is the type for a tuple with
three components, whose types are respectively a string, a list of integers,
and a single integer. A typical value of this type is ("ab", [1,2,3], 4).
Note that in type expressions, list has higher precedence than *, so this
type expression is properly parsed atring # (int list) * int, rather
than (string + int) list * int.
3. Type expression (int * int) list list is the type of a list of lists of
pairs of integers. An appropriate value for this type is
LLC,2),(3,4)], L(6,6)J, nill
The list consists of three elements. The first element is the list consisting
of the pairs (1,2) and (3,4). The second element is the list with only one
clement: (5,6). The third clement is the empty list.
a
2.4.7 Exercises for Section 2.4
Exercise 2.4.1: What are the values of the following expressions?
* a) #2(3,4,5)
b) hd([3,4,5])
d) explode("foo")
* e) implode([#"z", #"0", #"0"])
f) "ec
tran nen)2.4. TUPLES AND LISTS 43
* 8) [rc","omJ@L"b", "0", "1"
h) concat(["e","a","t"])
What
possible, suggest an appropriate correction.
* a) #4(3,4,5)
b) nac{])
*tc) #1(1)
d) explode(["bar"])
*e) implode(#"a",#"b")
f) Cer"Je: [rane]
+g) cco)
h) 102
*i) concat((#"a",#"b"))
Exercise 2.4.3: Give the types of the following expressions.
* a) (1.5,("3", [4,5]))
b) ((1,2] nil, (3]]
*c) [(2,3.5), (4,5.5), (6,7.5)]
d) ((#"a",#"b"J, (nil, (1,2,3]])
*! Exercise 2.4.4: Are (1,2) and (1,2,3) the same type? Are [1,2] and
[1,2,3] the same type?
Exercise 2.4.5: Give examples of appropriate values for each of the following
type expressions. Do not use the empty list as the value for any list component.
*a) int list list list
b) (int * char) list
*c) string list * (int « (real * string)) * int
d) ((int * int) * (bool list) * real) * (real * string)
*e) (bool * int) * char.
!f) real * int list list list list.
! Exercise 2.4.6: Using two of the operators we have learned in this section, it
is possible to convert a string of length one into the character of that string.
Show how to accomplish this transformation.Chapter 3
Defining Functions
Now we know everything there is to know about ML, except how to program!
In this chapter we shall learn about defining and using functions. Essentially
all programming in ML is conducted by the definition of functions and the
application of these functions to arguments. As we shall see, ML uses functions
in places where more traditional languages use iteration (e.g., while-loops).
3.1 It’s Easy; It’s fun
The keyword tun introduces function definitions. In this section we shaii see
the simplest form of function definitions, which are essentially single expressions
that are evaluated for the arguments of the function whenever the function is
called. Later sections discuss the more common forms of function definition,
involving the matching of arguments to patterns, and the use of temporary defi-
nitions in functions. We defer to Chapter 5 some of the more advanced concepts
regarding ML functions, such as polymorphic functions (those that can take ar-
guments of different types), higher-order functions (those that take functions
as arguments or produce functions as results), and Currying of higher-order
functions (writing a function so new functions may be created by instantiating
one of its arguments).
The simplest form of function declaration is
fun () = char
There are a number of observations we should make about the function
upper. First, it has one parameter c. The value of upper is computed by the
expression chr(ord(c)-32). We discuss the ML response to the definition ot
upper in Section 3.1.1
We may use function upper to convert lower-case letters, just as we would
in most languages, by applying upper to the desired letter. For instance, we
can convert #"a" to #"A" by:
upper (#"'a") ;
val it = A” : char
ML responds to expression upper (#"a") as it would to any expression. by
assigning its value to it and telling the value. 0
3.1.1 Function Types
Notice from Example 3.1 how ML represents function types. The response to
the definition of function upper was
val upper = fn : char - char
In general, when a function is defined, ML does not respond with the value
of that function, which is hard to express other than by repeating the defini-
tion of the function. Rather, it responds with the type of the function. The
specification of the function type has the form:
fn : ->
fs
'There is actually a function toUpper that is available in a library of functions that ML. calls
the Char “structure.” We shall cover struct Section 8.2 and the particular structure
Char in Section 9.4.4. Function toUpper also has an inverse function, toLower, which converts
upper-case letters to their corresponding lower-case letters, Moreover, each of the functions
topper and toLover leave intact those characters that are not lower- or upper-case letters,
respectively, which our simple function upper does not do.3.1, IT’S EASY; IT’S FUN 47
Function Parameters and Arguments
In this book we call the variables to which a function is applied in its
definition the parameters, while the expressions to which the function is
applied in a function call are arguments. In other literature, one sometimes
sees the terms “formal parameters” and “actual parameters” where we use
“parameters” and “arguments.”
1. The keyword fn.
2. A colon.
3. The type of the parameter(s), called the domain type for the function.
This type is chaz in Daample 3.1. ML regards cach function as having
one parameter, but the type of this parameter can be a product type. So
in practice there can be any number of parameters for a function.
o
The type of the result of the function, that is, the range type for the
function. In Example 3.1, the range type is also char, but it is common
for the domain and range types to differ. ML views each function as
returning a single valuc, but since this value may be a tuple, in effect a
function can return severai items.
The operator -> is another way to construct types, just like * and the word
List. If T, and 7; are types, then T; ~> Tz is the type of functions with domain
type T; and range type Tp, that is, functions which take an argument of type
T; and return a result of type T».
Operator -> is right-associative, so T + T ++ Ty is interpreted as
Ty > (hh > Ts)
and is the type of a frnetion whose parameter is of type T, and whose result.
is itself a function; that function has domain type Tz and range type Ts. The
notion of a function producing a function as a value may seem strange, but
these “higher order functions” are an integral part of ML programming that we
shall examine starting in Section 5.4.
3.1.2 Declaring Function Types
It might surprise the reader that we never had to declare the type of the param-
eter c in the function upper of Example 3.1 or the type of the value returned
by this function. ML deduced that these types are both char because of what
it knows about the functions ord and chr. In general ML does not require48 CHAPTER 3. DEFINING FUNCTIONS
Don’t Confuse fun With fn
The response in Example 3.1 uses the keyword fn, which should not be
confused with fun, even though both are short for “function.” We use
fun to introduce a declaration of a particular identifier to be a certain
function, while fn is used in ML to introduce a value that has a function
type
declarations for types. although you are free to declare the type if you wish.
We shall have more to say about how ML deduces types in Section 3.2.4, and
there we shall get a better idea of when we can rely upon ML to deduce types
for us.
The most common situation in which we have to declare a type is when ML
would use the default rule for an arithmetic or comparison operator to deduce
that certain variables were of integer type, and yet we want these variables to
be of some other type on which the operator can be used. If we need to, we can
follow any variable or expression by a colon and a type. The
that variable or expression to have that type. Recall that the colon symbol is
also used in ML responses to connect. valies with their types.
Example 3.2: Our next example is a function that squares reals.
fun square(x:real) = x#x;
val square = fn : real -> real
‘The function square has one parameter, x. By following parameter x with
a colon and the type real, we declare to ML that the parameter of function
square is of type real. ML then infers that the expression x#x represents real
multiplication, and therefore the value returned by square is of type real. O
It is necessary to indicate that x is real somewhere. Otherwise, ML will use
the default type, integer, for x, resulting in a function that can square integers
but not reals:
fun square(x) = x*x;
val square = fn: int + int
We couid have attached the :real to any or ail of the three occurrences of
x in the definition of square in Example 3.2, For example
fun square(x) = (x:real)*x
is a possibility. However:3.1. IT’S EASY; IT’S FUN 49
© We must be careful to parenthesize the arguments of the colon operator —
for example, (x:real) — because the colon has lower precedence than
the arithmetic or comparison operators.
Example 3.3: Some care must be exercised in how we specify the types of
variables in a function definition. Here is an example of a surprising error that
can occur if we do not group a variable with its type properly.
fun square(x) = xireal + x;
Error: unbound type constructor: «
Here, because + has higher precedence ian :, ME has isied iv “uultipiy”
real by the third of the three x’s before applying the operator :. ‘That is not
as strange as it. seems. ML knows real is a type, and * applied to types forms
a product type. That is, ML is trying to form a type consisting of pairs whose
first component is of type real and whose second component is of type x. But
it doesn’t know about any type named x, so it complains. The solution, which
we used following Example 3.2, is to parenthesize the x:real so ML will group
its operators as we intend. ©
3.1.3. Function Application
As an example of the use of the square function, suppose we have defined the
variables pi and radius to have values 3.14159 and 4.0, as in Example 2.23.
pi*square(radius) ;
val it = 50.26544 : real
In this example, function application looks just like it does in Pascal or most:
languages; a function is applied to a list of arguments, with parentheses around
the argument list. However, as we discussed in the box on “Applying Functions,
ML Style” in Section 2.2.3, formally, the ML syntax for function application is
simply a pair of expressions standing next to one another, with no intervening
punctuation. That is, F E requires the expression F to be evaluated and
interpreted as a function. ‘Then, expression # is evaluated and function F is
applied to the value of F.
Example 3.4; We could have computed the area of a circle by
pi * square radius;
val it = 50.26544 + real
Function application has higher precedence than any of the arithmetic opera-
tors, so the above expression first applies function square to argument radius,
and the result is multiplied by pi. O50 CHAPTER 3. DEFINING FUNCTIONS
In principle, it doesn’t matter whether or nol we pul parentheses around @
simple argument (i.e., an argument without operators); that is, £ x and £(x)
are treated the same hy MI.. However, we advise using the parentheses. Not
only do par pp!
but sometimes they prevent an error such as failure to put parentheses around
an operand and its type, to which the operand is connected by the : symbol.
Example 3.5: To undersenre the point that fimetion application has higher
precedence than the common operators, consider the sequence of statements
below, ending in an application of the function square from Example 3.2.
val ¥ = 3.0;
val y = 4.05
square x+y;
val it = 13.0 : real
We see from the value produced that ML has grouped the function application
(square x)+y; that is, function square is applied to x before the addition with
y takes place. If we want to square the sum of x and y. then we are required
square (x+y) ;
val it = 49.0 : real
as we would in most other languages. 0
3.1.4 Functions With More Than One Parameter
We can define a function that has any number of parameters. Normally, we put
parentheses around the list of parameters or arguments, both in the function
definition and use. The effect is to combine the list of arguments into a tuple,
which formally is a single argument but which we may Ueat as if there were
several arguments. It is also possible to write multiparameter functions without
parentheses; see Section 5.5.
Example 3.6: Figure 3.1 is another example of a function; it produces the
largest of three real numbers. It begins by comparing parameters a and b in
line (2). Tf a is larger, it returns as a result the larger of a and c at lines (3)
and (4). If b is larger, then in lines (6) and (7) it returns the larger of b and c.
a
Example 3.6 brings up a number of important points about ML types.
* Notice that in Fig. 3.1 ML deduces that b and ¢ are reals, even though
only a was declared. One way to make this deduction is to use the fact
that the if --- then --- else operator must have the same type in both
branches. We shall discuss type deduction further in Section 3.2.4.3.1, IT’S EASY; IT'S FUN 51
(1) fun max3(a:real,b,c) = (* maximum of three reals *)
(2) if a>b then
(3) if a>c then a
(4) else c
(8) else
(6) if bec then b
mM else c;
val maz? = fn ; real * real * real + real
Figure 3.1: Function computing the maximum of its three arguments
Comments
Now that we can write programs of more than one line, we shall have
reason to comment our code. The proper way to do so is shown in line
(1) of Fig. 3.1. The pair of characters (* introduce a comment, which
continues. even across lines, until the matching sequence of two characters
*) is encountered. This convention is similar to most implementations
of Pascal, but in ML it is possible to uest pairs of (#...4), just like
parentheses are nested.
Also notice that the type of max3 is a function that takes a triple of real
numbers as its argument and produces a real. That type is shown in the
response as real + real + real -> real.
© In type expressions + takes precedence over -. Thus in the type expres-
sion above, the domain type is real * real * real, and the range type
is real
* If we did not declare the type of any variable in function max3, then ML
would assume the variables compared by > have the type integer, the
default type for operator >.
One advantage of the ML view that functions have only one parameter
is that a variable whose value is of the appropriate product type can
be defined and used as the argument of a multiparameter function. For’
example,
val t = (1.0,2.0,3.0)5
max3(t) ;
is correct ML and produces the value 3.0.52 CHAPTER 3. DEFINING FUNCTIONS
3.1.5 Functions that Reference External Variables
The functions illustrated so far use only their parameters in computing their
result. Sometimes, we wish to write a function that uses previously defined
variables in its body. As in most other languages, the use of a variable x in
the definition of a function f “freezes” x as far as the function f is concerned.
That is, subsequent redefinitions of «c will not affect the function f. A simple
example will illustrate the rule.
Example 3.7: Consider the following sequence of steps:
1) val x = 3;
2) fun addx(a) = atx;
3) val x = 10;
4) addx(2);
int
val it =
A picture of the changes to the environment is shown in Fig. 3.2. At line (1)
we create a variable x and give it the value 3. When at line (2) the function
a id by an as in Fig. 2.9 that
the definition of addx refers to this value binding for z. Remember that, as
we suggested in Section 2.3.4, the value binding for «, after being added to the
environment at line (1), never changes. Thus, we can be sure that the definition
of addx will always use 3 as the value of «.?
Now, in line (3) we create a new variable, also named x. As we see in Fig. 3.2,
the new binding for x goes above the old binding for x and the definition of
addx. However, the definition of addx docs not change; it continues to refer to.
the value of x that pertained when the definition of addx was made. Thus, we
see that in line (4), addx(2) results in the value 5, not 12, because the value of
x in the definition of addx is still 3.
3.1.6 Exercises for Seciion 3.1
Exercise 3.1.1: Write functions to compute the following:
* a) The cube of a real number 2.
b) The smallest of the three components of a tuple of type int * int * int.
*c) The third element of a list. The function need not behave properly if
given an argument that is a list of length 2 or less.
?You may therefore wonder why we would want to write addx as we did. Indeed,
fun addx(a) = a¢3 would be a simpler way to write the same function. ‘There are some
good reasons to write functions that refer to external variahles, however For example, in
Section 8.1.2, we consider a collection of finetions that all use the same external variable, If
we change this variable, all the functions change in a coordinated way.3.1, IT'S EASY; IT’S FUN 53
x 10 Added at line (3)
definition aa
ad: Added at li
addx of addx 5 at line (2)
x 3 ? Added at line (1)
Previous
environment,
Figure 3.2: Changes to the environment in Example 3.7
a
yo
)
* e) The third character of a character string. Your function need not behave
well on strings of length less than 3. Hint: Use explode and your function
from Exercise 3.1.1(c).
{a2, a3, ++ +54, aa)-
! Exercise 3.1.2: Write functions to do the following.
* a) Given three integers, produce a pair consisting of the smallest and largest
b) Given three integers, produce a list of the three in sorted order.
©) Round a real number io ihe nearest tenth,
1d) Given a list, return that list with its second element deleted. Your function
need not behave well on lists of length shorter than 2.
Exercise 8.1.8: Suppose we execute the following sequence of definitions:
val a = 2:
fun f(b)
val b= 3
fun g(a) = a+b;
ab:
Give the value of the following expressions:
* a) £(4)54 CHAPTER 3. DEFINING FUNCTIONS
‘When and Where Function Definitions Occur
The behavior of ML regarding the value of variables used in a function
definition is essentially the same as the policy followed by C or Pascal, or
iost other languages. For example, in C a function’s definition can refer
to static variables defined prior to the function definition in whatever file
the function definition appears. That is, the variables a function definition
can use depends on where the definition appears in a C program.
It might appear that which variables are usable by a function defini-
tion in ML depends on when the function is defined. That is, the function
ly le — 5
is caused by the fact that we have been thinking of ML progranuning as
done in interactive mode, where steps are entered one at a time. If we think
of what we type in interactive mode as a single file of program elements,
then we see that ML follows the same rule as © you may use variables
that are located above the function definition in the file. ‘Lhis rule applies
exactly if we write ML programs in files and execute them using use, as
we discussed in Scction 1.2. There are, however, two differences between
ML and C in this regard:
1. In C, the value of a variable can change, thus changing what the
function docs; in ML the value cannot change.
2. In ML, it is possible to make several declarations for the same iden-
tifier, external to any function. In C, that would be considered an
illegal redefinition.
*b) £(4) 4b.
a
d) g(S)+a.
*e) £(g(6)).
f) g(£(7))
3.2 Recursive Functions
It is possible, and indeed frequently necessary, for ML functions to be recursive,
that is, defined in terms of themselves, either directly or indirectly. In fact, re-
cursive functions in ML substitute for most of the iterations such as while-loops
or for-loops than one finds in C, Pascal, and most other languages. Looping3.2, RECURSIVE FUNCTIONS 55
statements, while present in ML (See Section 7.3.4), are awkward and generally
discouraged.
When writing recursive functions, we must be careful that if a recursive
the programmer, sinaller than its own argument. For example, if the argument
is an integer i, we could safely call the function with argument i—1 or any
integer smaller than i. If the argument. is a list I., we could call the function on
the tail of the list or any shorter list.
Normally, a recursive function consists of
1. A basis, where for sufficiently small arguments we compute the result
without making any recursive call, and
2. An inductive step, where for arguments not handled by the basis, we call
the function recursively, one or more times, with smaller arguments.
Tn this section we shall learn abuut writing simple recursivus. We then in-
troduce two extensions: nonlinear recursion, where the recursive function calls
itself several times, and mutual recursion, where several functions are defined
recursively in terms of each other. We begin with a simple example of a recur
sion.
Example 3.8: Let us write a function reverse(L) that produces the reverse
of the list L.? For example, reverse([1,2,3]) produces the list [3,2,1]
BASIS: The basis is the empty list; the reverse of the empty list is the empty
INDUCTION: For the inductive step, suppose L has at least one element. Let
the first or head element of L be h, and let the tail or remaining elements of L
be the list T. Then we can construct the reverse of list L by reversing J’ and
following it by the element h.
For instance, if L is [1,2,3], then h = 1, T is [2,3], the reverse of T is
[3,2], and the reverse of T concatenated with the list containing only h is
{3,2]¢@(1], or (3,2,1].
(1) fun reverse(L) =
(2) if L = nil then nil
@) else reverse(ti(L)) @ [hd(L)];
nal reverse = fr: ’a list + ‘a list
Figure 3.3: A recursive function to reverse a list
In Fig. 3.3 we see the ML definition of reverse that follows the basis and
inductive step described above. Lines (2) and (3) are the expression that forms
IML actually has a built-in function rev that performs this operation.56 CHAPTER 3. DEFINING FUNCTIONS
When Does a Function Need to Know Its Type?
Given our discussion in Section 3.1.2, it might surprise you to find that
in Example 3.8 it was not necessary for the particular type of elements
to be deduced by the ML compiler. The difference between Example 3.8
and previous examples of functions that work on parameters of only one
type, is that. some functions use an overloaded operator such as + or < that
require us to tell ML what type its operands have (or to use the default
type for the operator). In Example 3.8, there is no overloaded operator,
and thus, we were able to avoid specifying the types of elements of the
Section 5.3
wl is to
know the type of its operands and when it can be “polymorphic,” working
on values of various types.
the body of the function definition. In line (2) we handle the basis case: the
reverse(t1(L)) takes the tail of the given list and reverses it, recursively. We
then concatenate this new list with the head element, which is obtained by
subexpression hd(L)
In order to concatenate the reversed tail with the head element, we must
place square brackets around the head element, as [hd(L)]. Remember
that the concatenation operator @ requires two lists as its arguments. If
we were to omit the square brackets, we would be concatenating a list
and an element, leading to a type mismatch.
The response to the definition of reverse in Fig. 3.3 illustrates an interesting
point. Unlike our previous examples of functions, ML cannot tell exactly what
the type of argument and result is. It can only deduce that these types are both
lists of elements of the same type. It calls the element type ’a, and it calls the
argument and result types ’a list.4 The type of reverse is then a function
from ?a lists Uo ?a lists.
3.2.1 Function Execution
Whenever a function is called, its arguments are evaluated, and an addition to
the environment is created that associates the resulting values with the param-
eters of the function. This style of argument passing is known as call-by-value
‘Recall that identifiers beginning with a quote are variables denoting types. Actually, the
type variable used by MI. in this example is ?%a (ie, two quotes hefore the a). There is a
subtle distinction hetween 'a and 1a, which we shall discuss in Section 5.3.4. Refore then,
we shall use only *a, "b and so on as variables denoting, types.a
g
3.2. RECURSIVE FUNCTI
VS
Ivis the same as the manner by which arguments are passed to functions and
procedures in C, and the manner in which non-var parameters are handled in
Pascal.
When the functi
entries that bind the parameters of the function to their associated values. If
the function is recursive, new additions are built on top of the old ones for
each recursive call. Each addition binds the parameters of the function to the
aigument values. These bindings intercept any reference to the parameters,
thus distinguishing themselves from the entries with the same identifiers in
levels below. When a function completes and returns its value, its addition to
the environment gocs away, but the returned value is available for use in the
expression being evaluated.
ic executed, we place on top of the old oi
Suppose we are in an environment that has the definition of
se from Example 3.8. If we call
Example 3.
the function re’
reverse([1,2,3])
then we add to the environment an entry for parameter L and its value. We
show this first step above the line in Fig. 3.4.
‘Added in call to
. {1,2,3] reverse((1,2,31)
reverse | definition of Environment
a before call
Figure 3.4: Environment after the initial call to reverse
With this value of L as argument, the condition of line (2) in Fig. 3.3 is
false; that is, L is not nil. Thus, we must evaluate the expression on linc (3),
which requires us to evaluate reverse(t1(L)) or reverse([2,3]). Thus we
set up another call to reverse, adding to the environment a new binding for L
that associates L with the value [2,3].
In a similar manner, the new cali to reverse causes us to make auviher ¢
with L bound to (31, and an addition to the environment is set up with this
binding. Again a recursive call to reverse is necessary, and in the fourth call L
is bound to nil. The additions to the environment for all four calls are stacked
one above the other as suggested in Fig. 3.5. At this point, the identifier L
refers to the top binding, with value nil.58 CHAPTER 3. DEFINING FUNCTIONS
Current.
View
/(
J . Added in call to
r — reverse(nil)
Added in call to
x = reverse([3])
Added in call ta
r 2,3] reverse([2,3])
Added in call to
' (9,31 severse([1,2,3])
reverse | Sou Environment
before call
Figure 3.5: Additions to the environment. when four calls to reverse are made
Now when we evaluate the body of reverse, the test of line (2) is satisfied,
because L has the value nil. The value nil is returned and used in place of
(1(L)) by reveree([3]) — to p:
its own answer on line (3). After the return, te lop entry fur L in Fig. 3.5
disappears, exposing the appropriate value of L, namely [3]. Since hd([3])
is 3, the result produced by reverse([3]) is the empty list concatenated with
[3], or just [3].
Now, the addition to the environment for reverse([3]) goes away, and its
result is used by the call below it: reverse({2,3]). That, in turn, produces
{5,2} as a result aid its addition tv the euvirumment goes away, leaving the
environment that was originally shown in Fig. 3.4. However, the corresponding
call, reverse([1,2,3]), now receives the value [3,2], returned from above, to
use in place of reverse(t1(L)) in line (3). Thus the original call to reverse
is able to produce its value, [3,2,1]. At this point, ali bindings for L have
disappeared. O3.2, RECURSIVE FUNCTIONS 59
3.2.2 Nonlinear Recursion
‘The form of recursion illustrated in Examples 3.8 and 3.9 is relatively simple.
Hach call either results in one recursive call with a smaller argument, or we
reach the basis case and there is no need for a recursion. Now we shall examine
a function where the recursion involves more than one recursive call.
The function combinations of m Ukings out of u or “n chose m,” usually
written ("), is the number of ways we can pick a set of m things out of n
distinct things. For example, two aces ont. of the four aces in a card deck can
be picked in six possible ways. That is, we can pick any of the four aces first
and any of the three remaining aces second. That looks like 12 ways, but in
fact we have picked each set in two different orders. For example. the aces of
spades and hearts could be picked spade-then-heart or heart-then-spade.
In general, (") = n!/((n — m)!m!), where 2! (x factorial) is the product of
all the integers from 1 up to x. For instance,
(8) = a/(212) = 4 8x2 1/(2x 1x21) =6
Intuitively n!/(n — m)!, which equals n x (n — 1) x +++ x (n= m+ 1), is the
number of ways we can select among n things for the first choice. then among
the m — 1 remaining things for the second choice, and so on for m choices. We
must divide this number by m! because each set of m elements will have been
selected in m! different orders.
There is also a natural recursive way to define (”). Here are the basis and
induction rules.
BASIS: There are two parts to the basis. If m = 0, then the number of ways to
pick 0 things out of n is 1 — don’t pick anything. Thus, (7) = 1 for any n > 0.
Also, if m = n, then there is one way to pick all n things out of n — pick them
all. Thus, (") =1 for all n > 0.
INDUCTION: If 0 < m n, so this basis and induction
entirely define the function.
Example 3.10: We can write a function comb(n,m) that computes (”.). The
code appears in Fig. 3.6. Line (2) handles the basis case, and line (3) implements
the inductive step. Note that the program will not behave well if the assumption
about n and m in the comment of line (1) is violated. We really should test for
violations, and there is an important. mechanism, the “exception,” that allows6 CHAPTER 3. DEFINING FUNCTIONS
us to do so and still adhere to the principle that functions return a value of one
particular type invariably. We discuss exceptions in Section 5.2. O)
(1) fun comb(n,m) = (« assumes 0 <= m <= n *)
(2) if m=0 orelse m=n then 1
(3) else comb(n-1,m) + comb(n-1,m-1);
val comb = fr: int * int > int
Figure 3.6: Function to compute n choose m
The sequence of recursive calls initiated by a single use of function comb is
rather complex. For example, in the expression
comb(4,2);
val it = 6: int
the initial call first calls comb(3,2) and later calls comb(3,1). Iowever, be-
tore the latter call, comb(3,2) calls comb(2,2) and comb(z,1), and so on.
Figure 3.7 shows the structure of the calls as time progresses from left. to right.
Figure 3.7: Structure of recursive calls for the comb function
3.2.3 Mutual Recursion
Occasionally, one needs to write two or more functions that are mutually re-
cursive, meaning that each calls at least one other function in the group. Most
languages, such as Pascal or C, put some obstacles in the way of writing such
functions, but ML has a straightforward mechanism. We shall give an example
of mutually recursive functions, first showing the problem that arises if we are
not careful. ‘hen, we shall show how ML lets us handle the problem.
Example 3.11: Suppose we want to write a function that takes a list L as
argument and produces a list consisting of alternate elements of L. There are
two natural versions of this function. One, which we call take(L), takes the
first element of L and alternate elements after that (i.e.. the first. third. fifth3.2. RECURSIVE FUNCTIONS GL
and so on). The other, which we call skip (L), skips the first element and takes
alternate elements after that (i.e., the second, fourth, sixth, and so on). It is
convenient to define these two functions in terms of each other.
BASIS: If L is empty, both functions return the empty list.
INDUCTION: If L is not empty, take returns the head element of L followed
by the result of applying skip to the tail of L. On the other hand, skip returns
the result of applying take to the tail of L.
Figure 3.8 shows a failed attempt to define the functions take and skip.
AU the third line of take, we assemble the result using the cons operator; the
head is the head of L, and the tail is the result of applying skip to the tail of
L. The problem is that at the third line, the function skip is not defined, even
though we intend to define skip immediately thereafter. Thus, ML responds
with an error message. Defining skip first would cause a similar error because
take is used in the third line of skip. O
fun take(L) =
at L = nil then nil
else hd(L)::skip(t1(L));
Error : unbound variable or constructor: skip
fon akip(t.) =
if L = nil then nil
else take(tl(L));
Figure 3.8: Erroneous attempt to define mutually recursive functions
We can get ML to wait until it has seen both functions take and skip before
trying to interpret. variables, if we use the keyword and between the function
definitions. The general form for defining n mutually recursive functions is
shown in Fig. 3.9. There we see the n definitions connected by and’s. There is
one use of fun at the beginning and one use of the semicolon, at the end.
‘* Do not confuse and, which is used to indicate mutual recursions, with
andalso, which is the logical AND operator in ML.
© It is not necessary to use the and construct if there is no mutual recur-
sion. If we define functions fi, fo,-... fn. and, for each i, in the detimtion
of f; we use only functions that appear carlier on the list — that is,
Sis fa--+ fi-1 — then there is no mutual recursion.
Example 3.12: ‘The correct definition of the functions take and skip from
Example 3.11 is shown in Fig. 3.10. Notice that the response from ML does not62 CHAPTER 3. DEFINING FUNCTIONS
fun
and
and
and
;
Figure 3.9: Form of a mutually recursive function definition
come until after both functions have been seen. Both are identified as functions
from lists to lists. The elements of the input and output lists of both functions
must be of one type ’a, but ML cannot identity the type.
fun
take(L) =
if L = nil then nil
else hd(L)::skip(t1(L))
and
skip(L) =
if L = nil then
else take(t1(L));
nal take = fn : ’a list + 'a list
val skip = fn: ’a list + a list
Figure 3.10: Correct definition of mutually recursive functions
Here are two exampies of the use of these functions.
vake([1,2,3,4,51);
val it = [1,3,5] : int list
skip([#"a"#"b" ,#"c",#"d",#%e"]) 5
val it = (#"b”,#"d"] : char list
When we use the functions on particular lists, ML can figure out the type of
list elements from the argument. Hence, the type of the result list is reported
with each use: int list in the first. case and char list in the second. 13.2, RECURSIVE FUNCTIONS 63
3.2.4 How ML Deduces Types
ML is quite good at discovering the types of variables, the types of function
parameters, and the types of values returned by functions. The subject of how
ML does so is quite complex, but there are a few observations we can make
that will cover most of the ways types are discovered. Knowing what ML can
do helps us know when we must declare a type aud when we can skip type
declarations.
1. The types of the operands and result of arithmetic operators must all
agree. For example, in the expression (a+b) +2.0, we see that the right
operand of the * is a real constant, so the left operand (a+b) must also
be real. If the use of + produces a real, then both its operands are real.
‘Thus, a and b are real. They will also have a real value any other place
they are used, which can help make further type inferences.
2. When we apply an arithmetic comparison, we can be sure the operands
are of the same type, although the result is a boolean and therefore not
necessarily of the same type as the operands. For example, in the expres-
sion a<=10, we can deduce that a is an integer.
3. In a conditional expression, the expression itself and the subexpressions
following the then and else must be of the same type.
4. Ifa variable or expression used as an argument of a function is of a known
type, then the corresponding parameter of the function must be of that
type. Similarly, if the function parameter is of known type, then the
variable or expression used as the corresponding argument must be of the
same type.
5. If the expression defining the function is of a known type, then the function
returns a value of that type.
G. If no way to determine the type of a particular use of an overloaded
operator exists, then the type of that operator is defined to be the default
for that operator, normally integer.
Example 3.13: Consider the function comb(n,m) in Fig. 3.6, which we repro-
duce here for convenience.
(1) fun comb(n,m) = (* assumes 0 <= m <= n +)
(2) if m=0 orelse m=n then 1
(3) else comb(n-1,m) + comb(n-1,m-1);
In linc (2), we sce that in one branch of the if then-clsc the result is the integer
1. Thus, the expression on line (3) must also be of type integer, and the function
comb returns an integer value. In line (3) we also see the expressions n-1 and
m-1. Since one operand of each subtraction is the integer 1, the other operands,*
64 CHAPTER 3. DEFINING FUNCTIONS
nin one case and m in the other, must also be integers. Thus, bouh parameters
of the function are integers, or strictly speaking, the (one) parameter of the
function is of type int. * int, that is, a pair of integers.
at line (2). We see m compared with integer 0, so m must be an integer. We also
see n compared with m, and since we already know m is an integer, we know the
same about n. O
3.2.5 Exercises for Section 3.2
Exercise 3.2.1: Write the following recursive functions.
*a) The factorial function that takes an integer n > 1 and produces the
product of all the integers from 1 up to n. Your function need not work
correctly if the argument is less than 1.
b) Given an integer é and a list L, cycle L i times. That is, if
then the desired result is [a:41,0:42)-++sn;41502,-.,a)]- You may use
the function cycle defined in Exercise 3.1.1(f).
*c) Duplicate each element of a list. That is, given the list [a,,a2,...,@n],
produce the list [a1,01,@2,02,..-,4n,4y)-
d) Compute the length of a list.>
¢) Compute x‘, where c is a real and 1 is a nonnegative integer. This function
takes two parameters, x and i, and need not behave well if i < 0.
*!f) Compute the largest element of list of reals. Your function need not
behave well if the list is empty.
! Exercise 3.2.2: In the following function definition
fun foo(a,b,c,d) =
if a=b then cti else
if a>b then c else b+d
it is possible to deduce that a, b, c, and d are all integers. Explain how ML
makes these deductions.
Exercise 3.2.3: Suppose we define a function f by a statement that begins
fun f(a:int, b, c, d, e) =.
®There is a function length in the MI. top-level environment that performs this function;
the exercise asks you to write the function as if it were not already available.3.3. PATTERNS IN FUNCTION DEFINITIONS 65
Tell what can be inferred about the types of b, ¢, d, and/or e if the body of the
function is each of the following if-then-else statements:
* a) if acbtc then d else e.
b) if acb then c else d.
*c) if acb then btc else dte.
!d) if ach then béc else d
e) if béc then a else ctd
a1)
!g) if b() =
I ()
|
! () = ;66 CHAPTER 3. DEFINING FUNCTIONS
‘The identifiers must all be the same (they are each the name of the function),
and the types of the values produced by the expressions on the right of the equal-
signs must all be the same. Likewise, the types of the patterns themselves must
be the came, but they can differ from the type of the values produced,
As with functions in general, the parentheses around the patterns are op-
tional. However, the juxtaposition of expressions representing application of a
function to its arguments has higher precedence than any of the usual opera-
tors. Thus it is wise to put parenthescs around patterns that arc more complex
than a single variable. Otherwise, we run the risk that only the first part of the
pattern will be treated as the function argument, and an error will result.
ML goes through the various patterns in the order that they appear until
it finds one that maiches its argument. The first maich determines the value
produced; other patterns are not considered. Thus, there can be overlap among
the various patterns.
«* It is also legal to fail to cover all possible casca with the forms. However,
you will get the diagnostic
Warning: match not exhaustive
You should then be very sure that the function will be used only with
arguments that match one of the patterns.
Example 3.15: Let us reconsider the function reverse from Example 3.8
There are two patterns for the argument L. If L is empty it matches the
pattern nil. If L is not empty, it will match the pattern x::xs. For instance,
if the list has a single element, x becomes that element and xs gets the value
nil. A nonempty list cannot match nil, and x::xs does not match the empty
list, because there is no head element. to give a value to x. (Tt is not possible to.
give x the value nil because x is an clement, not a list). Thus, the following
definition works:
fun reverse(nil) = nil
| reverse(x::xs) = reverse(xs) @ [x]
val reverse = fn: ‘a list + ‘a list
Compare this definition with the equivalent definition in Fig. 3.3.7 Here,
x plays the role of hd(L) and xs plays the role of t1(L). The above function
operates by first checking if its argument is nil and returning nil if so. If the
argument is not nil, then we can match x: :xs to the argument; x acquires the
valne of the head and x8 acquires the value of the tail
®However, SML/NJ, as a default, treats completely redundant patterns, that is, patterns
that can never be reached when the argument has any value that will match the pattern, as
an error.
There is actually a small difference hetween the two functions we called reverse, con-
cerning the types of the elements that may form the lists being reversed. We shall address
this distinction in Section 5.3.4.3.3. PATTERNS IN FUNCTION DEFINITIONS 67
Names for List Components in Patterns
It is conventional to use a pair of identifiers like x for the head of a list
and xs (read “exes”) for the tail of the same list. However, beware using
a::as. Since as is a keyword in ML (see Section 3.3.2), you will get a
strange diagnostic and must find another variable to use in place of as.
added in call
to reverse(nil)
= ot added in call
x 3 to reverse((3])
pee fe) added in call
x 2 to reverse((2,3])
as E283 added in call
x 1 to reverse([1,2,3])
L £1,2,3] initial environment
Figure 3.11: Binding values to the identifiers of a pattern
Figure 3.11 suggests the addition to the environment that occurs when
reverse(L) is called, where L has the value [1,2,3]. Notice at the last call,
to reverse(nil), there are no bindings for x or xs because the pattern nil
matches the argument. and we never even try to match the second pattern. All
these additions to the environment go away when the initial call to reverse
completes. O
3.3.2 “As” You Like it: Having it Both Ways
It is possible to take a single value and at one time give the value to an identifier
and match the value with a pattern. In the match, variables mentioned in the
¢ their own values. The form ie
as
Example 3.16: Let us wrile a function merge(L,M) that takes two lists of
integers, L and M, that are sorted lowest-first, and merges them. That is,
merge produces a single sorted list. with all the elements of L and M. The68 CHAPTER 3. DEFINING FUNCTIONS
following recursive definition of merge works, assuming that the given lists are
sorted. Note that, although no types are specified, integer type is inferred for
all values hecause that is the default type for <.
BASIS: If L is empty, then the merge is M. If M is empty, the merge is L.
INDUCTION: If neither L nor M is empty, compare the heads of L and M. If
the head of I, say x, is smaller, then the sorted list. is 2 followed by the merge
of the tail of L with all of M. Note that in this case, x is the smallest of all the
elements, so x followed by the merge of the other elements will be the proper
sorted list.
If instead, the head of Af, say y, is
ast ’ ae is
y followed by the merge of L and the tail of M. Since y belongs at the head of
the result, the complete list. will be sorted.
(1) fun merge(nil,M) = M
(2) | merge(L,nil) = L
(3) | merge(L as x::xe, Mas y:tys) =
@) if xsy then x::merge(xs,ii)
(5) else y::merge(L.ys);
val merge = fn : int list * int list + int list
Figure 2 19: Merging two sorted lists
Figure 3.12 defines the function merge. Lines (1) and (2) cover the basis
cases. Line (3) begins the inductive step. Here cach list is nonempty, or the
pattern match would have stopped at line (1) or line (2). When we assemble the
result in lines (4) or (5), sometimes we want to use an entire list and sometimes
only the tail. We also need to refer to the head of each list, to tell which is the
smaller on line (4). Thus, in line (3) we express the first argument both as L and
as x::xs. For instance, if we call merge with first argument [1,2,3], L gets
the value (1,2,3], x gets the value 1, and xs gets the value [2,3]. Similarly,
in line (3) we express the second arguinent both as M and as yiiys.
Then on line (4) we compare the heads. If the head of L is smaller, we
assemble the output by taking the head of L and following it by the result of
merging the tail of L, expressed by xs, with the entire list M. On line (5) we
cover the case where the head of L is not smaller than the head of M. We
assemble the result from the head of M — that
of the entire list Z and the tail of M. O
— followed by the merge
Incidentally, the as-construct in Fig. 3.12 is useful but not essential. We
could have used x: :xs in place of L and y::ys in place of M. Lines (3) through
(5) of Fig. 3.12 would then look like:3.3. PATTERNS IN FUNCTION DEFINITIONS 69
| merge(xiixs, y::ys) =
if x int
Notice that the type of the argument is (int * int) list, that is, a list of
pairs of integers. The head element in the pattern on the second line is (x,y),
80 x acquires as value the first component of the head pair and y acquires the
second component of the head pair. Also, zs in the pattern acquires the tail of
the argument as ils value.
Example 3.19: Another similar function is sumLists shown in Fig. 3.14. It
takes as argument a list whose clements are themselves lists of integers. The
purpose is to sum the integers found among all the lists. Notice that ML finds
the type of the argument to be int list list, that is, a list whose elements
are of type int List. For example, the value of3.3. PATTERNS IN FUNCTION DEFINITIONS 7
sumLists(((1,2], nil, [3,4,5], (6]])
is 21. Here, the argument is a list with four elements: the lists [1,2], nil,
(1) fun sumLists(nil)
(2) | sumLists(nil
(3) | sumLists((
val sumLists = fn :
sumLists (YS)
= x + sumlists(xs
YS);
int list list + int
Figure 3.14: Summing the elements of a list of lists
Line (1) of Fig. 3.14 covers the case where the list of lists is empty and the
sum is 0). Line (2) covers the case where there is a first element. on the list,
but that clement is itself the empty list. In this case, we can dispense with
the head and just sum the integers on the lists of the tail. Line (3) covers the
case where there is at least one element on the list that is the head of the list
We 1 it the f
applying sumLists to the list in which the element x has been removed from
the first list, but all other lists are the same. For instance, if the entire list is
((1,2], (3,4]], then the recursive call’s argument is ((2], (3,4]]. 9
As we learn about constructors and the creation of our own datatypes in
Section 6.2, we find there are many other ways to construct data structures
besides lists (which are constructed by the cons operator ::) and tuples (which
are constructed by parentheses and commas). All datatypes make patterus of
their own. However, there are some other patterns that make sense but are
illegal in ML. For example, we might expect to be able to construct patterns
using the concatenation operator @ or arithmetic operators. The next example
indicates what happens when we try to do so.
Example 3.20: We might expect to be able to break a list into the last element
and the rest of the list. For instance, we might try to compute the length of a
list by:*
fun length(nil) = 0
| Length(xs@[x]) = 1 + length(xs);
Error: non-constructor applied to argument in pattern: @
Error: unbound variabie or constructor: xs
However, as we can see, the pattern xs@[x] is not legal and triggers two error
messages. The first message complains that @ is not a legal pattern constructor.
BML does provide a function length that gives the length of a list. It may be
implemented by expressing a nonempty list as x::xa and returning i#length (xe)72 CHAPTER 3. DEFINING FUNCTIONS
The second message is caused by the fact that, because Ube pattern is flawed,
variable xs does not get bound to a value. Therefore, when we encounter it
later, in the expression length(xs), ML has no value to use for xs.
get a
arithmetic operator to construct a pattern. For instance,
fun square(0) =
| square(x#1) = 1 + 2ex + square(x):
is equally erroneous, even though it is based on a correct inductive definition
2
of’, O
As a final example of a nonpattern, a real constant cannot appear in pat-
terns. For instance, the following function definition
fun £(0.0) = 0
| f(x) =
is regarded as syntactically incorrect because a real number is not permitted in
a pattern.?
3.3.5 How ML Matches Patterns
A pattern, like any expression, can be represented by a tree. The outermost,
or highest-level, operator is the root of the tree, and it has one child for each
operand. The child for an operand ie, §
the root of a
operand. The basis case, an expression or subexpression that is a single constant,
or variable, is represented by a node labeled by that constant or variable.
Example 3.21: Consider the pattern cxpression
(xtiyrizs, w)
This expression has as outermost operator the pair-forming operator, which
we shall represent by (,). Its left operand is the subexpression x::y::zs, and
the right operand is the subexpression w. The latter is represented by a single
node labeled w. The former is grouped x::(y::zs) and is represented by a
tree with root operator ::, left child x (a single node) and right child the
Toot of a tree representing subexpression y::zs. The entire expression tree is
shown in Fig. 3.15(a); for the moment, ignore the curved lines connecting it to
Fig. 3.15(b).
Similarly, Fig. 3.15(b) represents the expression ([1,2,3,4], 5). The root
operator is again the pairing operator (,), and the right child of the root rep-
resents constant 5. The left operand is the list [1,2,3,4]. We build lists as
The reason for this seemingly strange restriction is that MI. does not allow equality tests
between reals; see Section 2.1.4, Without such a test it is impossible to tell whether a given
real constant matches the real constant in a pattern,3.3, PATTERNS IN FUNCTION DEFINITIONS 73
/\ /\
/\
rowers
La 7 \
/\
nil
(a)
Figure 3.15: Matching a pattern to an expression
=
the list consisting of the last element, so there are n uses of the cons operator
in a list of length nO
‘To match a pattern and an expression, we overlay the pattern’s tree and
the expression’s tree, starting, as a basis step, by matching the roots. For
the inductive step, if we have matched nodes N and M of the pattern and
expression respectively, then the children of N and M must also be matched in
order
However, sometimes a match will be impossible, and the pattern-match fails.
This situation occurs when we try to match a pattern node that is labeled by an
operator or constant, and the matching node of the expression has a different
label.
Example 3.22: If we try to match the pattern x::xs with the expression
nil, we must match operator :: with constant nil at the respective roots, and
we fail. If we try to match pattern x: zs with [11 (or as an expression:
nil), we match the roots with operators :: successfully. However, at the
right children we must match the second :: from the pattern with nil from
the expression, and thus we fail. O
If we successfully match the pattern with the expression, then any identifiers
at the leaves of the pattern tree match nodes that represent subexpressions.4 CHAPTER 3. DEFINING FUNCTIONS
These subexpressions become the values associated with those identifiers.
Example 3.23: Consider again Fig. 3.15. The pattern in Fig. 3.15(a) suc-
cessfully matches the expression in Fig. 3.15(b); the curved lines indicate the
correspondence of the nodes. As a result, the node labeled x in the pattern
corresponds to the node labeled 1 in the expression, so x acquires the value
1. The pattern node labeled y corresponds to expression node 2, and pattern
node zs corresponds to the expression node representing expression 3: :4::nil,
or equivalently, the list 3,4]. Finally, the pattern node w corresponds to the
expression node 5. 0.
3.3.6
Often we wish to use an identifier with a special meaning like nil in our pat-
terns. At this point we have few such special words. But beginning in Sec-
tion 6.2. we shall see that. words of this type, called “data constructors,” can he
created by the programmer and used in patterns. Such a misspelled word is usu-
ally a legal identifier and looks like a pattern that matches anything. SML/NJ
treats completely redundant patterns as an error, but other ML compilers may
be
nil
(1) fun reverse(niil)
(2) | reverse(x::xs) = reverse(xs) @ [x];
(3) Error: match redundant
(4) niil >...
6) 3 ras
Figure 3.16: The reverse function with a misspelling
Example 3.24: In Fig. 3.16 is the reverse function of Example 3.15, in which
we h pelled nil (1). We Hines (3) through (5) the
SML/NJ response. The system has detected the pattern niil will match any
argument, and therefore the pattern x: :xs on line (2) can never be reached. The
single arrow at the beginning of line (5) indicates which pattern is redundant.
a
3.3.7 Exercises for Section 3.3
wing functivus fom previous exercises, using
two or more patterns in each.
* a) The factorial function of Exercise 3.2.1(a).
b) The function from Exercise 3.1.1(f) that cycles a list one position. If the
list is empty, return the empty list.3.3. PATTERNS IN FUNCTION DEFINITIONS 75
c) The function from Exercise 3.2.1(b) that cycles a list ¢ limes, where i, as
well as the list, is a parameter.
* d) The function from Exercise 3.2.1(c) that duplicates each element of a list.
e) The function from Exercise 3.2.1(d) that computes z'.
* £) The function of Exercise 3.2.1(e) that computes the largest of a list of
reals.
! Exercise 3.3.2: Write a function that flips alternate elements of a list. That
is, given a list [ay,a2,...,aq] as argument, produce [ag, a1, 04,03, 06,05, -. J. If
! Exercise 3.3.3: Write a function that, given a list L and an integer i, returns
a copy of L with the ith element deleted. If the length of L is less than i, return
Tr
Exercise 3.3.4: Show the sequence of calls to sumLists (as defined in Fig.
3.14) and the bindings to variables af patterns that occur when we call
sumLists(({1,2] ,nil, [3]])
Exercise 3.3.5: Does the pattern of Fig. 3.15(a) match the following expres-
sions? If so, give the value bindings for each of the variables x, y, 2, and
w
* a) (La","b","c"] Da" "e"])
b) ({"a","b"] 4.5)
*c) ((5), (6,71)
Exercise 3.3.6: Draw trees as in Fig. 3.15 to show how the pattern
C(x, y) 28]
matches the expression [((1,2) ,3)].
Exercise 3.3.7: There is a recursive definition of the square of a nonnegative
integer: 0? = 0 (basis), and n? = (n — 1)? + 2n — 1 (inductive step for n > 0).
Write a recursive function that computes the square of its argument using this
inductive formula.
Exercise 3.3.8: Write a function that takes a list of pairs of integers, and
orders the elements of each pair such that the smaller number is first. Use the
as construct, so you can refer to the pair as a whole when it is not necessary
to change it.*
76 CHAPTER 3. DEFINING FUNCTIONS
Exercise 3.3.9: Write a function that takes a list of characters and returns
true if the first element is a vowel and false if not. Use the wildcard symbol
_ whenever possible in the patterns.
Exercise 3.3.10: The simple rule for translating into “Pig Latin” is to take
a word that begins with a vowel and add "yay", while taking any word that
begins with one or more consonants and transferring them to the back before
appending "ay". For example, "able" becomes “ableyay" and "stripe" be-
comes "ipestray". Write a function that converts a string of letters into its
Pig-Latin translation. Hint: Use explode and the function from Exercise 3.3.9
that tests for vowels.
Exercise 3.3.11: Suppose we represent scts by lists. The members of the set
may appear in any order on the lisi, but we assume ihai there is uever inure
than one occurrence of the same element on this list. Write tunctions to pertorm
the following operations on sets.
* a) member(x,S) returns true if element x is a member of set S; that is,
appears somewhere on the list representing S.
b) delete(x,S) deletes x from S$. Remember that you may assume that x
appears at most once on the list for S.
*c) insert(x,S) puts z on the list for S if it is not already there. Remember
that in order to preserve the condition that there are no repeating elements
on a list that represents a set, we must check that x does not already
appear in S; it is not adequate simply to make x the head of the list.
Exercise 3.3.12: Write a function that takes an element a and a list L of lists
of elements of the same type as a and inserts a onto the front of each of the
lists on the list L. For example, if a = 1 and L is ((2,3],(4,5,6] ,nil), then
the result is [[1,2,3], [1,4,5,6], (12).
Exercise 3.3.13: Suppose sets are represented by lists as in Exercise 3.3.12.
‘Lhe power set of a set S is the set of all subsets of S. A set of sets can
be represented in ML by a list whose elements are lists. For example, if S
is the set {1,2}, then the power set of S is {0, {1}, {2}, {1,2}}, where 0 is
the empty set. This power set can be represented in ML by the list of lists
(nil, (1), (2), (1,2]]. ‘That is, the elements of the lists are themselves lists,
each representing one of the subsets of S. Write a function that takes a list
as argument, representing some set S, and produces the power set of S. Hint:
Recursively construct the power set for the tail of the list and use the function
from Exercise 3.3.12 to help construct the power set for the whole list.
! Exercise 3.3.14: Write a function that, given list of reals [a;,a2,...,@n),
computes
Tic; (ai - a3)3.4. LOCAL ENVIRONMENTS USING LET 7
That is, we compute the product of all differences between elements, with the
element appearing later on the list subtracted from the element appearing first.
If there are no pairs, the “product” is 1.0. Hint: Start by writing an auxiliary
Exercise 3.3.15: Write a function to tell whether a list is emply. That is,
return true if and only if the argument is an empty list.!°
Exercise 3.3.16: Explain how ML deduces that the function sumPairs of
Example 3.18 has domain type (int * int) list
3.4 Local Environments Using iet
Sometimes we need to create some temporary values — that is, local variables —
inside a function. The proper way to do so is with a let --- in --- end
expression. A simplified form of this expression, where only val-declarations
are used, is shown in Fig. 3.17.
let
val = ;
val = ;
val =
in
end
Figure 3.17: Simple form of the “let” construct.
That is, following the keyword let is a list of one or more val-declarations,
just like those introduced in Section 2.3.3. These are followed by the keyword
in. Following in is an expression that may use the variables defined after let.
This expression may also use any other variables accessible in the environment
in which the function using let is defined, provided their identifiers are not
redefined by the temporary declarations between let and in. The keyword
end completes the expression. Here are a few important points to remember
about let expressions:
‘© Semicolons following the declarations are optional. We shall adopt Pascal
style and follow each but the last by a semicolon.
«Just as for val-declarations
use the keyword val.
in the top-level environment, don’t. forget to
1O-There is a built-in MI. function nu11 that does this task. We should not use this function
in the solution.78 CHAPTER 3. DEFINING FUNCTIONS
© We must not omit the keywords in and end, which are as essential as the
let.
« In truth, the let expression is more general than is suggested by Fig. 3.17,
and any “declaration” can appear where we have shown val-declarations.
So far, we have not seen any other kinds of declarations besides val-
declarations and function declarations (with the keyword fun). However,
there are several others; for example, we shall meet exception declarations
in Section 5.2. The complete syntax for declarations is in Fig. 9.19.
@ As another generalization, a pattern may appear in place of a single iden-
Lifier in any val-declaration. Also, more than one expression may appear
atter the let, although the utility of an expression list wili not become
apparent, until we study side-effects in Section 4.1.3.
3.4.1. Defining Common Subexpressions
One use of a let expression is to allow us to use common subexpressions. The
following example illustrates the technique.
ber . We could write the expression xx* -+- =x(100 2's) if we had the pa-
tience, but it is less tedious and less prone to error if we write the function in
Fig. 3.18.
fun hundredthPowes (a:veal) =
let
val four = x#x*x*x;
val twenty = four+four+four+four+four
in
twenty*twenty*twenty*twenty*twenty
end;
val hundredthPower = fn : real + real
hundredthPower (2.0);
val it = 1.2675060022823E30 : real
hundredthPower (1.01);
val it = 2.70481382942153 : real
Figure 3.18: Raising a number to the 100th power
In Fig. 3.18 we define two local variables, four and twenty (no jokes about
blackbirds, please). We first define four to be 2‘, and then define twenty to3.4. LOCAL ENVIRONMENTS USING LET 79
De four raised to the fifth power, or 2°, Finally, we use twenty in the final
expression after the keyword in, which is twenty raised to the fifth power, or
100
7100,
00. hitch is at
) Which is about
10%, and then computing (1.01)'°°. The latter value is close to € = 2.718-+-,
as it must be because e is the limit as n goes to infinity of (1+1/n)". oO
We then sec two uses of this function,
3.4.2 Effect on Environments of let
When we enter a let expression, an addition to the current environment is
created, adding value bindings for all the identifiers defined between the lat.
and the in.
twenty 1049576.0 added for
let-expression
four 16.0
ot |
added on call
x 2.0
to hundredthPower
environment before call
to hundredthPower
Figure 3.19: Additions to environment when hundredthPover is called
Example 3.26: In Fig. 3.19 we see the situation when the function of Fig. 3.18
is called. The first addition is for the function call; it is a binding for the
parameter x. The next additions are for the let expression and include bindings
for the local variables four and twenty. We have shown x bound to the value 2.0
in the call and the local variables bound to their consequent values. As always,
when the function call returns, the additions to the environment disappear.
However, the returned value is made available as the value of the function in
the environment that results after the return. O
Exampie 3. can rewrite Fig. 3.18 to use x not oniy as the agi
of the function hundredthPower, but also as both local variables. The function
then appears as in Fig. 3.20. It behaves exactly like the function of Fig. 3.18.
However, the additional bindings in Fig. 3.21 each associate the variable x with
avalue. O80 CHAPTER 3. DEFINING FUNCIIONS
fun hundredthPower (x:real) =
let
val x = xexexex;
val x = xexexexex
in
IMI
end;
val hundredthPower = fn ; real + real
Figure 3.20: Repeat of Fig. 3.18 with x used for all variables
added for second
x 1048576.0 val-declaration
added for first
x 16.0 val-declaration
added on call
x 20
to hundredthPower
environment before call
to hundredthPower
Figure 3.21: Additions to environment corresponding to Fig. 3.20
3.4.3 Splitting Apart the Value Returned by a Function
Another important use of let expressions is when the result of a function has
components or parts that we want to separate before we use them. In particular,
when the type of the value returned by a function is a tuple, we can get at the
components by a more general form of val-declaration than we suggested was
possible in Fig. 3.17. Instead of a single identifier following the word val, we
can have any pattern. For instance, if a function f returns a three-component
tuple, we could write
val (a,b,c) = £(...
aid have ihe dee compunenis uf the resuli of f bound iv variables a, », aud
c respectively. This approach is often more convenient than writing
val x = f(...
which associates the entire tuple with x, and then extracting the individual
components with #i operators in subsequent val-declarations such as3.4. LOCAL ENVIRONMENTS USING LET 81
Patterns for Lists of Length 1
Note that the way we express “list of length 1” as a pattern is to put
square brackets around a single identifier, like [a] in line (2) of Fig. 3.22.
Such a pattern can only match a list with a single element, and variable a
acquires that element as its value.
Another way to express “list of length 1” is with the pattern a::ni1
Again, a acquires the lone clement as its value.
val a = #1(x);
Example 3.28: Let us implement a function split(L) that takes a list L
and splits it into twa lists. One list. consists of the first element, third element,
fifth element, and so on; the other list consists of the second element, fourth
element, sixth element, and so on. This function has an important application.
In tandem with the function merge of Fig. 3.12, it lets us write a function
geSor
in Section 3.4.4.
‘We want the function split to produce a pair of lists. The recursion consists
of two basis parts and an inductive part.
BASIS: If L is empty, then produce a pair of empty lists. If L has a single
element, the first list of the pair produced has that element and the second iist
is empty.
INDUCTION: If the given list has two or more elements, let. the first. two ele-
ments be a and 6. Recursively split the remaining elements into a pair of lists
(M,.V). ‘Lhe desired result is the pair of lists (a :: M, b:: N). ‘That is, the first
list has head @ and tail equal to the first of the returned lists, and the second
has head 6 and tail equal to the second of the returned lists.
An ML implementation of sp1it is shown in Fig. 3.22. Line (1) implements
the first part of the basis: return a pair of empty lists in response to the empty
list. Line (2) implements the second part of the basis, where the given list has
length 1.
Lines (3) through (5) handle the inductive case. ‘Ihe pattern
line (3) can only match a list with at least two elements; a acquires the first
clement as valuc, b acquires the second, and ¢s acquires the list of the third
and subsequent elements as its vaiue. in iine (4), we appiy split recursively
to the third and subsequent elements; the result is bound to the pair (M,N).
‘That is, Mis bound to the first component of the result, which is the elements in
positions 3, 5, 7, and so on of the original list. N acquires the second component
of the return value, which is the elements in positions 4, 6, 8, and so on from
the original list.82 CHAPTER 3. DEFINING FUNCTIONS
(4) fun eplit(nil) = (nil,nil)
(2) | split(fa]) = (fa],nil)
(3) | split(a: s) =
let
(4) val (M,N) = split(cs)
in
(5) (ariM, b::N)
end;
val split = fn: ‘a ist > “a list * ‘a list
split((1,2,3,4,5]);
val it = ([1,3,5],[2.4]) : int list * int list
Figure 3.22: Splitting lists
ally, in Tine (5) we construct the retnrn value for the present call to
split. The first component has head a — that is, the first element of the given
list — followed by M, the list of all the other odd-position components. Thus,
the first component is the odd-position elements in order. Similarly, the second
component b::N is all the even-position clements. O
3.4.4 Mergesort: An Efficient, Recursive Sorter
We can combine the functions merge of Fig. 3.12 with split of Fig, 3.22 to sort
lists of integers. This algorithm is one of the simplest ways to sort n elements
in time proportional to nlogn steps. We shall not develop the analysis of this
algorithm here, but we shall complete the specification of the algorithm in ML.
The idea behind the mergesort algorithm is expressed in the following induction.
BASIS: If the given list L is empty or consists of a single element, then L is
surely sorted already, so just return L.
INDUCTION: If L has at least two elements, split L to produce the (approxi-
mately) half-size lists M and N. Recursively mergesort M and N. ‘Then merge
the sorted lists M and N to produce the sorted version of L.
The function mergeSort is shown in Fig. 3.23. It must be preceded by
the funciious merge and split io form ihe complete impiemeniation of the
mergesort algorithm. Incidentally, ML discovers that mergeSort works only on
integer lists because it uses merge, which we wrote to work only for integer lists.
Lines (1) and (2) implement the basis; the remaining lines are for the induc-
tive step. Line (4) splits the given list. Lines (5) and (6) sort the half-sized lists,
and the result is produced by merging the sorted lists in line (7). Incidentally,*
3.4. LOCAL ENVIRONMENTS USING LET 83
(1) fun mergeSort (nil) = nil
(2) | mergeSort([a]) = [al
(3) | mergeSort(L) =
let
«) val (M,N) = eplit(L);
(5) val M = mergeSort(M);
(6) val N = mergeSort (N)
in
mM merge (M,N)
end;
val mergeSort — for
Figure
: Mergesort
we could also have combined some steps by eliminating lines (5) and (6) and
replacing line (7) by merge (mergeSort (N) ,mergeSort (M)).
3.4.5 Exercises for Section 3.4
Exercise 3.4.1: Write a succinct function to compute 1°,
the components of pair x as needed.
Exercise 3.4.3: Improve upon the power-set function of Exercise 3.3.13 by
using a Let expression and computing the power set of the tail only once.
Exercise 3.4.4: Improve upon the function of Exercise 3.2.1(e), to compute
the maximum of a list of reals, by using a let expression. Hint: Compute the
wmaxiniuis of tae ‘ail of Une Lisi firsi.
Exercise 3.4.5: Write a function to compute 2”' for real x and nonnegative
integer i. You should make only one recursive call in your function. Hint: Note
that we can start with « and apply the squaring operation i times. For cxample,
when i = 3, we compute ((x*)*)?
Exercise 3.4.6: Write a version of sumPairs of Example 3.18 that sums each
component of tie pairs separately, returning a pai f the eum of the
first components and the sum of the second components.
Exercise 3.4.7: Write a function that takes a list of integers as argument and
returns a pair consisting of the sum of the even positions and the sum of the
odd positions of the list. You should not use any auxiliary functions.84 CHAPTER 3. DEFINING FUNCTIONS
3.5 Case Study: Linear-Time Reverse
We have seen two versions of a function to reverse lists: first in Fig. 3.3 and
then in Evample 215. These finetions each seem simple enough, but. they
suffer from a common flaw that they take time proportional to n? to reverse
lists of length n. In comparison, a well-designed reverse function, such as the
function rev in the ML top-level environment, can reverse lists of length n in
time proportional to n. In this section, we shall see how to write a list-reverse
function that is efficient and learn a general Lechnique for progranuning with
lists as we do.
3.5.1 Analysis of Simple Reverse
Let us begin by understanding why a function like that of Example 3.15 takes
time proportional to n?. The function is reproduced here for reference:
fun reverse(nil) = nil
| reverse(x::xs) = reverse(xs) @ [x];
Suppose T(n) is the time it takes reverse to work on a list of length n. We
can develop a recurrence relation, where T(n) is defined in terms of T(n — 1)
and then “solve” the equation for T(n), to get an expression for T(n) in terms:
of nalone (not T).
BASIS: The basis case is when n = 0; that is, the list is empty. In this case the
first pattern, nil, matches, and nil is returned. The whole process takes only
some constant amount of time, so we shall say T(0) = a for some constant a.
INDUCTION: Suppose n > 1. There are a number of steps that the program
will go Uhrough to process a list of length n 2 1:
1. ‘The first pattern doesn’t match, and it takes some constant amount of
time for the ML system to determine that nil doesn’t match the argu-
ment.
2. It takes another constant amount of time to match the pattern x::xs and
assign the head of the list to x and the tail to xs.
3. It takes time T(n — 1) to compute the value of reverse (xs). The reason
is that xs is surcly a list of length n 1 if x::x8 is of length n.
4. To compute the return value reverse(xs) @ [x] requires that we copy
the list reverse(xs) and append the final element x as we do. ‘This
process takes time proportional to n, the length of the resulting list.
The constant time taken by the first two steps is dominated by the linear
time taken by the last step. Thus, T(n) is approximately T(n — 1) + bn for
some constant 6; the T'(n — 1) represents the time for the recursive call and bn
represents the time for the other steps. The recurrence equation is thus:3.5. CASE STUDY: LINEAR-TIME REVERSE 85
T(0) =a
T(n) = T(n— 1) + 6n for n = 1,2,...
There are several ways to solve this equation. Perhaps the simplest to to
check that T(n) = a + bn(n + 1)/2 satisfies the equations and is therefore the
solution. Since a + bn(n + 1)/2 is proportional to n? as n gets large, we see the
justification for our claim that reverse takes time proportional to n? on lists
of length n
A more intuitive arguinent is Lo observe that ou a list of leugih n, reverse
gets called recursively on lists of length n—1, n—2, and so on, down to 0. Each
call on a list of length i results in work bi for some constant b, except the call
1 bi =a + bn(n + 1)/2
which, as we observed, is proportional to n?
3.5.2 ML’s Representation of Lists
be de:
it takes time proportional to the length of the first list to concatenate lists, we
can cons a head and a tail in constant time. Thus, before giving the proper
design for reverse, we must understand something of how ML represents lists
internally.
Lists are represented in a conventional, linked list fashion, as suggested by
Fig. 3.24. Cells consist of a pair of pointers, the first to an element of the list
and the second to the next cell. If a list is bound to a variable L, then in the
ML environment there is an entry in which the identifier L is associated with a
pointer to the first cell of the list.
Figure 3.24: Representing a linked list
ixe given values of head x and tail
Suppose we wish to construct the list x:
s to the
xs. We have only to create a new cell C. The first pointer of C point:
head of the list, that is, to the value of x, and the second pointer in C points86 CHAPTER 3. DEFINING FUNCTIONS
lo the value of xs. In this way, C becomes the first cell on the linked list tat
represents the value of x::xs. The process is suggested by Fig. 3.25. Notice
that creating cell C and setting its pointers to refer to the values of x and xs.
is. There is no need for ML to “look inside” the values of the head or tail.
Similarly, we can invert this process. In constant time we can find the head and
tail of a list. No copying is necessary.
SOE
AU
Vv
ce ff
Tail
Head
——)
Figure 3.25: Applying the cons operator
3.5.3 A Reversal Function Using Difference Lists
‘There is a trick known wo LISP programmers as difference fists, in which one
manipulates lists more efficiently by keeping, as an extra parameter of your
function, a list that represents in some way what you have already accomplished.
The idea comes up in a number of different applications; we hope that seeing
it used to reverse lists will illustrate the technique sufficiently that its use will
be apparent when you need it.3.9. CASE STUDY: LINEAR-TIME REVERSE =
We design an auxiliary function rev1(L,M) whose job is to return L? AL,
that is, the reverse of list L followed by the list M (not reversed). Note that
we use the superscript R as a convenient way to indicate the reverse of a list.
y
if o ha. “ el 5
If we w , We have only to call revi (Lyn: result is
L® concatenated with the empty list, which is just L®.
(1) fun revi(nil, M) = M
(2) | revi(x::xs, ys) = revi(xs, x::ys);
val revi = fn: ‘a list * ‘a list > ‘a list
fun reverse(L) = revi(L,nil);
val reverse = fn: ‘a list + a list
Figure 3.26: List reversal using difference lists
Figure 3.26 shows the function revi and its use to define a linear-time list
reversal function. Line (1) of rev1 handles the basis case. when there is nothing
left to reverse. Then, the result is just a copy of the second argument.
Line (2) handles the inductive case, where we need to reverse a list of one or
more elements. We move the head of the list we need to reverse to the beginning
of the list that is not to be reversed. We then call revi recursively on the new
pair of lists. Eventually, all the elements of the first list are moved to the front
of the second list, in reverse order. At that point, the basis case applies and
the recursion ends.
To see why the technique works, suppose we call
revi([ar,a2,.++,an], [b1,b2,-++50m})
Then the desired output is the list [an,a@n—1,-+.,@1,01,b2,-..;m]. When we
move the head of the first list to the second, we call
bm])
The result of this call is [an.an—1,...,.@2] followed by the element ai, followed
by [bi,b2,---;6m], which is the result desired for the original call to rev1.
revi([a2,a3,...,4n], [ar,b1,b2,
Example 3.29: Herc is the sequence of calls that results when we try to reverse
the list (1,2,3]:
reverse((1,2,3])
revi([1,2,3], nil)
revi([2,3], [1])
revi((3], [2,1])
revi(nil, [3,2,1])
At this point, the basis applies, and the result [3,2,1] is produced. O"
88 CHAPTER 3. DEFINING FUNCTIONS
3.5.4 Analysis of Fast Reverse
We can argue that the reversal programm of Fig. 3.26 takes time proportional to
the length of the list, as follows.
1. Function revi calls itself with a first argument that is shorter by 1 than
its parameter, so with a first argument of length n, rev1 makes n recursive
calls
2. Each recursive call to revi takes a constant amount of time lo break apart
a head and tail and then cons a head and tail, until we get to the basis
(1) of Pig. 3.96. The
3. Thus, revi takes time proportional to the length of its first argument.
4. The time taken by reverse on a list of length n is essentially the time
taken by revi when given a first argument of length n. Thus, reverse
takes time proportional to the length of the list it is to reverse.
3.5.5 Exercises for Section 3.5
Exercise 3.5.1: Write a function cat(L,M) that produces the concatenation
LOM of the lists L and M. However, your function should not use the @ operator;
only the cons operator :+ should he used. Your funetion mst run in time
ne length of L, inc
pi
Exercise 3.5.2: Write a function cycle(L, i) that cycles list L by i positions,
as in Exercise 3.2.1(b). Ilowever, your function must take time proportional to
the length of L (which we assume is at least i). Hint: You need to break this
function into a sequence of steps performed by auxiliary functions.
3.6 Case Study: Polynomial Multiplication
In this section we shall show one useful way to represent polynomials in a single
variable. We shall consider ways to perform polynomial multiplication, which is
also the important signal-proccssing operation known as convolution. We begin
with some simple functions that get the job done, but take time proportional to
n? to multiply polynomials of length n. Then, we exhibit a more complicated
algorithm that mult wal to n!-59. We do not
show the algorithm that is asymptotically most efficient — the “Fast. Fourier
‘Transform” approach. ‘That algorithm takes time proportional to n logn."!
¢ Aho, Hoperaft, and Ullman, Design and Analysis of Computer Algorithms, Addison-
. 1974, for a disenssion of efficient polynomial multiplication, including both the FFT.
and the Karatsuba-Ofman approach discussed in Section 3.6.5.3.6. CASE STUDY: POLYNOMIAL MULTIPLICATION 89
3.6.1 Representing Polynomials by Lists
We shall use lists of reals to represent polynomials by their coefficients, lowest
degree first. For instance, the polynomial «* + 42 — 5 is represented by the list
(5.0, 4.0, 0.0, 1.0]. In general, the polynomial 7, aiz' is represented
by the list of n + 1 elements [a9,@1,...,2,,]. Conventionally, we shall take the
empty list to represent the polynomial 0, but this polynomial also has other
representations such as [0.0] and [0.0, 0.0].
An important observation is that if L is a list representing polynomial P,
and L is of the form a : M (that is, L has head a and tail M), and the tail
represents polynomial Q, then P = a+Qz. That is, multiplication by z in effect
shifts the elements of the corresponding list. one position right. For instanco, if
P=a 440-5
then we observed that the representing list is [“5.0, 4.0, 0.0, 1.0]. Thus,
ais ~5.0and M is [4.0, 0.0, 1.0]. M represents the polynomial Q = x* +4.
Note that P = a+ Qz, that is, P= —5 + (2? +4)2.
In Fig. 3.27 we see three functions that perform common operations on polyno-
mials in this representation. The first, padd (P,Q), adds polynomials P and Q.
We recursively define the sum of two lists P and Q that represent polynomials
by:
BASIS: If either P or Q is the empty list, then the sum is the other. Note that
if both are empty, the result is the polynomial 0 represented by the empty list.
INDUCTION: For the induction, assume that neither list is empty. Suppose
P has head p and a tail representing polynomial R, while Q has head q and
a tail representing polynomial S. Then the sum P + Q is the list with head
element p+4q and tail equal to the result of applying padd to the two tails. The
nd Q = 44 Sr, then
oft
P+Q=(ptq)t+(K+S)x
In line (1) of Fig. 3.27 we sce one part of the basis. Whenever the second
polynomial is the empty list, the result is the first polynomial. Line (2) handles
the other part of the basis. If the first polynomial is empty, the result is the
second.
neither of the first two patterns match the argument
both polynomials are nonempty lists. Thus, in line (3) the pattern
to match the first argument, and q: :qs will surely match the second argument.
Notice we have attached type real to the variable p of this pattern. That is
enough for ML to figure out the type of all variables and to disambiguate the
use of + in line (3).90 CHAPTER 3. DEFINING FUNCTIONS
‘As a result of the match, p acquires the value of the first element of the
first polynomial, and q acquires the value of the first element of the second
polynomial. Their sum becomes the first element of the result, and padd is
(= padd(P,Q) produces the polynomial sum P+Q +)
(1) fun padd(P,nil) = P
(2) | padd(nil,Q) = Q
(3) | padd((p:real)::ps, q::qs) = (ptq)::padd(ps,qs);
(* smult(P,q) multiplies polynomial P by scalar q *)
(A) fun emult(nil,q) = nil
(5) | smult((p:real)::ps,q) = (p+q): :smult(ps,q) 5
(+ pmuii(?,G) produces PG +:
(6) fun pmult(P,nil) = nil
(7) | pmult(P,q::qs) = padd(smult(P,q), 0.0::pmuit(P,qs));
Figure 3.27: Polynomial addition and wultiplication
In lines (4) and (5) of Fig. 3.27 we see the function smult that multiplies a
polynomial P by a scalar q. That is, each term in the polynomial is multiplied
by q. ‘The recursive definition of this operation is:
BASIS: If P is empty, then the product is the empty list representing U.
INDUCTION: If P has head p, then the head of the result is pq. The tail of the
result is found by recursively applying smult to the tail of P and the scalar q.
Line (4) handles the basis and line (5) handles the inductive step. The
justification for this algorithm is that if P =p 4 Rx, then Pq = pq | Raz.
Now let us consider the function pmu1t of lines (6) and (7) of Fig. 3.27. This
function multiplies polynomials P and Q using a recursion on the length of the
second polynomial.
BASIS: If the second polynomial is empty, then the result is empty.
INDUCTION: If the second polynomial Q can be written as q+ S:r, then
PQ=Pq+PSu
‘The product Pq is a scalar multiplication. PS is a recursive application of the
polynomial multiplication with a smaller second argument.3.6. CASE STUDY: POLYNOMIAL MULTIPLICATION 91
The basis is implemented by line (6). In line (7) we see the inductive step;
smult (P,q) produces Pq, while pmult(P ,qs) produces the polynomial product
we called PS in the inductive formula above. To multiply this product. by 2,
that rep,
that rep-
resents PS. That shift is the purpose of the subexpression 0.0: : pmult (P,qs).
Finally, we use padd to add the lists representing Pq and PSz.
3.6.3 Analysis of Simple Multiplication
Let us analyze the running time of each of the three functions in Fig. 3.27.
Analysis of padd
First, we claim that the function padd takes time proportional to the shorter of
its two arguments. To see why, observe that both arguments decrease in length
Ly 1 at cach recursive call un line (3) of Pig. 3.27. When either agument
reaches length 0, line (1) or (2) will stop the recursion. ‘hus, the number of
recursive calls equals the length of the shorter argument.
Ilowever, the work done at each call, exclusive of the recursive call, takes
only a constant amount of time independent of the lengths of the lists. As we
discussed in Section 3.5.2, each of the pattern-matching steps is done without.
looking past the first cons operator, and building the result in line (3) by ap-
plying the cons operator likewise takes a constant amount of time. Finally,
the addition p + q at line (3) also takes constant time independent of the list
hy
Our conclusion is that padd takes time that is a constant per call times the
number of calls, which is the length of the shorter list. That is, the time for
padd is proportional to the length of the shorter list.
Analysis of smult
The number of recursive calls is equal to the length of the list in the first
argument, because the leugil decreases by one ai each call, aud when the be
reaches 0), the first pattern, in line (4), matches and the recursion stops. As for
padd, it is easy to see that the only steps performed at each call, other than the
recursive call, are constant-time operations of matching a cons operator or nil,
applying a cons operator, and an arithmetic step, multiplying two integers. The
running time of smu1t is thus proportional to the length of its first argument —
the polynomial being scalar-multiplicd.
Analysis of pmult
Suppose we execute pmult (P,Q), where P and Q are polynomials (lists) of
length n and m, respectively. Since the recursion is on the second argument,
whose length decreases by 1 at each call, the number of recursive calls is m. We
must calculate the work done at each call, exclusive of the recursive call.y2 CHAPTER 3. DEFINING FUNCTIONS
1. The pattern matching at lines (6) and (7) takes constant time.
The call to smult at line (7) takes time proportional to n.
The cons with head 0.0 at line (7) takes constant time.
Be N
‘The application of padd at line (7) takes time proportional to its shorter
argument. The first argument is always of length n (it is a scalar mul-
tiplication of polynomial P), while the second argument is never shorter
than n. Thus, the application of padd takes time proportional to n.
The calls to smult and padd dominate the work, which is thus proportional
to n at each call. Since there are m recursive calis to pmuit, the total work is
nm for polynomials of length n and m, respectively, or n? if the polynomials
are of the same length n.
3.6.4 Auxiliary Functions for a Faster Multiplication
It turns out that polynomial multiplication of length-n polynomials does not
have to take time proportional to n®. If we use the “fast Fourier transform,”
we can actually do the job in time proportional to nlogn. We shall not give
this algorithm here. Rather, we shall show an intermediate approach that
takes time proportional to n° (the constant 1.59 approximates log, 3) called
the Karatsuba-Ofman algorithm. To begin, there are a number of auxiliary
functions that we shall need. We show them first and analyze their running
times. The functions are shown in Fig. 3.28.
The Function psub
The purpose of psub(P,Q) is to compute P — @ for polynomials P and Q.
It does so by negating Q, i.e., scalar-multiplying it by —1, and then adding.
The running time of this function is no greater than the length of the longer
polynomial, since the call to smut Lakes Lime proportional to the length of Q
and the call to padd takes time proportional to the shorter length.
The Function length
This function, taking the length of a list, is actually a built-in function of ML.
However, we write it here so we can confirm its running time. Notice that the
number of recursive calls equals the length of the list to which it is applied, and
the work done at each call is a constant, independent of the list. Thus, length
requires time proportional to the length of its argument.
‘The Function bestSplit
‘This function serves a technical purpose that will become clear when we see
how the Karatsuba-Ofman algorithm works. The arguments n and m are the3.6. CASE STUDY: POLYNOMIAL MULTIPLICATION 93
(* psub(P,Q) computes the difference of polynomials P-Q *)
y= @
(* length(P) computes length (degree+1) of polynomial P «)
fun length(nil) = 0
1 1+length(ps) ;
(* bestSplit(n,m) computes an appropriate size for the
low-order "half" of polynomials of length n and m.
It is the smaller of n and m should one be less than
half the other. If they are approximately the
same size, then it is half the larger. *)
fun bestSplit(n,m) =
if Qen <= m then n
else if 2«m <= n then m
else if n <= m then m div 2
else (* n/2 ...
(ps psn) > «
The compiler has correctly pointed out that we have made an assumption
about the relationship between the arguments P and n of carve(P,n): n
will never be greater than the length of P. Thus, the first pattern looks
for n = 0, and the second pattern assumes that if n > 0, then P must
not be nil. If we were to call carve(nil,n), where n > 0, then neither
pattern would match and the function would fail. Fortunately, when we
use carve in the Karatsnha-Ofman algorithm, onr assumptions are certain
to be met. However:
© It is generally a bad practice to write functions whose patterns do
not. caver all possible cases, even cases for which the function was
not intended.
the polynomials. That is, we can write a recurrence equation for T(n) the time
it takes to multiply polynomials of length n using Formula 3.1 directly:
T(l)=a
T(n) = 4T(n/2) +n
The solution to this equation is
(a +b)n? — bn
O= Vv
Figure 3.29: Breaking polynomials into half-sized niecoe96 CHAPTER 3. DEFINING FUNCTIONS
That is, T(n) is proportional tu n”, exactly as for the straightforward polyno-
mial multiplication method.
To design a faster algorithm, we need to reduce the number of times we mul-
number of operations that take time linear in the size of the polynomials, such
as adding or subtracting polynomials, “shifting” (multiplying by a power of z),
or “carving” polynomials into two.
We can reduce the number of half-siced mulliplivativus tu hive if we cum
pute TV and UW as in Formula 3.1, but write the middle term as:
TW +UV =(T+U)(V+W)-TV-UW (3.2)
Since TV and UW are already computed, Formula 3.2 uses only one additional
half-sized multiplication, (T+U) times (V +1), rather than the two additional
multiplications nceded if we computed TW + UV directly. Notice that the fact
Formula 3.2 uses two additions and two subtractions in place of a single addition
is not a real problem. Intuitively, multiplication takes time that grows faster
than linear in n, so the cost of the multiplications swamps out the cost of the
additions for large n.
(* komult(P,Q) computes the product of polynomials PQ using
the Karateuba-Ofman method that only calls itself three
times rather than four on half-sized polynomials. *)
(1) fun komult(P,nil) = nil
(2) | womult(nil,Q) = nil
(3) | komult(P,[q]) = smult(P,q)
(4) | komult ([p],Q) = smult(Q,p)
|
(8) komult (P,Q) =
let
(6) = length(P);
wm v = length(q);
(8) val s = bestSplit(n,m);
@) val (T,U) = carve(P,s);
(10) val (V,W) = carve(Q,s);
(41) val TV = komult(T,V);
(12) val UW = komult(U,W);
(13) val TUVW = komult(padd(T,U), padd(V,W));
(14) val middle = psub(psub(TUVW,TV), UW);
in
(15) padd(padd(TV, shift (middle,s)), shift (UW,2*s))
end;
Figure 3.30: The Karatsuba-Ofman multiplication algorithm.3.6. CASE STUDY: POLYNOMIAL MULTIPLICATION 97
Figure 3.30 implements this idea in a recursive ML function. Lines (1) and
(2) handle the basis cases where one of the polynomials is the empty list. In
these cases, the empty list is returned. Lines (2) and (4) handle additional
a polynomial
time scalar multiplication algorithm to
is a constant, so we can use the linear
handles these cases.
Line (5) begins the inductive case. We use each of the auxiliary functions
fium Fig. 3.28 at Icast once in a sequence of val-declarations. Lines (6) aud (7)
compute the lengths of the two polynomials, and line (8) picks the value of s
using the bestSplit function. The role of s, the length of the low-order pieces
T and V, was illustrated in Fig. 3.29.
Then, iines (9) and (i0) divide the two poiynomiais into low-order and
high-order pieces, as suggested by Fig. 3.29. Line (11) computes the first. half.
sized product, TW, and line (12) computes the second: UW’. Lines (13) and
(14) implement the expression of Formula 3.2. That is, line (13) computes
(I +U)(V + W), and line (14) subtracts from this expression the terms TV
and UW
Finally, the result of the function is computed in line (15). This expression
implements Formula 3.1. However, the middle term, TV + UW, has been
computed by Formula 3.2, rather than directly.
3.6.6 Analysis of the Karatsuba-Ofman Algorithm
We can show that the dominant cost of the algorithm of Fig. 3.30 is the three
halt-sized multiplications. Let T(n) be the running time of this function on two
polynomials of length n. For the hasis, where n = 1, one of the basis cases of
lines (3) or (4) applies. The running time is thus some constant, say T(1) = a.
For the induction, let n > 1. Then the inductive case starting at line (5)
applies. The following is a list of the running times for each of steps (6) through
(15):
6: Proportional to n.
7: Proportional to n
8: Constant.
9: Proportional to n.
10: Proportional to n.
11: T(n/2).
12: T(n/2).
13: A term proportional to n for the calls to padd plus T(n/2) for the call to
komult.98 CHAPTER 3. DEFINING FUNCTIONS
14: Proportional to n.
15: Proportional to n.
The sum of the times is thus 3T(n/2) plus a term that is proportional ton. We
may write the recurrence equation as:
T(L
T(n) =
3(n/2) +n
The solution to this equation is
T(n) = (a + 20)ni625 — zon
as you may check by substitution in both equations. Thus, the running time of
the Karatsuba-Ofman algorithm is proportional to n!°#2, or n!-®9, significantly
less than the n* of more straightforward algorithms.
3.6.7 Exercises for Section 3.6
Exercise 3.6.1: Write a function genPoly(n) that generates a polynomial of
length n (degree n — 1), all of whose coefficients are 1.0. Measure the running
time of the straightforward algorithm pmult of Fig. 3.27 and the algorithm of
Fig. 3.30 with its attendant auxiliaries from Figs. 3.27 and 3.28. The code can
Consider polynomials of length n
aPoly. fr does
the running time of komult drop below the running time of pmult?
Exercise 3.6.2: One problem with komult is that for small n it wastes time,
compared with the straightforward approach to polynomial multiplication. Re-
write komult so it calls pmult to multiply polynomials whose length is below
some limit. Experiment with running times as in Exercise 3.6.1 to find the limit
below which it makes sense to use pmult, and adjust your function accordingly.
Exercise 3.6.3: Write a function to evaluate a polynomial at a given real
value a. That is, define a function eval (P,a) that takes a list (polynomial) P
and a real number a, and computes P(a).
Exercise 3.6.4: Given a list of reals [a1,a2,...,@n), find the polynomial whose
roots are a1,42,...,@,. Hint: Note that this polynomial is the product of
(2 -aj) for i=1,2,...,0.
Exercise 3.6.5: We can represent polynomials in two variables, x and y, by
a list of lists. Think of such a polynomial as a polynomial in x, whose coef-
ficients, instead of being real numbers, are polynomials in y. Represent these
polynomials in y by lists as we did in Section 3.6.1. Then use the lists repre-
senting these polynomials as the elements of a list representing the polynomial3.6. CASE STUDY: POLYNOMIAL MULTIPLICATION 99
in «. For example, the polynomial 1+ 2xy + 3xy? + 4x%y can we written as
1+ (2y + 3y)a + (4y)x°. The polynomial 2y + 3y? is represented by the list
(0.0, 2.0, 3.01 and the polynomial 4y is represented hy [0.0, 4.0]. Thus,
al be written
[1.0], [0.0,2.0,3.0], 0, [0.0,4.0]]
Write functions to add polynomials in two variables. scalar-multiply such poly-
nomials, and polynomial-multiply these polynomials. You need not usc a
“Karatsuba-Ofman” type trick to improve efficiency.Chapter 4
In this chapter we shall learn how to read and write information from files. ML
offers us a number of tools, ranging from a simple function that prints strings
to the standard output to more complex functions that perform UNIX-style
input/output and more.
Our study of input and output forces us to learn a number of additional
features of ML. In this chapter we shalll find discussions of the following topics
in addition to input/output:
1. The unit type, which is similar to “void” in C.
2. The type constructor option, which allows us to express values that are
either present or absent.
3. Lists of statements.
4. A way to access functions that are in the standard basis of ML but not
in the top-level environment
4.1 Simple Output.
ML provides a print operator that writes a character string to the standard
output. ‘his function is simple to use and can do most of what we need for
typical output operations. Thus, it is a good point to begin our study of I/O.
4.1.1 The Print Function
‘The expression print (x) causes the value of a character string x to be printed
on the “standard output,” which would be the terminal unless you have called
SML/NJ with another standard output designated (via the UNTX > operator).
The value returned by the print function is the unit (). This symbol, whichLoz CHAPTER 4. INPUT AND OUTPUT
we have not seen before, is the lone value of the type unit, which we have also
not encountered previously.
One purpose of the unit is to serve as the value returned by a function, such
+t, that docs its
ML encountered so far, print has an effect on more than the ML environment;
it changes the external world: either what appears on the user’s terminal or the
contents of the file that is the current standard output.
ork by aside offcct, Not!
by aside off
¢ Note that print does not return the value printed as its own value.
Example 4.1: In Fig. 4.1 is a function called testZero, which tests whether or
not its integer argument is 0 and prints one of the strings "zero" or "not zero"
as appropriate. Notice that ML responds by saying that testZero is a function
from the type integer to the Lype unit, because the unit is the “value” produced
by the print function. The fact that a string is produced as a side-effect is not
reflected in the type of the function.
fun testZero(0)
| testZero(_)
print(“zero\n")
print("not zero\n’
val testZero = fn: int + unit
testZero(2);
nui zero
val it = () : unit
Figure 4.1: A function that uses the print function
We also see in Fig. 4.1 a use of testZero(2) and ML’s response. We first see
the printed response not zero on the standard output. Following immediately
is the normal response of ML after evaluating a function:
val it = () : unit
Notice that the value of the expression testZero(2) is the unit (). That is
what print returns, and therefore that is what testZero returns.
« Remember from Section 2.1.1 that in strings we can use the sequence \n
to represent a newline. Had we omitted printing this character in the
print statements of Fig. 4.1, the output would have run together, as
not zeroual it = () : unit4.1. SIMPLE OUTPUT 103
The Type unit
The unit type is another of the basic types of the ML system. like int. In
a sense it is like the C type void. However, while there is no value for a
“void” in C, the ML unit type has exactly one value, (.
The unit appears in a surprising place in ML: as the argument of
a seemingly zero-argument, function. ‘Thus, if we were to write a zero.
argument function that when called returns the string hello world, it
would appear as follows:
fun hello() = "hello world"
val hello = fn : unit + string
That is, function hello has an argument after all; the unit. It would be
called by applying it to the unit, ac:
hello();
val it = “hello world” : string
4.1.2 Printing Nonstring Values
i 3 other thi rst th
a string. For example, we learned in Section 2.2.4 that the function str will
change a character into a string of length 1. Thus, we could write
Ii
val ¢ = #"a";
print(str(c));
to print an a on the standard output.
we would like to print integers or real numbers, or perhaps values of other
types. There is a function toString associated with integers, reals, and some
other types that converts valucs of those types to appropriate character strings.
The identifier toString denotes one of several rather different functions, and in
order to tell which one is meant, it is necessary to prefix the identifier toString
by the name of the “structure” to which it belongs, and a dot. We shall take
up structures, both user-defined structures and structures provided by ML,
in Section 8.2. However, roughly, for each type there is a structure with the
same name but with the first letter capitalized. For example, the structures Int,
Real, and Bool are associated with the types int, real, and bool, respectively.
Example 4.2: Here is an example of printing the value of a real number as a
string: