| A Concise Introduction to ML 97 Copyright © 1998, 2000 by David Matuszek |
This paper describes the 1997 version of Standard ML. There are a very few differences from the earlier (1990) version of ML, and most implementations of Standard ML are very similar. CAML is another dialect of ML, and is not covered here.
ML is case-sensitive.
ML is interactive; you start it up, then you type expressions (at the prompt) to be evaluated. Every expression ends with a semicolon. ML responds with both the value and the type of the result. Example:
-3 * 4;
> val it = 12 : int-
In this example, it stands for the English word "it."
You can read this response as: Its value is 12, and its type is
int. The value of it is always the value most
recently computed at the prompt.
To define a value, use the form:
val pi = 3.1416;
ML is an expression-oriented language, not a
statement-oriented language. That is, everything in it is considered
to be an expression and to have a value. A few expressions that are
executed for their side effects rather than their value (mostly
output expressions) return the unit, (), as their
value--this is like void in C or Java.
To define a function, use the form:
fun average (x, y) = (x + y) / 2.0;
ML is strongly typed; however, it almost always figures out the types for itself. If you need to help it along, you can specify the type of any identifier by following it by a colon and the name of the type. For example, in the function
fun max (x, y) = if x > y then x else y;
the variables x and y could be int, real, char, or
string; if you don't specify, ML assumes that they are int. To define this function
to use char values, give ML a hint by attaching :char to any one
of the variables, anyplace in the function. For example, you could say
fun max (x:char, y) = if x > y then x else y; |
|
| or | fun max (x, y):char = if x > y then x else y; |
| or | fun max (x, y) = if x > y then x:char else y; |
| or any of several other possibilities. | |
To execute the expressions in file myFile.sml (usually definitions
that you are testing), use
use "myFile.sml";
Predefined ML functions are in groups called structures, organized around some particular data type. A structure is like a function library. The collection of these structures is called the common basis. The most common functions are in the "top-level environment," which means you don't have to do anything special to use them. For less commonly used functions, you have to either prefix the function name with the name of the structure, or you have to "open" the structure. Opening the structure means making the contents visible to the program.
For example, abs (absolute value) is defined for real
numbers in the top-level environment, so we can say
-abs 10.0;
> val it = 10.0 : real
but if we want to take the square root of a real number, we need to say either
-Math.sqrt 10.0;
> val it = 3.16227766 : real
or
-open Math;
>(* long list of functions, including sqrt, omitted *)
- sqrt 10.0;
> val it = 3.16227766 : real
Finally, comments are indicated as:
(* This is a comment (* and comments may be nested.
*) *)
There are two kinds of identifiers.
' or
with a letter and consist of letters, digits, underscores, and
single quotes (called "primes" in mathematics; for example,
x' would be read as "x prime."). Alphabetic identifiers are
usually used for names of variables and functions.( ) [ ] { } " , . or whitespace. They are
usually used for operators, for
example, + ++ := <<<Alphanumeric identifiers beginning with a ' (single
quote) are used only for type identifiers. Often ML will use 'a
to indicate a variable (unknown or arbitrary) type. For
example, 'a list means a list whose elements are of
type 'a. Additional variable types are indicated
by 'b, 'c, and so on.
The variable _ (an underscore all by itself) is used
as a "wildcard" or "don't care" variable.
There are several primitive types in ML; the following table gives the most important ones.
|
Primitive type |
Examples |
Notes |
|---|---|---|
|
|
|
0x starts a hexadecimal number
|
|
|
|
Can't start or end with a decimal point |
|
|
|
These are the only boolean values. |
|
|
|
|
|
|
|
# before a string of length 1 |
|
|
|
This is a value. It is the only value of its type,
and is often used when the value isn't important (much
like |
There are two special real constants in ML:
inf (infinity) represents the result of dividing
a positive real number by 0.0, andnan (not a number) represents the result of the
division 0.0/0.0.There are three families of constructed types in ML: lists,
tuples, and functions. Functions are first-class objects: they
can be created, manipulated, passed as parameters, and otherwise
treated like other kinds of values. (However, when ML prints the
result of an expression, and that result is a function, ML doesn't
print out the entire function; it just prints the word
fn.) Every function takes exactly one parameter as input
and returns one value as its result; however, that parameter and that
result may each be of a constructed type, such as a tuple.
The following table gives a few examples of constructed types. Pay special attention to the second column, which shows how ML expresses type information.
|
Example expression |
Expression type |
Notes |
|---|---|---|
|
|
|
Lists may be of any length, but all elements must be of the same type. |
|
|
|
The empty list can be represented by |
|
|
|
The type of a tuple depends on its length and the types in each position. |
|
|
|
Tuples can contain lists, and vice versa. |
|
|
|
A tuple with one element is the same as that one element. |
|
|
|
All functions take exactly one parameter, and parentheses are optional. |
|
|
|
In this example the one parameter is a tuple. |
|
|
|
In this example the one parameter is the "unit," and so is the result. |
|
|
|
Functions are values, and can be put into lists and tuples. |
|
Function |
Examples |
Notes |
|---|---|---|
|
|
|
(Prefix) Unary negation. |
|
|
|
(Infix, left associative) Conjunction, with short-circuit
evaluation. " |
|
|
|
(Infix, left associative) Disjunction, with short-circuit evaluation. |
|
Function |
Examples |
Notes |
|---|---|---|
|
|
|
(Prefix) Unary negation. |
|
|
|
(Infix, left associative) Multiplication; operands and result are all of same type. |
|
|
|
(Infix, left associative) Division of real numbers. |
|
|
|
(Infix, left associative) Integer division with truncation. |
|
|
|
(Infix, left associative) Modulus. |
|
|
|
(Infix, left associative) Addition of reals or of integers. |
|
|
|
(Infix, left associative) Subtraction of reals or of integers. |
|
Function |
Examples |
Notes |
|---|---|---|
|
|
|
Convert integer to real. |
|
|
|
Numbers ending in .5 are rounded to an even number. |
|
|
|
Fractional part is discarded. |
|
|
|
Largest integer not greater than argument. |
|
|
|
Smallest integer not less than argument. |
|
|
|
ASCII value of character. |
|
|
|
Character corresponding to ASCII value; argument must be in range 0..255. |
|
|
|
Convert single character to string of length 1. |
|
Function |
Examples |
Notes |
|---|---|---|
|
|
|
Less than. |
|
|
|
Less than or equal to. |
|
|
|
Equals. |
|
|
|
Not equal. |
|
|
|
Greater than or equal to. |
|
|
|
Greater than. |
The operators
< <= = <> >= >
can be applied to strings for lexical comparisons.
|
Function |
Examples |
Notes |
|---|---|---|
|
|
|
Infix concatenation of two strings. |
|
|
|
String concatenation. |
|
|
|
Number of characters in string. |
|
|
|
Convert string to list of characters. |
|
|
|
Convert list of characters to string. |
|
|
|
Convert int to string. |
|
|
|
Convert real to string. |
|
|
|
Convert bool to string. |
|
|
|
Convert single character to string of length 1. Same
as |
The operators
< <= = <> >= >
can be applied to characters.
Functions that begin with Char. are in the Char
structure. Enter open Char; to use these
functions without typing Char. each time (open
will also display a list of the functions thus made
available).
Char.contains and Char.notContains use
currying: Char.contains "aeiou" returns a
function of type char -> bool that will return
true if given a vowel as an argument. For example, the
following two expressions are equivalent (and both return
true):
Char.contains "aeiou" #"e";
(Char.contains "aeiou") #"e";
|
Function |
Notes |
|---|---|
|
|
Get the next character in the ASCII sequence; same as
chr |
|
|
Get the previous character in the ASCII sequence; same as
chr |
|
|
Given a lowercase letter, return the corresponding capital letter. Given any other character, return that same character. |
|
|
Given a capital letter, return the corresponding lowercase letter. Given any other character, return that same character. |
|
|
Test if character is alphabetic. |
|
|
Test if character is a capital letter. |
|
|
Test if character is a lowercase letter. |
|
|
Test if character is a digit. |
|
|
Test if character is a hexadecimal digit. Hex digits A thru F may be uppercase or lowercase. |
|
|
Test if character is alphabetic or numeric. |
|
|
Test if character is a seven-bit character. |
|
|
Test if character is whitespace (blank, tab, newline, etc.) |
|
|
Test if character is printable (including whitespace). |
|
|
Test if character is printable and not whitespace. |
|
|
Test if the string contains the given character. |
|
|
Test if the string does not contain the given character. |
A list is a set of elements, all of the same type, enclosed in
brackets and separated by commas. Example: ["hello",
"bonjour", "guten Tag"]. The type of this example
is string list.
The empty list is represented by [] or
by nil.
|
Function |
Examples |
Notes |
|---|---|---|
|
|
|
The "head" of a list is its first element. Same as |
|
|
|
The "tail" of a list is the list with its first element
removed. Same as |
|
|
|
(Infix, right associative) Inserts an element as the new
head of a list. Same as |
|
|
|
(Infix, right associative) Append two lists. Same
as |
|
|
|
Convert string to list of characters. |
|
|
|
Convert list of characters to string. |
|
|
|
Reverse the list. |
|
|
|
Return a new list consisting of the first int elements of the given list. |
|
|
|
Return a new list with the first int elements removed. |
If T is a tuple of three elements, then #1(T)
is the first element, #2(T) is the second
element, and #3(T) is the third element. #4(T)
would be an error. This works only for integer constants;
if n is a variable, #n(T) is illegal
syntax.
The most useful functions with side effects are the I/O functions. Since ML automatically prints the values of expressions entered at the prompt, you can often avoid doing any explicit I/O.
The most commonly used output function is print,
which prints only strings. If you want to print something other than
a string, it is your responsibility to convert it to a string. For
example, if N is an integer, Int.toString (N)
is its string representation.
|
Function |
Examples |
Notes |
|---|---|---|
|
|
|
|
The function print is automatically available. If you
want to use other I/O functions, you should open the TextIO
library by saying
open TextIO;
(or you can prepend TextIO. to each of the function
names in the following table). TextIO provides the
streams stdIn, stdOut, and
stdErr; you don't need to explicitly open these streams.
An instream is an input stream; an outstream
is an output stream.
|
Function |
Notes |
|---|---|
|
|
The argument is the name of a file to be opened for reading. |
|
|
Tests for end of file. |
|
|
Reads int characters and returns them as a string. Will wait for input from a terminal, or return fewer characters if at the end of a file. |
|
|
Reads up to |
|
|
Reads some system-defined number of bytes from the input stream. |
|
|
Result is either |
|
|
Peeks at next character, if any. |
|
|
Test if at least int characters remain. |
|
|
Close the input stream. |
|
Function |
Notes |
|---|---|
|
|
The argument is the name of a file to be opened for writing. |
|
|
The argument is the name of a file to be opened for appending. |
|
|
Write the string to the file. |
|
|
Flush the output buffer, thus ensuring that all data that has been written to the file actually makes it there. |
|
|
Close the file. |
ML is a purely functional language: everything is built from functions. Functions return values. Statements don't return values, they have side effects. There are no statements in ML. Things in ML that look like statements are actually expressions, and return values.
The state of a program is the collection of values in the environment. Statements, especially assignment statements, have side effects--that is, they change the state of a program by altering values in the environment. To understand what a program is doing, you have to know the state of the program. Since states can be very large, understanding a program can be difficult.
ML claims to be stateless. That is, you don't need to know about any environmental variables in order to understand a program. Everything you need is either in the function parameters or in the execution stack (the set of function calls that got you to this point). In theory, this should make an ML program easier to understand than a program in a nonfunctional language. There is actually some truth in this point of view.
Because you can define and redefine values and functions at the prompt, the argument that ML is stateless is a little difficult to justify. However, if all your functions are defined in a file, and you just read in that file and use the functions, the stateless nature of ML is more obvious.
The case expression looks like this:
case<expression>of<match>
where a <match> has the form:
<pattern1>=><expression1>|
<pattern2>=><expression2>|
. . .
<patternN>=><expressionN>
First, the initial <expression> is evaluated, then
its value is compared against each of the <patterns>.
When a matching <patterni> is found, the
corresponding <expressioni> is evaluated
and becomes the value of the case expression.
The most common patterns are
(x, y), 5 or "abc", x::xs, to match the head and tail of a
nonempty list, and as expression to have a name for the entire actual
parameter as well as its parts; for example, L as x::xs, which
matches the same things as x::xs, but also assigns to L
the entire list.Examples:
case (N + 1) of 1 => "a" | 2 => "b" | 3 => "c" | 4 => "d";
case myList of [] => "empty" | x::xs => "nonempty";
Because every expression must have a value, ML will warn you if it does not think you have a pattern for every possible case.
When expressions are nested, it can be difficult to tell exactly where the case expression ends. As with any expression, it doesn't hurt to put parentheses around it, as for example,
(case (N + 1) of 1 => "a" | 2 => "b" | 3
=> "c" | 4 => "d");
The if expression looks like this:
if<boolean expression>then<expression1>else<expression2>
If the <boolean expression> is true, then <expression1>
is evaluated and is the value of the expression, otherwise <expression2>
is evaluated and is the value of the expression.
Since the if expression is an expression and not a statement, it must have
a value; therefore, the else part is required.
The if expression is really shorthand for the following case expression:
case<boolean expression>of true =><expression1>| false =><expression2>
When the ML compiler detects an error in an if expression, it reports the error as though it occurred in the corresponding case expression.
We can execute a sequence of expressions by separating them with semicolons and enclosing the group in parentheses; the value of the sequence is the value of the last expression in the sequence. Any values produced by earlier expressions are discarded. This is only useful with expressions that are evaluated for their side effects.
(print "Hello, "; print name; print "\n");
Expressions with side effects in ML are like the mad relative that
lives in the attic and that no one likes to talk about. However,
printing output is a side effect, and output is about the only
use of side effects in ML. Output operations produce the unit,
(), as their value. ML therefore expects all the expressions
(except the last) in a semicolon-separated list to have a unit value,
and will warn you if that isn't the case.
An "exception" is an error. You can declare new types of exceptions, with or without a parameter, as follows:
exception <name> ;
exception <name> of <type> ;
To signal that one of these exceptions has occurred, use
raise <name> ;
The way you use exceptions is as follows:
<expression>handle <match>
where <expression> can perhaps raise an exception;
if it does, the first rule in <match> that matches
the expression will be executed. If you raise an exception and fail to handle
it, ML will give you an "uncaught exception" error.
Example:
exception WrongOrder of int*int; fun fromTo(m, n) = if m > n then raise WrongOrder(m, n) else if m = n then [m] else m::fromTo(m + 1, n); fun makeList(m, n) = fromTo(m, n) handle WrongOrder(m, n) => (print("Error"); []);
The usual form of a function definition is
fun <name> <parameter> = <expression>
;
A function only has one parameter, but that parameter is frequently a tuple, for example,
fun max (x, y) = if x > y then x else y;
Because a function has only one parameter, the following is legal:
val pair = (3, 5);
max pair;
(The value 5 is returned.)
It doesn't hurt to use a tuple of length 1 in place of a single parameter; the following are equivalent definitions:
fun score x = if x < 0 then 0 else
x;
fun score (x) = if x < 0 then 0 else x;
Functions that don't use their parameter must still have one; they can be given the unit as the parameter:
fun tab () = print "\t";
Similarly, a function can only return one value, but that value can easily be a tuple. For example,
fun vectorAdd ((x1, y1), (x2, y2)) = (x1 + x2, y1 +
y2);
Functions are values, and the definitions given above are syntactic sugar. Specifically,
fun <name> <parameter> = <expression>
| <name> <parameter> = <expression> | ...
; |
|
| stands for | val rec <name> = <match> ; |
(The rec stands for "recursive," and can be omitted
for non-recursive functions.)
Functions use patterns as formal parameters, and parameter transmission is by pattern matching. Examples:
fun length (nil) = 0
| length (x :: xs) = 1 + length xs;
fun member (X, nil) = false
| member (X, x::xs) =
if (X = x)
then true
else member (X, xs) ;
As noted above, the usual form of a function definition is
fun<name> <parameter>=<expression>;
but this is actually implemented using matches, as
val rec<name>= fn<match>;
(The rec stands for "recursive" and may be omitted for nonrecursive
functions.) As an example, here is an alternate definition for length:
val rec length = fn
nil => 0
| x::xs => 1 + length xs;
Functions must be defined before they are used. To define mutually
recursive functions, use fun...and...and.... The
following example (to return every other element of a list) is from
Elements of ML Programming by Jeffrey D. Ullman:
fun
take (L) =
if L = nil then nil
else hd(L) :: skip(tl(L))
and
skip (L) =
if L = nil then nil
else take(tl(L)) ;
It is sometimes convenient to declare local variables and
functions using let...in...end. For example,
fun circleData (radius) =
let
val pi = 3.1415926536;
val circumference = two * pi *
radius;
fun area radius = pi * radius
* radius
in
(circumference, area
(radius))
end;
Matches can also be used to define anonymous functions:
(fn x => x + 1) 3
(Result is 4.) Anonymous functions cannot be recursive because, being anonymous, they have no name by which you can call them.
In other languages, "polymorphic" means that you have two or more functions with the same name; in ML it means that a single function can handle more than one type of parameter.
An example of a built-in polymorphic function is the list
function hd; it returns the first element of any type of
list.
ML functions that you write will be polymorphic if their parameters are used only with polymorphic functions and operators. For example, the following function to reverse the two components of a 2-tuple is polymorphic:
fun revPair (x, y) = (y, x);
To write a polymorphic function, you need to avoid:
<,
<=, >, >=You can use:
hd,
tl, ::, @, along with the
constant nil #1, #2, and so
on = and
<>.A higher-order function is one which takes a function (or a function-containing structure) as an argument, or produces a function (or function-containing structure) as its result, or both. For example,
-fun try (f, x) = f x;
> val try = fn : ('a -> 'b) * 'a -> 'b
-try (hd, [1,2,3]);
> val it = 1 : int
-try (tl, [1,2,3]);
> val it = [2, 3] : int list
Notice in particular the type returned when try is defined:
('a -> 'b) * 'a -> 'b. Here, ('a -> 'b)
is a function from type 'a to type 'b, type 'a
is the needed parameter type, and 'b is the result type.
To use an infix operator as a function, precede it by the
word op. For example, op + (3,5)
is the same as 3+5. Hence,
-try (op +, (3, 5));
> val it = 8 : int
As was stated earlier, every function takes exactly one argument and produces one result.
A curried function is a higher-order function that takes an argument and produces as result a new function with that argument embedded in the function. For example,
-fun incr x y = x + y;
> val incr = fn : int -> int -> int
-incr 5 3;
> val it = 8 : int
-(incr 5) 3;
> val it = 8 : int
-val incr5 = incr 5;
> val incr5 = fn : int -> int
-incr5 3;
> val it = 8 : int
Notice the way the function incr is defined, as if it
had two blank-separated arguments. In fact, incr takes
one argument, x, and produces the curried function
(incr x), which then has y as an argument.
Now incr can be called as incr 5 3
or as (incr 5) 3, but cannot be
called as incr (5, 3). This is because
incr takes one argument, an integer, and returns a function as
a result; it does not take a tuple as an argument.
The fact that incr 5 returns a function as a result
is further emphasized in the assignment of this value (a function
that adds 5 to its argument) to the variable incr5,.
Note also that we can use val rather than
fun because we don't need to specify a parameter; a function
is just another kind of value. We could, if we wished, have
defined incr5 by
fun incr5 (x) = incr 5 x; with
exactly the same result.
map
map is a curried function that takes a function that
applies to one thing of type 'a and produces a function
that applies to a list 'a. For example,
-round 3.75;
> val it = 4 : int
-map round [2.7, 3.1, 3.8, 9.4, 6.5];
> val it = [3, 3, 4, 9, 6] : int list
-(map round) [2.7, 3.1, 3.8, 9.4, 6.5];
> val it = [3, 3, 4, 9, 6] : int list
List.filter
The function List.filter takes a boolean test and
returns a function that will extract from a list those elements that
pass the test.
- List.filter (fn x => x > 0) [3, 0, 2, ~5, ~8, 4];
> val it = [3, 2, 4] : int list
Important: When you define a function, you may use other values (including other functions) in the definition. Later changes to those values do not change the meaning of the function you have defined. For example, look at the following sequence (for clarity, only the last response by ML is shown):
- val x = 3;
- fun aa () = 5;
- fun bb () = aa () + x;
- val x = 17;
- fun aa () = 83;
- bb ();
> val it: int = 8
Here, bb() was defined in terms of aa() and
x. Later changes to aa() and x do not affect
the definition of bb().
The above may seem strange, but there is a parallel in algorithmic languages.
Consider the sequence of statements a := 5; b := a; a := 10; You
would not expect the value of b to change just because the value
of a changed. In ML, functions are values, and they are treated
just like any other values.
A definition is said to have referential transparency if its meaning does not depend on the context it is in. Functions in ML have referential transparency, that is, changing the context (other variables and other functions) does not change the meaning of any functions you have already defined. This fact can be crucial when you are debugging a program, because you are likely to be redefining functions fairly frequently.
(To be continued)