You will be responsible for learning ML on your own.
Today I will cover some basics
Read Robert Harper’s notes on “an introduction to ML”
See course webpage for pointers and info about how to get the software
Highlights
Functional Language
Functions are pervasive:
First-class values
Storable, arguments, results, nested
Strongly-typed language
Every expression has a type
Certain errors cannot occur
Polymorphic types provide flexibility
Flexible Module System
Abstract Types
Higher-order modules (functors)
Interactive Language
Type in expressions
Evaluate and print type and result
Compiler as well
High-level programming features
Data types
Pattern matching
Exceptions
Mutable data discouraged
Read – Eval – Print – Loop
- 3 + 2;
Read – Eval – Print – Loop
- 3 + 2;
> 5: int
Read – Eval – Print – Loop
- 3 + 2;
> 5: int
- it + 7;
> 12 : int
Read – Eval – Print – Loop
- 3 + 2;
> 5: int
- it + 7;
> 12 : int
- it – 3;
> 9 : int
- 4 + true;
Type clash in : 3 + true
Looking for a : int
I have found a : bool
Type
Error
Read – Eval – Print – Loop
- 3 + 2;
> 5: int
- it + 7;
> 12 : int
- it – 3;
> 9 : int
- 4 + true;
Type clash in : 3 + true
Looking for a : int
I have found a : bool
- 3 div 0;
Failure : Div
Type
Error
- run-time error
- ();
> () : unit => like “void” in C (sort of)
=> the uninteresting value/type
- true;
> true : bool
- false;
> false : bool
- if it then 3+2 else 7;
> 7 : int
- false andalso loop_Forever;
> false : bool
“else” clause is always necessary and also, or else short-circuit eval
Integers
- 3 + 2
> 5 : int
- 3 + (if not true then 5 else 7);
> 10 : int
Strings
- “Dave” ^ “ “ ^ “Walker”;
> “Dave Walker” : string
- print “foo\n”; foo
> 3 : int
Reals
- 3.14;
> 3.14 : real
No division between expressions and statements
Interactive mode is a good way to start learning and to debug programs, but…
Type in a series of declarations into a
“.sml” file
- use “foo.sml”
[opening foo.sml]
… list of declarations with their types
SML has its own built in interactive
“make”
Pros:
It automatically does the dependency analysis for you
No crazy makefile syntax to learn
Cons:
May be more difficult to interact with other languages or tools
sources.cm
Group is a.sig
b.sml
c.sml
a.sig
b.sml
c.sml
% sml
- OS.FileSys.chDir “ ~/courses/510/a2 ” ;
- CM.make();
[compiling … ] looks for “ sources.cm
” , analyzes dependencies compiles files in group
[wrote … ] saves binaries in ./CM/
- CM.make
’ “ myproj/ ” (); specify directory
ML has a rich set of structured values
Tuples: (17, true, “stuff”)
Records: {name = “Dave”, ssn = 332177}
Lists: 3::4::5::nil or [3,4]@[5]
Datatypes
Functions
And more!
Rather than list all the details, we will write a couple of programs
Interpreters are usually implemented as a series of transformers: lexing/ parsing evaluate print stream of characters abstract syntax abstract value stream of characters
compilers
COS 320
US!
lexing/ parsing evaluate stream of characters abstract syntax abstract value print stream of characters
An arithmetic expression e is
a boolean value an if statement (if e1 then e2 else e3) the number zero the successor of a number the predecessor of a number a test for zero (isZero e)
datatype term =
Bool of bool
| If of term * term * term
| Zero
| Successor of term
| Predecessor of term
| IsZero of term
-- constructors are capitalized
-- constructors can take a single argument of a particular type vertical bar separates alternatives type of a tuple another eg: string * char
If (Bool true, Zero, Successor (Successor Zero)) represents “if true then 0 else succ(succ 0)”
If
Suc.
Booltrue
Zero
Suc.
Zero
function name function parameter fun isNumberValue t = case t of
Zero => true
| Successor t2 => true
| _ => false default pattern matches anything
function name function parameter fun isNumberValue t = case t of
Zero => true
| Successor t2 => true
| _ => false default pattern matches anything
fun isNumberValue (t:term) : bool = case t of
Zero => true
| Successor t2 => true
| _ => false val isNumberValue : term -> bool
ML does type inference => you need not annotate functions yourself (but it can be helpful)
fun isNumberValue t = case t of
Zero => 0
| Successor t2 => true
| _ => false line 25 line 22 ex.sml:22.3-25.15 Error: types of rules don't agree [literal] earlier rule(s): term -> int this rule: term -> bool in rule:
Successor t2 => true
Actually, ML will give you several errors in a row: ex.sml:22.3-25.15 Error: types of rules don't agree [literal] earlier rule(s): term -> int this rule: term -> bool in rule:
Successor t2 => true ex.sml:22.3-25.15 Error: types of rules don't agree [literal] earlier rule(s): term -> int this rule: term -> bool in rule:
_ => false
fun isNumberValue t = case t of zero => true
| Successor t2 => true
| _ => false
The code above type checks. But when we test it refined the function always returns “true.”
What has gone wrong?
fun isNumberValue t = case t of zero => true
| Successor t2 => true
| _ => false
The code above type checks. But when we test it refined the function always returns “true.”
What has gone wrong?
-- zero is not capitalized
-- ML treats it like a variable pattern (matches anything!)
fun isNumberValue t = ...
fun isValue t = case t of
Bool _ => true
| t => isNumberValue t
exception Error of string fun debug s : unit = raise (Error s)
exception Error of string fun debug s : unit = raise (Error s) in SML interpreter:
- debug "hello"; uncaught exception Error raised at: ex.sml:15.28-15.35
fun isNumberValue t = ...
fun isValue t = ...
exception NoRule fun eval1 t =
...
case t of
Bool _ | Zero => raise NoRule
...
fun eval1 t = case t of
Bool _ | Zero => raise NoRule
| If(Bool b,t2,t3) => (if b then t2 else t3)
| If(t1,t2,t3) => If (eval1 t1,t2,t3)
...
exception NoRule fun eval1 t = case t of
Bool _ | Zero => ...
| ...
| Successor t => if isValue t then raise NoRule else let val t’ = eval1 t in
Successor t’ end
fun eval1 t = case t of
...
| ...
| Successor t => ...
| Predecessor t => ...
| IsZero t => ...
be sure your case is exhaustive
fun eval1 t = case t of
...
| ...
| Successor t => ...
What if we forgot a case?
fun eval1 t = case t of
...
| ...
| Successor t => ...
What if we forgot a case?
ex.sml:25.2-35.12 Warning: match nonexhaustive
(Bool _ | Zero) => ...
If (Bool b,t2,t3) => ...
If (t1,t2,t3) => ...
Successor t => ...
fun eval1 t = ...
fun eval t = let in fun loop t = loop (eval1 t) val message = “Done\n”
((loop t) handle
NoRule => print message
| Error s => print s) end
Be very careful with the syntax of handle
(use extra parens)
There are many different ways to define functions!
I almost always use “fun f x = ...”
When I am only going to use a function once and it is not recursive, I write an anonymous function:
(fn x => ...)
val n = 3 binds a variable (n) to a value (3) binds a variable
(isNumberValue) fn keyword introduces anonymous fun val isNumberValue =
(fn t => case t of zero => true
| Successor t2 => true
| _ => false) to the anonymous function value
a type definition (very convenient) type ifun = int -> int val intCompose : ifun * ifun -> ifun = ...
a pair of anonymous functions fun add3 x = intCompose ((fn x => x + 2), (fn y => y + 1)) x
type ifun = int -> int pattern match against arg val intCompose : ifun * ifun -> ifun = fn (f,g) =>
(fn x => f (g x)) argument is pair of functions result is a function!
fun add3 x = intCompose ((fn x => x + 2), (fn y => y + 1)) x
fun f x = ........ can be written as: val f = (fn x => ......) provided the function is not recursive; f does not appear in ........
fun f x = ....
can always be written as: val rec f = (fn x => ...f can be used here...) keyword rec declares a recursive function value
fun isNumberValue Zero = true
| isNumberValue (Successor t2) = true
| isNumberValue (_) = true
This is just an abbreviation for fun isNumberValue t = case t of
Zero => true
| Successor t2 => true
| _ => true
fun isNumberValue 0 = true
| isNumberValue (Successor t2) = true
| isNumberValue (_) = true ex.sml:9.1-11.29 Error: parameter or result constraints of clauses don't agree [literal] this clause: term -> 'Z previous clauses: int -> 'Z in declaration: isNumberValue =
(fn 0 => true
| Successor t2 => true
| _ => true)
Functions like compose work on objects of many different types val compose = fn f => fn g => fn x => f (g x) compose (fn x => x + 1) (fn x => x + 2) compose not (fn x => x < 17)
Functions like compose work on objects of many different types val compose = fn f => fn g => fn x => f (g x) compose not (fn x => x < 17) compose (fn x => x < 17) not
BAD!!
Functions like compose work on objects of many different types val compose = fn f => fn g => fn x => f (g x) compose: (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b)
Note: type variables are written with ‘
compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) not : bool -> bool
compose not : (‘c -> bool) -> (c’ -> bool) compose not not : bool -> bool
compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) not : bool -> bool
compose (fn x => x) : ?
compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) not : bool -> bool
compose (fn x => x) : ?
‘d -> ‘d
compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) not : bool -> bool compose (fn x => x) : ?
‘d -> ‘d must be the same ie:
‘a = ‘d
‘b = ‘d
compose : (‘d -> ‘d) -> (‘c -> ‘d) -> (‘c -> ‘d) not : bool -> bool compose (fn x => x) : ?
‘d -> ‘d must be the same ie:
‘a = ‘d
‘b = ‘d
compose : (‘d -> ‘d) -> (‘c -> ‘d) -> (‘c -> ‘d) not : bool -> bool
compose (fn x => x) : ?
‘d -> ‘d
(‘c -> ‘d) -> (‘c -> ‘d)
Lists: nil : ‘a list
:: : ‘a * ‘a list -> ‘a list
3 :: 4 :: 5 :: nil : int list
(fn x => x) :: nil : (‘a -> ‘a) list
Functions over lists are usually defined by case analysis (induction) over the structure of a list fun length l = case l of nil => 0 l x :: l => 1 + (length l)
Hint: often, the structure of a function is guided by the type of the argument (recall eval)
fun map f l = case l of nil => [] l x :: l => (f x) :: (map f l) an incredibly useful function:
- map (fn x => x+1) [1,2,3];
> val it = [2,3,4] ; int list
fun fold f a l = case l of nil => a l x :: l => f (fold f a l) x another incredibly useful function what does it do?
use it to write map.
Signatures
Interfaces
Structures
Implementations
Functors
Parameterized structures
Functions from structures to structures
structure Queue = struct type ‘a queue = ‘a list * ‘a list exception Empty val empty = (nil, nil) fun insert (x, q) = … fun remove q = … end
structure Queue = struct type ‘a queue = ‘a list * ‘a list end exception Empty
...
fun insert2 q x y =
Queue.insert (y, Queue.insert (q, x))
structure Queue = struct
...
end structure Q = Queue fun insert2 q x y =
Q.insert (y, Q.insert (q, x)) convenient abbreviation
structure Queue = struct
...
end open Queue fun insert2 q x y = insert (y, insert (q, x)) for lazy programmers
-- not encouraged!
structure Queue = struct end type ‘a queue = ‘a list * ‘a list
...
fun insert2 (q1,q2) x y : ‘a queue =
(x::y::q1,q2) by default, all components of the structure may be used
-- we know the type ‘ a queue
signature QUEUE = sig type ‘a queue exception Empty abstract type
-- we don ’ t know the type ‘ a queue val empty : ‘a queue val insert : ‘a * ‘a queue -> ‘a queue val remove : ‘a queue -> ‘a * ‘a queue end
signature QUEUE = sig type ‘a queue
... end structure Queue :> QUEUE = struct type ‘ a queue = ‘ a list * ‘ a list val empty = (nil, nil)
… end does not type check fun insert2 (q1,q2) x y : ‘ a queue =
(x::y::q1,q2)
Opaque ascription
Provides abstract types structure Queue :> QUEUE = …
Transparent ascription
A special case of opaque ascription
Hides fields but does not make types abstract structure Queue_E : QUEUE = …
SEE Harper, chapters 18-22 for more on modules
functors (functions from structures to structures) references (mutable data structures)
ref e; !e; e1 := e2 while loops, for loops arrays
(* comments (* can be *) nested *) a bunch of other stuff...
Learning to program in SML can be tricky at first
But once you get used to it, you will never want to go back to imperative languages
Check out the reference materials listed on the course homepage