[ ^ to index ]

341 : 17 Jan 2002 : Scheme and functions

Function literals (a.k.a., anonymous functions)

All useful languages have a variety of "literal" values, such as integer literals (1, 2, etc.) and string literals ("hi", "bye"). Unlike C and other primitive languages, Scheme has a way of representing raw function "values", without names:

  ; Takes arg1 ... argN as arguments, and returns expr
  (lambda (arg1 arg2 ... argN) expr)

Function values can be passed around, just like any other values. Function values are applied just like variable names bound to functions---in fact both evaluate the same way:

  (lambda (x) (+ x 1))     ; The "increment" function

  ((lambda (x) (+ x 1)) 2) ; A roundabout way of adding 1 to 2: the first element
                           ; of this evaluates to a function, so we apply it to the
                           ; rest of the list

  (define increment        ; Of course, we can bind functions to names
    (lambda (x) (+ x 1)))  ; as well, for later use.

  (increment 2)            ; Equivalent to applying the lambda directly, as above.

Higher-order/first class functions

Functions can be passed as parameters and returned from other functions. This may sound mysterious and confusing at first, but it is only so because C has taught you to think of actions---that is, verbs---as "second-class". In other words, you can't treat functions in C the way you treat all other values. But a computer language without "first-class" functions is like a human language in which you can use verbs, but you cannot talk about verbs.

What do I mean? In English, you can say the following "first-order" sentences:

These sentences merely use verbs. But in English, verbs are first-class, so you can also have "higher-order" sentences that talk about other actions. Consider the following two exchanges:

In English, it is easy to recognize the common parts of the two conversations. The common pattern, or abstraction, of the two conversations is:

    "Cleaning up stuff means:
         `Take the stuff, and do something with it.'"

This sentence---"Take the stuff, and do something with it"---is an English "higher-order function". The "something" in "do something" is an English "function variable". Without higher-order functions, "do something with it" is hard to express. In Scheme, this thought is expressed as easily as it is expressed in English:

    (define clean-up-this-stuff
      (lambda (do-something list-of-stuff)
        (map do-something list-of-stuff)))

Well, okay, I'm cheating, because the "do something to all this stuff" function is really another name for "map". There is an important difference, of course: "map" leaves the old stuff lying around and produces a whole new set of stuff---whereas in the real world, doing your laundry does not generally involve producing a new copy of your clothes. (We'll come back to map later.)

Functions as return values

The previous example used a function parameter, "do-something". Functions can not only be passed as parameters, but also returned from other parameters. Again, this is very simple in English:

    "Go to the receptionist and give him your ID number.  He will give
    you a slip of paper with instructions on it.  Follow the instructions
    on the piece of paper."

The "instructions" the receptionist gives you are a "function" that he returns. You have been told to execute this function when you get it, but you do not know what it is until you actually receive the instructions. This is easy to express in Scheme as well:

    (define instructions (give receptionist id-number))
    (instructions) ; Evaluate instructions, with no arguments

map, filter, reduce

There are three higher-order functions which encapsulate many of the tasks that you will commonly want to perform on lists:

(map func alist)
"Apply func to every item in the list, and return a list of the results." Example:
  (map (lambda n (+ n 1)) '(7 0 4 2 9))
  ; => (8 1 5 3 10)
(filter test? alist)
"Apply test? to every item in the list, and return a list of all those items for which (test? item) is true." Example:
  (filter (lambda (n) (> n 5) '(7 0 4 2 9))
  ; => (7 9)
(reduce func base alist)
The most complex of these functions. "Set an accumulator equal to the base value. For each item in alist, apply func to the accumulator and the current item. Set the accumulator to the result and proceed to the next item." Example:
  (reduce + 0 '(7 0 4 2 9))
  ; => 22

  (reduce cons () '(7 0 4 2 9))
  ; => (7 0 2 4 9)

Some real code

    ; Adds n to each item of a list
    (define add-n
       (lambda (n list)
          (if (null? list)
              ()
              (cons (+ n (car list))
                    (add-n n (cdr list))))))

    (add-n 5 '(1 2 3 4))
    ; => (6 7 8 9)

    ; Adds n to each odd-numbered item of a list
    (define add-n-odd
       (lambda (n list)
          (if (null? list)
              ()
              (cons (if (odd? (car list))
                        (+ n (car list))
                        (car list))
                    (add-n-odd n (cdr list)))))) 

    (add-n-odd 5 '(1 2 3 4))
    ; => (6 2 8 4)

    ; A minor generalization of the above?
    (define add-n-if
       (lambda (n test? list)
          (if (null? list)
              ()
              (cons (if (test? (car list))
                        (+ n (car list))
                        (car list))
                    (add-n-if n test? (cdr list))))))

    (add-n-if 5 odd? '(1 2 3 4))
    ; => (6 2 8 4)

    (add-n-if 5
              (lambda (x) (= (+ x 2) 6))
              '(1 2 3 4))
    ; => (1 2 3 9)

    ; A more useful generalization
    (define map-if
       (lambda (fn test? list)
          (if (null? list)
              ()
              (cons (if (test? (car list))
                        (fn (car list))
                        (car list))
                    (map-if fn test? (cdr list))))))

    (map-if (lambda (x) (+ x 5))
            (lambda (x) (and (odd? x) (> x 2)))
            '(1 2 3 4))
    ; => (1 2 8 4)

    ; A shorter way of writing map-if, using built-in map
    (define map-if
      (lambda (fn test? list)
        (map (lambda (item)
               (if (test item) (fn item) item)
             list))))

In general, anywhere you see a function doing "actual work", that's often an opportunity for abstraction and function parameters.

Exercises

[ ^ to index ]

cse341-webmaster@cs.washington.edu
Last modified: Wed Jan 23 17:09:28 PST 2002