15-150: Principles of Functional Programming

Lecture 2: Functions

We discussed functions today.

Functions are values.
A function value consists of an anonymous lambda expression along with a (possibly empty) environment of bindings for any nonlocal variables that appear in the body of the function. The combination of a lambda expression and an environment is called a closure.

Example 1: The anonymous lambda expression (fn (x:int) => x*x) is a function that squares its argument. (There are no nonlocal variables in the body of this function, so we don't need to specify an environment.)
Example 2: [3.14159265358979/pi](fn (r:real) => pi*r*r) is an environment and an anonymous function that together provide a function for computing the area of a disk of radius r.

NOTE carefully: A lambda expression (plus any bindings of nonlocal variables) is a value. A value is a value; one cannot reduce a value further.
So one would not say that [2/s, 3/t](fn (x:int) => (s+t)*x) ==> (fn (x:int) => 5*x).
However, it is true that these two function values are extensionally equivalent, i.e., [2/s, 3/t](fn (x:int) => (s+t)*x) ≅ (fn (x:int) => 5*x).

Also: When SML prints a function value in the REPL, it will merely print   fn   (plus the type of the function), not all the internal details of the closure. Only in proofs or similar reasoning do we use the written representation shown above. And of course, you can't directly write a closure inline in code. If you want bindings in the environment, you have to create those using declarations. However, you can write anonymous lambda expressions inline in your code, as in (fn (x:int) => x*x)(7), for instance.

We discussed function application. In order to evaluate   e1 e2:

  1. Reduce e1 to a function value f of the form fn(x:t)=>e.
  2. Reduce e2 to a value v.
  3. Locally extend the environment that existed at the time of definition of f with a binding of value v to the variable x.
  4. Evaluate the body of f in the resulting environment.
    Notationally, we might write [v/x]e for this evaluation, with the relevant prior environment clear from lexical scoping.
(Once evaluation of  e1 e2   completes, the environment is again the same environment as at the beginning of evaluation (assuming no mutation).)

Example:   The declaration
          fun square (x:int) : int = x * x
produces a binding of a function value to the variable square. Then:
      ==> (fn(x:int) => x*x)(3+4)
      ==> (fn(x:int) => x*x)(7)
      ==> [7/x]x*x
      ==> 7*7 
      ==> 49

See also again the evaluation notes from the previous lecture.

Side comment: In class we thought of a function closure as consisting of a lambda expression and the environment in effect at the time of definition of the function. That is slightly different than the definition given above, since the environment at the time of definition may contain more bindings than are necessary to resolve the values of any nonlocal variables appearing in the body of the function. However, the two definitions amount to the same thing operationally, since those extra bindings never matter.

We introduced the 5-step methodology for writing functions:

  1. In the first line of comments, write the name and type of the function.
  2. In the second line of comments, specify via a REQUIRES clause any assumptions about the arguments passed to the function.
  3. In the third line of comments, specify via an ENSURES clause what the function computes (what it returns).
  4. Implement the function.
  5. Provide testcases, generally in a format like this: val 6 = fact(3)

We introduced case expressions.

We introduced clausal function definitions based on pattern matching.

Key Concepts

We also started in on next lecture's topic (induction) by proving that the our implementation of fact always returns a value for nonnegative arguments.

Sample Code