Functional programming is a style that emphasizes:
Secondary characteristics (usually, but not always part of FP) include:
All language constructs (in a statically typed language) have three parts:
Stating these three properties gives you the essence of the language construct. When language designers design a language, they go through these three steps for each construct --- either consciously or unconsciously.
In these notes, we will do two examples informally, to show this formula in action.
if/then/else
in a NutshellSyntax:: If/then/else expressions have the syntactic form:
if expr1 then expr2 else expr3
Dynamic semantics: First,
expr1
is evaluated to a value
v
. If v
has the boolean
value true
, then expr2 is evaluated and
returned. Otherwise, v
has the boolean value
false
, and expr3 is evaluated and
returned.
Static semantics:
expr1
must have
the type bool
. It must be possible to unify
expr2
and expr3
to the
same type; call this type T
.T
There are two constructs related to functions: definition (sometimes called abstraction in the programming languages literature) and application (a.k.a. function call).
Syntax: Function definitions have the syntactic form
fn pattern => returnValue
Dynamic semantics: A function definition constructs a closure that contains two parts:
Static semantics:
argType -> returnType
, where
argType and returnType are inferred from the
argument pattern and body.Syntax: Function applications have the syntactic form
expr1 expr2
Dynamic semantics: First, expr1 is evaluated to a value value1. Second, expr2 is evaluated to a value value2. Then, value1's function body is evaluated in the environment produced by matching value2 against the argument pattern.
Static semantics:
expr1
** With polymorphic type variables appropriately instantiated. We'll learn about polymorphic types in the next couple of lectures.
The answers to the following exercises are available here.
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);
val (_, b) = (("hi", "bye"), fn x => x + 1);
val {a=a, b=b} = ({a="hi", b="bye"}, fn x => x + 1)
val (x:char)::xs = ["a","b","c"];
val x::y::z = ["a","b","c"];
val x::y::z = ["a","b"];
val x::y::z = ["a"];
val x::y::(z:string list)::zz = ["a", "b", "c"];
val (a:int->int, b) = (fn x => x + 1, fn x => x ^ "1");
val (a, b) = (fn x => x + 1, {foo=fn x => x ^ "1", bar=fn x => x * x});
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 factorial 0 = 1 | factorial n = n * factorial (n-1);
fun joinStrings nil = "" | joinStrings (x::xs) = x ^ joinStrings xs;
fun countDown 0 = [0] | countDown n = n::(countDown (n-1));
fun countUp 0 = [0] | countUp n = countUp (n-1) @ [n];
val
binding. (You do not have to describe
pattern matching --- assume this has been defined.)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.* 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.