(* 15-150, Spring 2020 *)
(* Michael Erdmann & Frank Pfenning *)
(* Code for Lecture 5: Datatypes and Trees *)
(***************************************************************************)
(* Declaring a new type called color, with three constant constructors: *)
datatype color = Red | Blue | Green
(* fiery : color -> bool
REQUIRES: true
ENSURES: fiery(C) returns true if C is the color Red, and false otherwise
*)
fun fiery (Red : color) : bool = true
| fiery _ = false
val true = fiery Red
val false = fiery Blue
val false = fiery Green
(***************************************************************************)
(* Declaring a new type called extint to model the extended integers: *)
datatype extint = NegInf | Finite of int | PosInf
(* The values of this type are
NegInf, PosInf, Finite(0), Finite(1), Finite(~1), etc.
Notice how the constructor "Finite" expects an argument, namely an int.
The name of the constructor can be anything, it doesn't have to be "Finite".
Similarly for "NegInf" and "PosInf".
We are creating a new datatype and can name the constructors anything
we want. In fact, in the noon lecture we wrote "Int", but that may
have confused some students, so the code here uses "Finite" instead.
*)
(*
What is the minimum of an empty set of integers? Positive Infinity!
Why? Because positive infinity is the identity element for minimization.
We can now readily implement such minimization, without using some default
integer as a hack to represent positive infinity. (Let the compiler do
that, not the human programmer.)
*)
(* minList : int list -> extint
REQUIRES: true
ENSURES: minList [x1, ..., xn] ==> min{x1, ..., xn} in the usual sense.
minList [] ==> PosInf
*)
fun minList ([] : int list) : extint = PosInf
| minList (x::xs) =
case minList xs of
PosInf => Finite(x)
| Finite(y) => Finite(Int.min(x,y))
| _ => raise Fail "this clause should be unreachable"
(* Int.min : int * int -> int is a predefined function in ML. *)
(* We don't really need the third clause in the case (prove that!),
but the compiler will give us a nonexhaustive warning, which we
can silence by adding the clause shown.
*)
val PosInf = minList []
val Finite(2) = minList [17, 2, 8]
(***************************************************************************)
(* A type of binary tree with empty leaves and integer values at internal nodes: *)
datatype tree = Empty | Node of tree * int * tree
val t1 : tree = Node(Empty, 1, Node(Empty, 2, Empty))
val t2 : tree = Node(Node(Empty, 3, Empty), 4, Empty)
val t12 : tree = Node(t1, 5, t2)
(* depth : tree -> int
REQUIRES: true
ENSURES: depth(t) ==> depth of tree t
*)
fun depth(Empty : tree) : int = 0
| depth(Node(l,x,r) : tree) : int = 1 + Int.max(depth l, depth r)
val 2 = depth t1
val 3 = depth t12
(* In class we used structural induction on trees to prove that
depth is total, i.e., that
depth(T) always reduces to a value when T is a value of type tree.
*)
(***************************************************************************)
(* Here is a different binary tree, in which data is stored at the leaves: *)
datatype tree = Leaf of int | Node of tree * tree
(* Sample: *)
val tr : tree = Node(Node(Node(Leaf 1, Leaf 2), Leaf 3), Leaf 4)
(* flatten : tree -> int list
REQUIRES: true
ENSURES: flatten(t) ==> L, with L consisting of the leaf values in t
as encountered in an inorder traversal of t.
*)
fun flatten (Leaf(x):tree) : int list = [x]
| flatten (Node(t1,t2)) = flatten t1 @ flatten t2
(* In the noon lecture, we named the next function "squash",
instead of "flatten2", per student request. *)
(* flatten2 : tree * int list -> int list
REQUIRES: true
ENSURES: flatten2(t, acc) == flatten(t) @ acc
*)
fun flatten2 (Leaf(x):tree, acc:int list) : int list = x::acc
| flatten2 (Node(t1,t2), acc) =
flatten2 (t1, flatten2 (t2, acc))
val [1,2,3,4,0,0,7] : int list = flatten2(tr, [0,0,7])
(* flatten' : tree -> int list
REQUIRES: true
ENSURES: flatten'(t) ==> L, with L consisting of the leaf values in t
as encountered in an inorder traversal of t.
*)
fun flatten' (t:tree) : int list = flatten2 (t, nil)
val [1,2,3,4] : int list = flatten tr
val [1,2,3,4] : int list = flatten' tr
(* Please see the pdf from lecture 4 for a proof that flatten2 satisfies
its ENSURES spec.
*)
(***************************************************************************)
(* Here is an operator/operand tree:
Leaves hold integers, internal nodes hold functions of
type int * int -> int.
*)
(* Example: The value computation below models the tree
*
/ \
/ \
max 4
/ \
/ \
3 7
*)
datatype optree = Oper of optree * (int * int -> int) * optree
| Val of int
val comp = Oper(Oper(Val 3, Int.max, Val 7), fn (x:int, y:int) => x*y , Val 4)
(* eval : optree -> int
REQUIRES: all the int*int->int functions in T are total.
ENSURES: eval(T) evaluates to the integer result of the computation
described by T (assuming a post-order type of traversal).
*)
fun eval (Val x : optree) : int = x
| eval (Oper(left, f, right)) = f (eval left, eval right)
val 28 = eval comp
(* Side comment:
We could also have defined comp as follows:
val comp = Oper(Oper(Val 3, Int.max, Val 7), op *, Val 4)
"op" is an SML keyword that expects an infix operator and
produces the underlying function.
For example, * is an infix binary operator.
In order to get the actual multiplication function
of type int*int->int, one writes ( op * ).
Similarly, for other infix operators.
So, for instance, (op +)(3,4) ==> 7.
*)
(***************************************************************************)
(* Here are some simple examples of int -> int functions that are not total: *)
fun fact (0 : int) : int = 1
| fact (n) = n * fact(n-1)
(* fact is not total because it loops forever when called on negative
integers. Even if we specify via a REQUIRES that a user should only
call fact on nonnegative integers, the function itself is not total.
*)
(* Both of the following functions loop forever on all integer arguments: *)
fun f (x : int) : int = f(x)
fun g (x : int) : int = g(x)*0
(* Notice that f and g have different bodies but that
f(x) is extensionally equivalent to g(x) for all integer arguments x,
since both functions loop forever on all arguments.
We thus say that f is extensionally equivalent to g.
*)
(* Observe also that
(fact 1) + (7 div 0) == (7 div 0) + (fact 1).
(Recall that in code files we write "==" to mean "extensionally equivalent".)
However,
(fact ~1) + (7 div 0)
is NOT extensionally equivalent to
(7 div 0) + (fact ~1).
Why is that?
*)
(***************************************************************************)