Chapter 4Standard ML

Exceptions

raise, handle, exception bindings

Concept

Exceptions are SML's mechanism for non-local control transfer. They are first-class values of type exn that can be raised (like throw) and handled (like catch).

Per Def 5.4 (notes p.115): an exception is a special SML object. Raising an exception in a function aborts computation and returns the exception to the next enclosing handler.

Declaring an exception:
exception Empty;
exception Factorial;
exception SysError of int;
The third form creates an exception constructor that carries an int payload.

Raising:
raise Factorial — aborts the current computation and propagates upward.

Handling — the handle clause (Def 5.10):

expr handle Empty => 0 | SysError n => ~1 | _ => 999;

The handle clause attaches a list of rules to an expression. If expr raises an exception, SML tries each rule top-to-bottom and uses the FIRST one that matches.

Order of handlers matters. If you write _ => 0 first, every exception will match it; the more specific rules below are dead code.

Common pattern — local exception + safe wrapper:

local exception Negative fun fact 0 = 1 | fact n = n * fact (n - 1) in fun safe_factorial n = if n < 0 then raise Negative else fact n end;

The wrapper checks the precondition once; the local fact trusts its argument. This is the recommended pattern from the slides.

Animation — sml exceptions
Transcript — click a line to jump9 cues
  1. 0.0sExceptions: raise and handle
  2. 1.4sA cyan call box: f x
  3. 2.5sraise Foo text appears with an amber bolt
  4. 4.3sBolt moves down toward the green handle block
  5. 6.3sHandle box draws, handle header appears
  6. 7.0sBolt travels into the handle region
  7. 7.7sPattern Foo implies fallback flashes amber
  8. 9.0sResult label: fallback
  9. 10.5sraise transfers control to the nearest matching handle
Effects on machine state

An effect is an action during computation that does not return a value but changes something external. Effects include:
- I/O: reading from / writing to the outside world (files, network, terminal).
- Mutation: changing a mutable variable or array element.
- Communication: sending messages between processes.
- Exceptions: non-local transfer of control (covered separately).
- Logging: writing to an audit trail.

A pure function has no effects. In SML, effects are mediated by special types:
- 'a ref for mutable cells.
- TextIO.instream / TextIO.outstream for I/O streams.
- exn for exceptions.

Documentation convention: write the signature as f : arg → value ! effects where ! effects lists side effects.

TextIO: reading and writing streams

SML provides the TextIO structure for character-based I/O on text streams:

val stdin : instream (* default input stream *) val stdout : outstream (* default output stream *) val inputLine : instream -> string option val print : string -> unit val openIn : string -> instream val openOut : string -> outstream val closeIn : instream -> unit val closeOut : outstream -> unit val output1 : outstream -> char -> unit val input1 : instream -> char option

inputLine returns SOME s if a line was read, or NONE at end-of-file. input1 reads one character (returning NONE at EOF). The 'a option type is the SML idiom for partial functions.

Reading integers with case-of

The function Int.fromString : string -> int option parses a string as an integer, returning NONE if parsing fails. A read_integer function:

exception NaN; fun read_integer () = let val intstring = case TextIO.inputLine TextIO.stdIn of NONE => raise NaN | SOME s => s in case Int.fromString intstring of NONE => raise NaN | SOME i => i end;

Used in a factorial_driver:

fun factorial_driver () = let val input = read_integer () val result = Int.toString (safe_factorial input) in TextIO.print (result ^ "\n") end handle Factorial => TextIO.print "Out of range.\n" | NaN => TextIO.print "Not a Number!\n";
Exception-driven tree operations: getSubtree and cutSubtree

For tree ADTs, exceptions are used to signal error conditions like 'position out of bounds'.

Example (assignment 6.9):

datatype btree = Empty | Node of int * btree * btree; datatype position = Here | Left of position | Right of position; exception TreeExceeded; fun getSubtree p Empty = raise TreeExceeded | getSubtree Here (Node (_, l, _)) = l | getSubtree Here (Node (_, _, r)) = r | getSubtree (Left p) (Node (_, l, r)) = getSubtree p l | getSubtree (Right p) (Node (_, l, r)) = getSubtree p r | getSubtree _ Empty = raise TreeExceeded;

Trace. getSubtree (Left Here) (Node (1, Node (2, Empty, Empty), Empty)):
- Match Left p with p=Herep = Here. Recurse into left subtree.
- Match Here against Node(2,Empty,Empty)Node(2, Empty, Empty). Return l=Emptyl = Empty.

Reading. A position is a path in the tree, from the root: Here = 'at this node', Left p = 'go left then follow p', Right p = 'go right then follow p'. If the path leads off the tree (or to an Empty), raise TreeExceeded.

Used in assignment 6.9. The complete program combines getSubtree, cutSubtree, and a print driver.

Worked example
Step 0 of 2
Practice — score 100% to advance
Multiple choice
Q1
What does raise do?
Q2
What does expr handle rules do (Def 5.10)?
Q3
Why does the ORDER of handler rules matter?
Q4
Per Def 5.4, what is an exception?
Q5
In exception SysError of int, what kind of exception is this?
Q6
TextIO.inputLine TextIO.stdIn returns:
Q7
getSubtree Here of any Node returns:
Fill in the blank
Q1
To abort computation and propagate an exception, we use the keyword ___.
Q2
To attach a list of exception handlers to an expression, we use the keyword ___.
Q3
exception Empty; creates a(n) ___ exception (one without payload).
Q4
Int.fromString "42" returns SOME ___.
Q5
getSubtree raises ___ if the position leads off the tree.
Loading…