Comp210 Lecture # 20    Spring 2003

I rode through the listing on a func with no name ...

This section is a bit out of order to insure that we get to it before the end of class. By rights, it should come at the end of this lecture, not at the beginning.

Let's take another look at the code for filtering that we saw in the last lecture:

;; filterAbyB: list-of-num list-of-any --> list-of-num-and-any
;; replaces any element in listB according to the gate function, 
;; which depends on the corresponding element in lonA.
;; The gateFn must have the following abstract contract:
;; gateFn: num --> function: symbol --> symbol
(define (filterAbyB lonA listB gateFn)
    (cond
        [(empty? lonA) empty]
        [(empty? listB) empty]
        [(cons? lonA)  (cons ((gateFn (first lonA)) (first listB)) 
                             (filterAbyB (rest lonA) (rest listB) gateFn))]))

;; stepFn: num --> function: sym --> sym
;; returns a no-op function if a > 0
;; returns a function that returns '_ otherwise.
(define (stepFn a)
    (local 
        [(define (f1 s) '_)
         (define (f2 s) s)]
        (cond
            [(>= 0 a) f1]
            [else f2])))


"filterAbyB test case, stepFn:"
(equal? (list 'A 'B '_ 'D '_ '_ 'G)
        (filterAbyB (list 1 4 -2 3 -1 -3 2) (list 'A 'B 'C 'D 'E 'F 'G) stepFn))

In particular, let's look at the stepFn function. Internally, the functions f1 and f2 are defined. But they are only used once and in fact, they are never executed by stepFn. And filterAbyB doesn't call them by name either. So the names "f1" and "f2" are used exactly once. Why should we bother naming them in the first place?

What we need is an "anonymous" definition of a function--that is, a mechanism for defining a function without giving it a name. Scheme provides the lambda operator:

Lambda

Provides for the anonymous definition of a function

Syntax:

(lambda (arg1 arg2   ...) 
    expression)

The easiest way to understand lambda is to compare it to a define statement. The following two statements are equivalent:

(define (f arg1 arg2 ...)
    expression)
(define f (lambda (arg1 arg2 ...)
    expression))

In fact, theoretically speaking, this is exactly what define does. Thus, lambda does not follow regular Scheme evaluation rules, instead it follows the same rules as define.

Functions created using the lambda operator are called lambda functions.

It can be shown that everything computable can be expressed in terms of lambdas. The mathematics to do this, called lambda calculus, was invented by Alonzo Church (1903-1995) in 1936. The Greek letter lambda that is on the DrScheme logo is a reference to lambda functions.

So let's see stepFn in terms of lambdas:

(define (stepFn a)
    (cond
        [(>= 0 a) (lambda (s)
                      '_)]
        [else (lambda (s)
                  s)]))

Lambdas enable us to cut our code down to its very essence.

Likewise, signFn from last lecture can be re-written using lambdas:

;;signFn: num --> function: sym --> sym
;; returns a function that prepends a "+" to the symbol if a >0
;; otherwise returns a function that prepends a "-" to the symbol.
(define (signFn a)
    (cond
        [(>= 0 a) (lambda (s) 
                      (string->symbol (string-append "-" (symbol->string s))))]
        [else (lambda (s) 
                  (string->symbol (string-append "+" (symbol->string s))))]))

"filterAbyB test case, signFn:"
(equal? (list '+A '+B '-C '+D '-E '-F '+G)
        (filterAbyB (list 1 4 -2 3 -1 -3 2) (list 'A 'B 'C 'D 'E 'F 'G) signFn))

Definitely much more direct and to-the-point than before.

The above code can be downloaded here: lambda.scm

Since a lambda function is the same as one created using the define operator, everything that follows about closures also applies to lambda functions.

Closures, continued

Coinsider the following code:

(define x0 5)

(define (g1 x)
    (local [(define x0 10)]
           (* x x0)))

(g1 2)

What is the result of (g1 2)?

The argument goes something like this:

  1. x0 is in scope inside of g1.
  2. But x0 is redefined inside of g1.
  3. Thus the new value of x0 overrides (shadows, masks) the original value of x0.
  4. The computation, (* x x0), thus uses the overriden value of x0.
  5. (g1 2) means that x is 2 inside of g1.
  6. Therefore, (= (g1 2) (* x x0) (* 2 10) 20)

And it's even right...woohoo!

Ok, but now suppose we want to get get fancier and instead of this internally defined function, I want to define that same function externally, like this:

(define (f x)
    (* x x0))

And my original g1 function simplifies down to

(define (g2 x)
    (local [(define x0 10)]
           (f x)))

So, for a "sanity check" we run the same test as before to make sure everything's still working.

(g2 2)

And the answer comes back....10.

We even try moving the definition of f to after the definition of g2, and the answer still comes back...10.

Our sanity check has resulted in "Yeah, we're crazy". What happened here??

 

In the deep dark recesses of the past, like two days ago, we mentioned closures.

Every function has a closure, which includes its own code plus the environment in which it was created, at the time it was created.

When the function f was created, x0 was in scope. But that x0 was the globally defined one, which has a value of 5.

When a function is executed, it runs in its own closure. (For those who care, this is called "lexical scoping")

The internally defined x0 inside of g2 is not in the closure of f and thus is not used by it.

The answer is thus (= (g2 2) (f 2) (* x x0) (* 2 5) 10)

Curiouser and curiouser...

For the record, we've been dealing with higher order functions:

Higher Order Functions

Higher order functions are functions that either take functions as input parameters or return functions as their output.

 

The code in the following section will be shown both using and not using lambda functions. In all cases the versions are functionally identical. Be sure you understand both versions!

Let's investigate the behaviors of functions returned by other functions. Suppose we had the following code that returns an internally defined function:

(define (make-f1)
    (local
        [(define x0 10)
         (define (internal-f x)
             (* x x0))]
        internal-f))  ;; notice that the function itself is being returned!

Or, in terms of lambdas:

(define (make-f1-lambda)
    (local
        [(define x0 10)]
        (lambda (x)
            (* x x0))))

((make-f1) 2)
or
((make-f1-lambda) 2)

And the answer is....20 (in both cases). Why? For the same reason as before: The function, originally defined as internal-f (and the lambda function) has a closure wherein x0 is overriden with the value of 10. Even if it is executed outside of where it was made, where x0 is not overridden, it retains its closure, where x0=10.

One more because we're having so much fun:

Here's one where x0 is not defined anywhere per se, but rather is an input parameter.

(define (make-f2 x0)
    (local
        [(define (internal-f x)
             (* x x0))]
        internal-f))

Or, in terms of lambdas:

(define (make-f2-lambda x0)
    (lambda (x)
        (* x x0)))


((make-f2 10) 2)
or
((make-f2-lambda 10) 2)

The result is the same as before, 20, and for the same reason: the input parameter x0 is in the closure of internal-f and hence internal-f (and the lambda function) uses its overriden value of 10.

No problem.

So, what happens if we call make-f2 (or make-f2-lambda) again -- with a different value of x0? Like this:

(define f2a (make-f2 100))
(define f2b (make-f2 1000))

or

(define f2a-lambda (make-f2-lambda 100))
(define f2b-lambda (make-f2-lambda 1000))

Now the value of x0 has changed. What will the result be when f2a and f2b are finally run?

(f2a 2)
(f2b 2) or (f2a-lambda 2)
(f2b-lambda 2)

The answers are 200 and 2000 respectively in both the lambda and non-lambda versions.

Whoa Nellie! What about the value of x0? Wasn't it 1000 the last time we saw it?

Remember that a function retains in its closure, the environment at the time the function was created.

  1. Each invocation of make-f2 creates a new environment with x0 in it.
  2. Each time, x0 had a different value.
  3. Each time, internal-f (or the lambda function) was defined anew.
  4. Each time, the internal-f (or the lambda function) was bound with its closure, which included that invocation's value for x0.
  5. f2a and f2b (or f2a-lambda and f2b-lambda ) thus have different values for x0.
  6. Thus, f2a and f2b (or f2a-lambda and f2b-lambda ) are not technically the same function and thus behave differently, hence the different results.

Is it me, or is the room spinning?

 

The above function, make-f2 is important enough that it actually has a name. make-f2 is a curried function.

Curried functions are of the form (in regular algebraic notation):

h(a, b) = (g(a))( b) = f(b)

A function of two input variables, h, is expressed in terms of a function of 1 variable, g, that returns a function of one input variable, f.

In Scheme terms, the curried function make-f2 can be described as:

(equal? (f2a x) ((make-f2 100) x))

or

(equal? (f2a-lambda x) ((make-f2-lambda 100) x))

Here, 100 is in the role of the input parameter a.

Official definition:

Curried Function

A curried function is a function of N input parameters that can be considered to be a function of 1 variable that returns a function of N-1 variables.

Named after the logician Haskell Curry (1900-1982) who was instrumental in developing combinatorial logic who proposed it in 1958. The functional programming language "Haskell" is named after him. Curried functions were actually first proposed in 1893 by Friedrich Ludwig Gottlob Frege (1848-1925) and published by Moses Schönfinkel in1924. Oh well, c'est la vie! If it makes you feel better, call the process schönfinkeling as some people do.

Notice that the lambda function version, make-f2-lambda really expresses just the pure essense of currying:

(define (make-f2-lambda x0)
    (lambda (x) (* x x0)))

Is that sweet or what? This is the form we will be using most often. Any wonder why?

This material will take some time to sink in. Sleep on it. Go over the examples with a fine tooth comb. Make up pathological examples with your friends. Think about what the implications are for making functions whose behaviors are determined at run-time, not at design time. Whoa, dude! We're talking some serious power here!

Download the above code here: closure1.scm

 

 

©2003 Stephen Wong