Sie sind auf Seite 1von 56

Introduction to ML

CS 331 Principles of Programming Languages revised Spring 2003

Features of ML
A pure functional language
serious programs can be written without using variables

Widely accepted
reasonable performance (claimed) can be compiled syntax not as arcane (or as simple) as LISP

In these slides,
We use Standard ML of New Jersey Runs on PCs, Linux, and other flavors of UNIX Much of this material is based on Ullmans book, Elements of ML Programming
See the SML documentation at
http://www.smlnj.org

Running SML on linix.gl


The SML processor is available at
/afs/umbc.edu/users/n/i/nicholas/pub/331/smlnj.linux/bin

or equivalently
~nicholas/../pub/331/smlnj.linux/bin

Add this directory to your path, and do a rehash Then invoke sml from a shell prompt with the command sml Use control d to exit interpreter

Hello, world in SML


Standard ML of New Jersey, - print("Hello world\n"); Hello world val it = () : unit -

Arithmetic in ML
Copy and paste the following text into a Standard ML window
2+2; 3*4; 4/3; 6 div 2; 7 div 3; (* note semicolon at end*) (* an error! *) (* integer division *)

Declaring Constants
Constants are not exactly the same as variables
they can be redefined, but existing uses of that constant (e.g. in function definitions) arent affected by such redefinition
val freezingFahr = 32;

Ints and Reals


Note ~ is unary minus min and max take just two input arguments, but that can be fixed! The real operator converts to real Parens can sometimes be omitted, but I dont recommend it
Int.abs ~3; Int.sign ~3; Int.max (4, 7); Int.min (~2, 2); real(freezingFahr); Math.sqrt real(2); Math.sqrt(real(2)); Math.sqrt(real 3);

- Int.abs ~3; val it = 3 : int - Int.sign ~3; val it = ~1 : int - Int.max (4, 7); val it = 7 : int - Int.min (~2, 2); val it = ~2 : int - Math.sqrt real(2); stdIn:57.1-57.18 Error: operator and operand don't agree [tycon mismatch] operator domain: real operand: int -> real in expression: Math.sqrt real - Math.sqrt(real(2)); val it = 1.41421356237 : real - Math.sqrt(real 3); val it = 1.73205080757 : real

Strings
Delimited by double quotes the caret mark ^ is used for string concatenation, e.g. house^cat \n is used for newline, as in C and C++

Comparison Operators
The usual <, >, <=, >= and <> are available For reals, = and <> are not available
For reals a and b, a <= b andalso a>= b is an equality test

The connectors andalso and orelse are logical operators with short-circuit evaluation

If Then Else
If Then Else is an expression, not a control structure Example, if quotient, dividend and divisor are reals, we might have
val quotient = if divisor > 0 then dividend/divisor else 0

Tuples
Tuples are data items drawn from a Cartesian product type. Example:
type fraction = int * int; val foo: fraction = (44,100); #1(foo); (* returns 44 *) #2(foo); (* returns 100 *)

Tuples are of fixed size, e.g. 2 in this example

Lists in ML
Objects in a list must be of the same type
[1,2,3]; [dog, cat, moose];

The empty list is written [] or nil

Making Lists
The @ operator is used to concatenate two lists of the same type The :: operator makes a new list in which its first operand is the new first element of a list which is otherwise like the second operand. The functions hd and tl give the first element of the list, and the rest of the list, respectively

List Operations
- val list1 = [1,2,3]; val list1 = [1,2,3] : int list - val list2 = [3,4,5]; val list2 = [3,4,5] : int list - list1@list2; val it = [1,2,3,3,4,5] : int list - hd list1; val it = 1 : int - tl list2; val it = [4,5] : int list

More List Operations


- val list1 = [1,2,3]; val list1 = [1,2,3] : int list - val list2 = [3,4,5]; val list2 = [3,4,5] : int list - 4::list1; val it = [4,1,2,3] : int list - val list3 = list1::list2; an error! - val list3=list1@list2; val list3 = [1,2,3,3,4,5] : int list - length(list3); val length(list3) = 6

Strings and Lists


The explode function converts a string into a list of characters The implode function converts a list of characters into a string Examples: - explode("foo");
val it = [#"f",#"o",#"o"] : char list - implode [#"c",#"a",#"t"]; val it = "cat" : string -

Heads and Tails


The cons operator :: takes an element and prepends it to a list of that same type. For example, the expression 1::[2,3] results in the list [1,2,3] Whats the value of [1,2]::[ [3,4], [5,6]] ? Whats the value of x::[], for any atom x?

Declaring Functions
A function takes an input value and returns an output value ML will figure out the types

Notes
ML is picky about not mixing types, such as int and real, in expressions The value of it is always the last value computed Function arguments dont always need parentheses, but it doesnt hurt to use them

Types of arguments and results


ML figures out the input and/or output types for simple expressions, constant declarations, and function declarations If the default isnt what you want, you can specify the input and output types, e.g.
fun divBy2 x:int = x div 2 : int; fun divideBy2 (y : real) = y / 2.0; divBy2 (5); divideBy2 (5.0);

Two similar divide functions


- fun divBy2 x:int = x div 2 : int; val divBy2 = fn : int -> int - fun divideBy2 (y : real) = y / 2.0; val divideBy2 = fn : real -> real - divBy2 (5); val it = 2 : int - divideBy2 (5.0); val it = 2.5 : real -

Functions and Patterns


Recall that min and max take just two arguments However, using the fact that, for example,
min(a, b, c) = min(a, min(b, c))

Generalizing Min
An example of ML pattern matching
the cons notation x::xs is both a binary constructor and a pattern cases arent supposed to overlap

Note that lists of any size are supported


but the elements are expected to be integers checking that the rest of the list is non-empty is critical - but why?

(* Sample ML program - MinList *)


(* Takes a list of integers as input, and returns a list with at most one element, i.e. the smallest element in the list *) fun MinList([]) = [] | MinList(x::xs) =

if null(xs) then [x]


else [Int.min(x,hd(MinList(xs)))]; MinList([]);

MinList([1,2]);
MinList([315, 41, 59, 265, 35, 897]);

When we run MinList,


- use "MinList.sml"; [opening MinList.sml] val MinList = fn : int list -> int list val it = [] : int list val it = [1] : int list

val it = [35] : int list


val it = () : unit

Building trees
Its easy to build recursive data types in ML Some examples follow

(* Sample ML program - Abstract Syntax Trees *)

(* Declare the ast datatype *)


datatype ast = empty | leaf of int | node of string*ast*ast;

fun traverse(empty) = print "empty tree" |


traverse(leaf(n)) = (print (Int.toString(n)); print " ") | traverse(node(operator, left, right)) = ( traverse(left); print operator; traverse(right)); fun prefix(tree:ast) = (traverse(tree); print "\n"); prefix(empty); prefix(leaf(4)); prefix(node("*",node("+",leaf(5),leaf(3)),node("-",leaf(10),leaf(4))));

Two ways to count


(* count from i to j *) fun countUp(i:int, j:int) = if i=j then print(" "^Int.toString(j)) else (countUp(i,j-1);print(" "^Int.toString(j))); (* count from i to j *) fun TcountUp(i:int, j:int) = if i=j then print(" "^Int.toString(j)^"\n") else (print(" "^Int.toString(i));TcountUp(i+1,j));

What about control structures?


Well, there arent any in the usual (procedural) sense If then else, case, and iteration are all accomplished by evaluation of expressions

Iteration vs. Recursion


(* note that F is a functional parameter *) fun loopIt(i:int,n:int,F) = if i = n then F(i) else let val dummy = F(i) val dummy2 = loopIt(i+1,n,F) in dummy2 (* any expression could be used *) end;

The Print Function


print(This string\n);
print(2+2 is ^Int.toString(2+2)^\n);

Expressions may be grouped with parentheses, e.g (print(a);print(b)) But the grouped expressions may not change the environment, so this is not the same as a block in a procedural language

More About I/O


To access functions in the TextIO structure, open TextIO; To open a file openIn(somefile); The value returned is of type instream endOfStream(file:instream): bool inputN(file:instream,n:int):string input(file:stream):string (* whole file *)

Matches and Functions


Example of match expression:
val rec reverse = fn nil => nil| x::xs => reverse(xs) @ [x];

The rec keyword stands for recursive, which is necessary because the binding of reverse as a function name is not yet established

Anonymous Functions
Functions dont have to have names, e.g. (fn x => x+1) (3) yields 4 Such functions can be passed as parameters, e.g. for use in the map or reduce functions, to be discussed later in this chapter.

If Then Else = Case


The familiar
if E1 then E2 else E3

is equivalent to
case E1 of true => E2 | false => E3

Example: if x<y then #a else #b is the same as


case x<y of true => #a | false => #b (* note same types *)

Exceptions
exception Foo and Bar; raise Foo; exception Foo of string; The handle clause matches exceptions with (hopefully) suitable actions Exceptions can be defined in let clauses

Polymorphic Functions
If you dont know the type in advance, or if it doesnt matter, a list matches a list of any type Example:
fun listLen(x: a list) = if x = nil then 0

else 1+listLen(tl(x));

Higher Order Functions


Functions may be passed as parameters,e.g.
fun trap(a,b,n,F)= if n <= 0 orelse b-a <= 0.0 then 0.0 else let val delta = (b-a)/real(n) in delta*(F(a)+F(a+delta))/2.0+ trap(a+delta,b,n-1,F) end;

Higher-Order Function map


The map function map(F,[a1,a2,,an]) produces the list [F(a1),F(a2),,F(an)] The function may be defined (per Harpers new ML book)
fun map f nil = nil | map f (h::t) = (f h)::(map f t)

Higher-Order Function reduce


The reduce function reduce(F,[a1,a2,,an]) produces F(a1,F(a2,F(,F(an-1, an)))) The reduce function may be implemented as follows (from Ullman)
exception EmptyList; fun reduce (F, nil) = raise EmptyList | reduce (F, [a]) = a | reduce (F, x::xs) = F(x, reduce(F,xs));

More on reduce
Harper gives a more general form of reduce
fun reduce (unit, opn, nil) = unit | reduce (unit, opn, h::t) = opn(h, reduce (unit, opn, t))

Example: two ways to sum a list of numbers


fun add_up nil = 0 | add_up(h::t) = h + add_up t

or
fun add_up alist = reduce (0, op +, alist)

The op keyword allows + to be a parameter

More on reduce
To avoid passing unit and opn as parameters that dont change, again from Harpers book,
fun better_reduce (unit, opn, alist) = nil = unit | red (h::t) = opn(h, red t)) in red alist end let fun red

We have less overhead by passing only those parameters that change

More on reduce
Staging helps even more! Again from Harper
fun staged_reduce (unit, opn) = = unit | red (h::t) = opn(h, red t)) in red end let fun red nil

We can use staged_reduce on many lists, e.g.


reduce(unit, opn, alist)

is the same as (but slower than)


staged_reduce(unit, opn) alist

Higher-Order Function filter


The filter function takes a predicate P and a list [a1,a2,,an] and returns the sublist such that P is true for every element of the sublist To implement filter
fun filter(P, nil) = nil | filter(P, x::xs) = if P x then x::filter(P,xs) else filter(P,xs)

The ML Type System


Basic types include int, real, string, char, bool, and others Tuple types, e.g. int*real*char Function types, e.g. int->bool Type constructors list and option
int list char option

Creating Names for Types


type orderpair = int*int type finiteSequence = real list; and these can be parameterized

Datatypes
Enumerated types, e.g.
datatype berryType = raspberry | blueberry | blackberry;

So then we can say, for example,


val b:berryType = raspberry;

Recursive Datatypes
Example: binary trees, where the values may be of some type label:
datatype label btree = Empty | Node of label * label btree * label btree

val inBinary: int btree = Node(5,Node(1,Empty,Empty),Empty)

ASTs Revisited
(* Sample ML program - Abstract Syntax Trees *) (* Assume that terminalType and nonterminalType already known *) (* Declare the ast datatype *) datatype ast = empty | leaf of terminalType | node of nonterminalType*(ast list); fun traverse(empty) = print "empty tree" | traverse(leaf(t)) = (printTerminal t; print " ") | traverse(node(nt, []) = printNonterminal(nt) | traverse(node(nt, x::xs)) = (printNonterminal(nt); traverse(x); traverseList(xs)) and fun traverseList([]) = print | traverseList(x::xs) = (traverse(x); traverseList(xs));

Record Structures
Records are wrapped in curly braces, and fields are separated by commas Field names may be used to refer to specific elements of the record

Record Example
- type addressType = {street:string, city:string, zip:int}; type addressType = {city:string, street:string, zip:int} (note that SML sorted the fields alphabetically) - val umbc:addressType = {street="1000 Hilltop Circle", city="Baltimore",zip=21250}; val umbc = {city="Baltimore",street="1000 Hilltop Circle", zip=21250} : addressType - #city(umbc); val it = "Baltimore" : string

Pattern Matching in Records


Pattern matching works, as in
x as {street=xstr,city=xcity,zip=xzip}::xs

If we dont care about all the fields, use an ellipsis, e.g. x as {street=xstr,}::xs Or even x as {city,}

Arrays
open Array; val zeroVector = array(100,0); sub(zeroVector,0) is zero, as is sub(zeroVector,99) update(zeroVector,2,3.14) changes the third element of the (now misnamed) zeroVector

Case Studies
Hash tables
Make an array of hash buckets, each bucket containing a simple list of values

Triangularization of a matrix
If the array has m rows and n columns, make an array of m elements, each element being an array of n elements.