In this document we introduce the basic concepts of the ML functional language.
We note that anything you type into the interpreter (or in an ML program) will be either:
In what follows we look at some expressions and at two types of definitions: value definitions and function definitions.
In general terms, an expression would be something like:
To each expression we can associate a type to which it belongs; the basic types in ML are:
The basic operators are + for addition, - for subtraction, * for multiplication, div for division and mod for remainder after division.
The comparison operators <, >, <= and >= work as expected.
The function real converts an integer to a real.
The basic operators include +, -, *, and / is for division, and the comparison operators are the same as for int; however expressions cannot contain mixed data types.
The function trunc truncates a real to an integer, and the function round rounds a real to the closest integer.
The main operators are andalso for conjunction, orelse for disjunction and not for negation. Both andalso and orelse use "short circuit evaluation", in that they only evaluate their second operand if necessary.
The main operator is ^ which is used for string concatenation.
- 3+4;
The interpreter then responds as follows:
> val it = 7 : int
The interpreter responds with its most recently evaluated expression; in this case it is telling us that the value of this expression is 7, which is of type int. The value is also represented by the constant "it", thus the constant "it" can be used in the next expression.
- it + 2; > val it = 9 : int
Here are some more examples of ML's expression evaluation:
- 3+4*6; > val it = 27 : int - (3+4)*6; > val it = 42 : int - 5 div 2; > val it = 2 : int - 5 mod 2; > val it = 1 : int - 65.0 / 4.0; > val it = 16.25 : real - 4 > 1+2; > val it = true : bool - (3>6) orelse (3>2); > val it = true : bool - (3>6) andalso (3>2); > val it = false : bool - "abc" ^ "def"; > val it = "abcdef" : string - "ab" ^ "DE" ^ "fg"; > val it = "abDEfg" : string
If we use an operator with the wrong type of expression, the interpreter responds with a suitable error message. For example, we might get:
- 9 / 4; ! Toplevel input: ! 9 / 4; ! ^ ! Type clash: expression of type ! int ! cannot have type ! real
- if (3>2) then 4 else 5; > val it = 4 : int
Note that:
- (); > val it = () : unit
There are no associated operators.
We use the unit type when we are interested in some expression for its side-effects, rather than for its value - a common example of this is input/output.
Every function must take some arguments and return a result: specifying the unit type for these indicates that we're not really interested in them (e.g. a function that has return type unit is a little like a function with return type void in C).
While the unit type is common to many functional languages, its use should be taken as an indication that we are departing from a purely functional style of programming.
For example:
- val pi = 22.0 / 7.0; > val pi = 3.14285714286 : val pi : real
The only change in the interpreter's reaction is to acknowledge that it now has a different name for this value.
These constants can them be used in later expressions:
- pi * 2.0; > val pi = 6.28571428571 : real
A function body is simply an expression; there is no need to explicitly indicate the value to be returned, since it is taken to be the value of this expression.
As an example, the following function simply adds 1 to its argument:
- fun succ n = n+1; > val succ = fn : int -> int
Note the differences in the interpreter's response this time:
To apply this function we simply give its name followed by the argument, thus:
- succ 5; > val it = 6 : int
We can also apply the function using parenthesis around arguments, thus:
- succ(10); > val it = 11 : int
- fun mult x y = if y=0 then 0 else x + (mult x (y-1)); > val mult = fn : int -> int -> int
Here, x and y are the parameters, and we are making use of the fact that x * y = x + x * (y-1).
Remember that we are using the "functional" style when we apply a function to its arguments in the case of the application of mult to the arguments x and y-1. In a procedural language we might have written mult(x,y-1), but in a functional language we simply write mult x (y-1).
Note that function application binds to the left, so that if we write mult x y-1 it would actually be interpreted as (mult x y) - 1, hence the extra parenthesis.
The second point to note is the type of the function: int -> int -> int. This is the type of a function that takes a parameter of type int and returns something of type int -> int. Thus
- mult 2; > val it = fn : int -> int
Even though we have not given the mult function all of its arguments, we still do not get an error! What's going on here is a process of "partial application" of the function known as currying. When the interpreter sees that we have only given one argument, it is quite happy to carry out the normal substitution, and wait for the second argument.
Thus mult 2 is the same as the function double defined as:
- fun double y = if (y=0) then 0 else 2 + (mult 2 (y-1)); > val double = fn : int -> intThis is a function which, given any y, will evaluate to 2 * y, and is indeed of type int -> int.
Whenever we define a function with more than one parameter, we can always supply it with less arguments than necessary to get specialized versions.
So when we write e.g. mult 2 3, what effectively happens is that:
Thus mult 2 3 could also be written as (mult 2) 3 - we don't need to write down all the extra brackets since function application always associates to the left.
f a b
f (a+1) (b mod 2)
(f a b)
You also do *not* try to bracket it and add the comma. If you write "f(a,b)" then you are telling ML to apply the function f to a single argument, the "pair" (a,b).
Imperative languages have many features which violate referential transparency:
This side-effect free style of programming does take some getting used to, particularly if you are used to having updateable variables (and using assignment statements), but pays substantial dividends when it comes to debugging and maintaining your code.
The important thing to remember is that:
Hence the main way of implementing iteration in a functional language is through the use of recursive functions.
For example, the function in ML to print a string is called print and has type string -> unit. Normally something which has return type unit effectively returns no information so, in terms of referential transparency, this function appears to do nothing. The real situation of course is that it does nothing other than printing its argument; for example:
- print "Hi there!\n"; Hi there! > val it = () : unit
The evaluation of the function simply yields (); the string that is printed on screen is a side-effect.
Similarly, function that we define which contains just the print function will also return nothing:
- fun howdy name = print ("Hello " ^ name ^ "\n");
>val howdy = fn : string -> unit
- howdy "John";
Hello John
> val it = () : unit
Performing output (and input) in ML is an exception to the rule of
referential transparency; this is somewhat to be expected, since the input and
output environments that ML is dealing with are outside the scope of the
interpreter.
Thus we might write:
- ("abc"; 1; true);
> val it = true : bool
With ordinary expressions sequencing does not have much use, since the values of all expressions other than the first are effectively lost. Thus sequencing is mainly used when some expressions in the sequence have side-effects, as is the case with input and output.
An example of this might be the simple function:
- fun printPlus n =
if (n=0) then print "\n"
else (print "+"; printPlus (n-1));
> val printPlus = fn : int -> unit
- printPlus 4;
++++
> val it = () : unit
Since print expressions are probably the only non-pure expressions you'll be using, it is worth noting this simple rule of thumb:
| Only use expression-sequencing when the first expression is a print expression |
For any two expressions e1 and e2, if you write "(e1 ; e2)" then you are telling ML to evaluate and then throw away the value of e1. Thus, if e1 is anything other than a print expression (which we only use for its side-effect), you're probably doing something wrong!
Written by James Power
Revised September 2010 by M. van Bommel