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.
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&emdash;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,
fun max (x:char, y) = if x > y then x else y;
To execute the expressions in file myFile (usually definitions that you are testing), use
use "myFile";
To load a library unit, such as Int, use
load "Int";
Once loaded, the functions in the library unit can be used, but you have to prefix them with the name of the library and a dot, as in Int.max (5, 3); To "open" a library unit means to make the functions in it available without prefixing them; open Int like this:
open Int;
(* This is a comment (* and comments may be nested. *) *)
There are two kinds of identifiers.
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.
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 |
---|---|---|
int |
0, 5, 42, ~17, 0x00FF |
~ is used for unary minus, |
real |
0.0, ~5.3, 1.7e14, 1e~10 |
Can't start or end with a decimal point |
boolean |
true, false |
These are the only boolean values. |
string |
"", "One\nTwo" |
"\n" is newline, |
char |
#"a", #"\n" |
# before a string of length 1 |
unit |
() |
This is the only value of this type, and is often used to mean "no value" (much like void in C). |
There are two special real constants in ML:
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 |
---|---|---|
[2, 3, 5, 7, 11] |
int list |
Lists may be of any length, but all elements must be of the same type. |
nil |
'a list |
The empty list can be represented by [] or by nil. The type of this list might be unknown. |
(5, "hello", ~16) |
int * string * int |
The type of a tuple depends on its length and the types in each position. |
("abc", [1, 2, 3]) |
string * int list |
Tuples can contain lists, and vice versa. |
(3.5) |
real |
A tuple with one element is the same as that one element. |
fun double x = 2.0 * x; |
real -> real |
All functions take exactly one parameter. |
fun sum (x, y) = x + y; |
int * int -> int |
In this example the parameter is a tuple. |
fun hi () = |
unit -> unit |
In this example the parameter is the "unit," and so is the result. |
(double, [sum]) |
(real -> real) * |
Functions are values, and can be put into lists and tuples. |
Function |
Examples |
Notes |
---|---|---|
not : bool -> bool |
not true, not (i = j) |
(Prefix) Unary negation. |
andalso : bool * bool -> bool |
(i = j) andalso (j = k) |
(Infix, left associative) Conjunction. "and" is a keyword with a different meaning. |
orelse : bool * bool -> bool |
(i = j) orelse (j = k) |
(Infix, left associative) Disjunction. |
Function |
Examples |
Notes |
---|---|---|
~ : int -> int |
~5, ~limit |
(Prefix) Unary negation. |
* : int * int -> int |
2 * limit, |
(Infix, left associative) Multiplication; operands and result are all of same type. |
/ : real * real -> real |
7.0 / 3.5, score / average |
(Infix, left associative) Division of real numbers. |
div : int * int -> int |
limit div 2 |
(Infix, left associative) Integer division with truncation. |
mod : int * int -> int |
limit mod 2 |
(Infix, left associative) Modulus. |
+ : int * int -> int |
2 + 2, limit + 1 |
(Infix, left associative) Addition of reals or of integers. |
- : int * int -> int |
2 - 2, limit - 1 |
(Infix, left associative) Subtraction of reals or of integers. |
Function |
Examples |
Notes |
---|---|---|
real : int -> real |
real i, real (limit) |
Convert integer to real. |
round : real -> int |
round (average) |
Numbers ending in .5 are rounded to an even number. |
trunc : real -> int |
trunc average |
Fractional part is discarded. |
floor : real -> int |
floor average |
Largest integer not greater than argument. |
ceil : real -> int |
ceil (average + 3.0) |
Smallest integer not less than argument. |
ord : char -> int |
ord #"a" |
ASCII value of character. |
chr : int -> char |
chr 97 |
Character corresponding to ASCII value; argument must be in range 0..255. |
str : char -> string |
str initialLetter |
Convert single character to string of length 1. |
Function |
Examples |
Notes |
---|---|---|
< : 'a * 'a -> bool |
i < 0 |
Less than. a' can be int, real, char, or string. |
<= : 'a * 'a -> bool |
x <= 0.0 |
Less than or equal to. a' can be int, real, char, or string. |
= : 'a * 'a -> bool |
s = "abc" |
Equals. a' can be int, char, or string, but not real. |
<> : 'a * 'a -> bool |
ch <> #"\n" |
Compares two ints for inequality. a' can be int, char, or string, but not real. |
>= : 'a * 'a -> bool |
i >= j |
Greater than or equal to. a' can be int, real, char, or string. |
> : 'a * 'a -> bool |
x > y |
Greater than. a' can be int, real, char, or string. |
The operators < <= = <> >= > can be applied to strings for lexical comparisons.
Function |
Examples |
Notes |
---|---|---|
^ : string * string -> string |
"Hello, " ^ name |
Infix concatenation of two strings. |
concat : string list -> string |
concat ["ab", "c", "de"] |
String concatenation. |
size : string -> int |
size "hello" |
Number of characters in string. |
explode : string -> char list |
explode inputString |
Convert string to list of characters. |
implode : char list -> string |
implode [#"h", #"i"] |
Convert list of characters to string. |
Int.toString : int -> string |
print (Int.toString 5); |
Convert int to string. Load Int library before using. |
Real.toString : real -> string |
print (Real.toString 3.1416); |
Convert real to string. Load Real library before using. |
Bool.toString : bool -> string |
print (Int.toString (5 < 3)); |
Convert bool to string. Load Bool library before using. |
Char.toString : char -> string |
print (Char.toString #"a"); |
Same as str. On my system, appears to be pre-loaded. |
The operators < <= = <> >= > can be applied to characters.
Functions that begin with Char. are in the Char library package. Enter load "Char"; to load this library. 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 |
---|---|
Char.succ : char -> char |
Get the next character in the ASCII sequence; same as chr (ord #"a" + 1). |
Char.pred : char -> char |
Get the previous character in the ASCII sequence; same as chr (ord #"a" - 1). |
Char.toUpper : char -> char |
Given a lowercase letter, return the corresponding capital letter. Given any other character, return that same character. |
Char.toLower : char -> char |
Given a capital letter, return the corresponding lowercase letter. Given any other character, return that same character. |
Char.isAlpha : char -> bool |
Test if character is alphabetic. |
Char.isUpper : char -> bool |
Test if character is a capital letter. |
Char.isLower : char -> bool |
Test if character is a lowercase letter. |
Char.isDigit : char -> bool |
Test if character is a digit. |
Char.isHexDigit : char -> bool |
Test if character is a hexadecimal digit. Hex digits A thru F may be uppercase or lowercase. |
Char.isAlphaNum : char -> bool |
Test if character is alphabetic or numeric. |
Char.isAscii : char -> bool |
Test if character is a seven-bit character. |
Char.isSpace : char -> bool |
Test if character is whitespace (blank, tab, newline, etc.) |
Char.isPrint : char -> bool |
Test if character is printable (including whitespace). |
Char.isGraph : char -> bool |
Test if character is printable and not whitespace. |
Char.contains : string -> char -> bool |
Test if the string contains the given character. |
Char.notContains : string -> char -> bool |
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 |
---|---|---|
hd : 'a list -> 'a |
hd [3, 5, 7] |
The "head" of a list is its first element. Same as car in Scheme. |
tl : 'a list -> 'a list |
tl [3, 5, 7] |
The "tail" of a list is the list with its first element removed. Same as cdr in Scheme. |
:: : 'a * 'a list -> 'a list |
1 :: [3, 5, 7] |
(Infix, right associative) Inserts an element as the new head of a list. Same as cons in Scheme. |
@ : 'a list * 'a list -> 'a list |
[1, 3, 5] @ [7, 9] |
(Infix, right associative) Append two lists. Same as append in Scheme. |
explode : string -> char list |
explode inputString |
Convert string to list of characters. |
implode : char list -> string |
implode [#"h", #"i"] |
Convert list of characters to string. |
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. #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 |
---|---|---|
print : string -> unit |
print "Hello\n" |
print only prints strings. |
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 |
---|---|
openIn : string -> instream |
The argument is the name of a file to be opened for reading. |
endOfStream : instream -> bool |
Tests for end of file. |
inputN : instream * int -> string |
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. |
inputLine : instream -> string |
Reads up to \n; if no \n, one is added, unless the line is completely empty. |
input : instream -> string |
Reads the entire file. |
input1 : instream -> char option |
Result is either NONE or SOME <char>. These values can be used in patterns. |
lookahead : instream -> char option |
Peeks at next character, if any. |
canInput : instream * int -> bool |
Test if at least int characters remain. |
closeIn : instream -> unit |
Close the input stream. |
Function |
Notes |
---|---|
openOut : string -> outstream |
The argument is the name of a file to be opened for writing. |
openAppend : string -> outstream |
The argument is the name of a file to be opened for appending. |
output : outstream * string -> unit |
Write the string to the file. |
flushOut : outstream -> unit |
Flush the output buffer, thus ensuring that all data that has been written to the file actually makes it there. |
closeOut : outstream -> unit |
Close the file. |
Example:
fun copyFile (inputFileName, outputFileName) = let val inFile = openIn inputFileName; val outFile = openOut outputFileName in ( output (outFile, input inFile); flushOut outFile; closeOut outFile ) end;
(Note: this function copies the first 60 bytes from the input file to the output file. I assume this is controlled by some setting that I don't know about yet.)
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 this is an expression and not a statement, it must have a value; therefore, the else part is required.
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
Examples:
case (N + 1) of 1 => "a" | 2 => "b" | 3 => "c" | 4 => "d";case [1] 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.
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. This is most useful with expressions evaluated for their side effects.
(print "Hello, "; print name; print "\n");
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.
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";
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;
Functions can use patterns in their arguments. 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) ;
Patterns can also be used in rules, which have the form
pattern => expression
A match expression consists of one or more rules separated by vertical bars:
pattern1 => expression1 | pattern2 => expression2 | ...
As noted above, the usual form of a function definition is
fun <name> <parameter> = <expression> ;
but an alternate form, using matches, is
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;
Matches can also be used to define nonrecursive 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 they contain only 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 arithmetic operators (because ML needs to tell whether the arithmetic is integer or floating point), the inequality comparisons <, <=, >, >=, string concatenation, boolean operators, and type conversion operators. You can use: the list operators hd, tl, ::, @, and brackets and commas, along with the constant nil; parentheses and commas to form tuples, along with #1, #2, and so on; and equality tests = 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 (a, x) = a 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. It is a function whose argument is a pair: the first element of the pair is a function of type ('a -> 'b), the second element is a suitable type for that function, 'a, and the result type is the result type of the function, 'b.
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
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. Also notice that when the function is applied, the implicit parenthesization is that of the function and the usual one argument. Finally, notice the way incr5 is defined, as a value rather than as a function.
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
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
(To be continued)