Which of the following pattern-matches fail? Which succeed? For successful matches, draw a diagram of the bindings that result, and annotate each name binding with its type. For unsuccessful matches, describe briefly the reason for the failure.
val (a, _) = (("hi", "bye"), fn x => x + 1);
Succeeds.
val (_, b) = (("hi", "bye"), fn x => x + 1);
Succeeds.
val {a=a, b=b} = ({a="hi", b="bye"}, fn x => x + 1)
Fails with a static type error. The
pattern is a record with {a, b}
fields; the
value is a tuple whose first element is a record
with {a, b}
fields.
val (x:char)::xs = ["a","b","c"];
Fails with a static type error. The
pattern is a cons whose head has the ascribed type
char
; therefore, the type of the entire cons
pattern is char list
. The values is a cons of
type string list
.
val x::y::z = ["a","b","c"];
Succeeds. The pattern is a two-level
cons; this can be matched against the list expression, which
has at least two cons cells (actually it has three:
"a"::"b"::"c"::nil
).
val x::y::z = ["a","b"];
Succeeds. The pattern has two conses, and
the value has two conses.
val x::y::z = ["a"];
Fails with a dynamic match error. The
left-hand (top-level) cons pattern can match, but the
right-hand (inner) cons pattern fails against
nil
. Note that, unlike the previous two
failures, this pattern match is statically well-typed ---
the failure only occurs when value nil
is
dynamically matched against the cons pattern.
val x::y::(z:string list)::zz = ["a", "b", "c"];
Fails with a static type error. Cons
associates right-to-left, so z
appears on the
left-hand-side of a cons operator. Therefore, the ascribed
type of z
(string list
must be the
element type of the whole list pattern. The entire
list pattern must therefore have type string list
list
(list of string lists). The value on the
right-hand side is only string list
.
Therefore, the declaration does not type check, and is
statically rejected.
val (a:int->int, b) = (fn x => x + 1, fn x => x ^ "1");
Succeeds.
val (a, b) = (fn x => x + 1, {foo=fn x => x ^ "1", bar=fn x => x * x});
Succeeds.
For each of the following recursive functions, state briefly why it isn't properly tail-recursive, and then write a tail-recursive version.
fun sumN 0 = 0 | sumN n = n + sumN (n-1);
fun sumN n = let fun helper (sum, 0) = sum | helper (sum, n) = helper (sum+n, n-1) in helper (0, n) end;
fun factorial 0 = 1 | factorial n = n * factorial (n-1);
fun factorial n = let fun helper (m, 0) = m | helper (m, n) = helper (m*n, n-1) in helper (1, n) end;
fun joinStrings nil = "" | joinStrings (x::xs) = x ^ joinStrings xs;
fun joinStrings aList = let fun helper (joined, nil) = joined | helper (joined, x::xs) = helper (joined ^ x, xs) in helper ("", aList) end;
fun countDown 0 = [0] | countDown n = n::(countDown (n-1));
fun countDown n = let fun helper (counted, 0) = 0::counted | helper (counted, n) = helper (n::counted, n-1) fun reverse (reversed, nil) = reversed | reverse (reversed, x::xs) = reverse (x::reversed, xs); in reverse ([], helper ([], n)) end;
fun countUp 0 = [0] | countUp n = countUp (n-1) @ [n];
fun countUp n = let fun helper (counted, 0) = 0::counted | helper (counted, n) = helper (n::counted, n-1) in helper ([], n) end;
Syntax: expr1
+ expr2
.
Dynamic semantics: Evaluate
expr1
to a value v1
.
Evaluate expr2
to a value
v2
. The result of the expression is the
integer addition of v1
and
v2
, unless the result overflows, in which
case an exception is raised.
Static semantics:
int
.Syntax: expr1
:: expr2
.
Dynamic semantics: Evaluate
expr1
to a value v1
.
Evaluate expr2
to a value
v2
. The result of the expression is a
fresh cons cell whose head points to v1
and whose tail points to v2
.
Static semantics:
list
.list
.val
binding. (You do not have to describe
pattern matching --- assume this has been defined.)
Syntax: val pattern
= expr
.
Dynamic semantics: Evaluate
expr
to a value v
.
Then, attempt to match v
against
pattern
, yielding zero or more bindings to
variables { x1, x2, ... xN }. If the match succeeds,
add the bindings to the current environment stack. If the
match fails, add no bindings, but raise an exception.
Static semantics:
(You might complain that some of this definition amounts to hand-waving; and you would be right. Defining the precise semantics of name binding in a formal fashion requires some moderate complexity. For this class, an informal description such as this one will suffice.)
let
expressions. This can be defined two
ways --- as a "primitive" construct, from the ground up, or as
a syntactic sugar for a series of function applications.**
Define it both ways.
Syntax: let
declList in expr end
.
Dynamic semantics: Evaluate
each declaration in declList
in sequence,
producing zero or more bindings to be added to the current
environment. Then, evaluate expr
in the
current environment.
Static semantics:
Syntax: let
declList in expr end
.
Dynamic semantics: First,
rewrite the term as a series of nested let
expressions, with only one declaration per
let
, as follows:
let decl in (let decl in ... in
expr end ... end) end)
.
Then, rewrite each let-expression let val pattern
= expr1 in expr2 end
as a function call
((fn pattern => expr2) expr1).
Evaluate the resulting expression.
Static semantics: Perform the rewriting specified in the dynamic semantics, except for the evaluation step. Typecheck the resulting rewritten expression.
* For most of these constructs, the static
semantics for full ML uses polymorphic types. For now pretend
ML only has monomorphic types like int
,
string
, or (int * int * int)
.
** Actually, there are slightly different
typechecking requirements between function application and
let
, but the distinctions are beyond the scope of
your current knowledge.