Comp210: Principles of Computing and Programming
Fall 2004 -- Lecture #19   


More local

Note: A function implictly creates a local, where the input parameters are locally scoped.

Using local can save one from unnecessarily passing parameters. Consider the following function to create a list of n multiples of a given number.

;; multiples0: natNum num --> list-of-num

;; creates a list with the first n multiples of x

(define (multiples0 n x)

     (cond [(zero? n) empty]

           [(< 0 n) (cons (* x n) (multiples0 (sub1 n) x))]))



"multiples0 test case:"

(equal? empty (multiples0 0 2))

(equal? (list 10 8 6 4 2)(multiples0 5 2))

Notice how x is passed along with each recursive call just because each time the function is called, it has no idea what the value of x in the caller was.

As per the First Law of Programming, we tend to think of the process as "we know what x is, so we just multiply it by different values of n." We don't tend to think of it in terms of passing the value of x every time we switch to a new value of n..

So let's try to code this up as we think of it:

;; multiples: natNum num --> list-of-num

;; creates a list with the first n multiples of x

(define (multiples n x)

    (local [(define (helper n)

                (cond [(zero? n) empty]

                      [(< 0 n) (cons (* x n) (helper (sub1 n)))]))]

           (helper n)))



"multiples test case:"

(equal? empty (multiples 0 2))

(equal? (list 10 8 6 4 2)(multiples 5 2))

The helper function only has one input parameter, the counter n. Since x is in scope when helper is defined, it can use x's value without having to pass it as an input parameter.

Here's another example using local to eliminate the passing of an invariant input parameter: divisorsof.scm

Big word time:

Closure

A function's code and the environment in which it was defined consitute that function's closure. The environment includes all variables that are in scope and their values at the time the function is defined.

A function retains its closure even when it is used in a different enviroment.

A very useful way to think about the behavior of the helper function above is that x is in its closure and thus helper always knows x's value and can thus use it directly.

Understanding a function's closure will be increasingly important as we work more and more with abstracted functions. Using the closure effectively will make many things possible for us as the semester progresses.

The above examples of eliminating the passing of invariant input parameters relies on the fact that the outside function creates a closure where the input parameter is in scope. Thus the outer function's input parameters are withing scope in the inner, helper function's closure. Therefore, the inner function can access those parameters since they are not masked by anything.

 

Functions as Objects?

A function is just a bunch of code data in memory. A structure is just a bunch of data in memory. You can pass structures as input parameters to functions. Why not be able to pass functions too?

In Scheme, functions are what are known as "first-class objects" which means that they can be treated the same as any other data object in the system, such as structures and values, which are also first-class objects. This means that they can be passed as input parameters to functions, they can be returned as the output of a function and that placeholderscan be assigned to them.

Functions that take functions as inputs

Consider the following function:

;; op: function any any --> any
;; evaluates the given function, f, with the remaining two input parameters, x & y.
;; The f must have the following abstract contract:
;; f: any any --> any (define (op f x y) (f x y)) "op test cases:" (= 5 (op + 3 2)) (= 1.5 (op / 3 2)) (boolean=? true (op > 3 2)) (string=? "Comp210 just gets more nifty each day!" (op string-append "Comp210 just gets " "more nifty each day!"))\

op takes a function as an input parameter and then executes that function.

Note that op must also specify an "abstract contract" for the supplied function. op doesn't know what the function it will be handed, but can say that whatever that function is, it must have a specific contract or it won't work. The contract is abstract because it doesn't apply to any specific function, just any function that might be handed to op.

Here's another, but just as silly, example:

;; isBetterThan:  function num num --> string
;; Uses the function comp to determine if the x is "better than" y.
;; Returns a string with its answer.
;; The supplied function must adhere to this abstract contract:
;; comp: num num --> boolean
(define (isBetterThan comp x y) (cond [(comp x y) (string-append (number->string x) " is better than " (number->string y))] [else (string-append (number->string y) " is better than " (number->string x))])) "isBetterThan tests:" (isBetterThan > 4 5) (isBetterThan < 4 5)

Here the isBetterThan function uses the supplied function to make an internal decision. Once again, we see that an abstract contract must be specified.

Of course, we can write our own function and pass it as an input parameter:

;; randComp: num num --> boolean

;; randomly compares x and y

(define (randComp x y)

    (< (* x (random 100)) (* y (random 100))))



"randComp test cases:"

(isBetterThan randComp 4 5) 

(isBetterThan randComp 4 5) 

(isBetterThan randComp 4 5) 

(isBetterThan randComp 4 5) 

(isBetterThan randComp 4 5) 

(isBetterThan randComp 4 5) 

Well, this is all well and cool, but what does it all mean? Why are we bothering with this?

The thing to notice is that the behavior of op and isBetterThan changed, depending on what function was passed to it. Even though their behavior drastically changed, their code was the same.

Creating abstraction processing using function variables creates abstract, invariant code.

What we did above was to extract just the variant behaviors and encapsulate them in an abstract contract. The remaining code is invariant. The variant code is much simpler and contains pure variant behaviors without any invariant "baggage."

This is all very much in keeping with the Second Law of Programming.

Decoupling the variant and invariant code by separating them into different functions helps us create reusable code with a single point of control. It also keeps different code from stomping on each other because the amount of overlap between what any functions do is minimized.

This function abstraction is one of our most powerful tools in representing abstractions in our code.

 

Functions that return functions

Consider the following function that filters out symbols in one list based on positive vs. negative values in another list:

;; filterAbyB_bad: list-of-num list-of-sym --> list-of-sym

;; replaces any element in listB with '_ if the corresponding 

;; value in lonA is negative.

(define (filterAbyB_bad lonA listB)

    (cond

        [(empty? lonA) empty]

        [(empty? listB) empty]

        [(cons? lonA) 

         (cond [(>= 0 (first lonA)) 

                  (cons '_ 

                        (filterAbyB_bad (rest lonA) (rest listB)))]

               [else 

                  (cons (first listB) 

                        (filterAbyB_bad (rest lonA) (rest listB)))])]))



"filterAbyB_bad test cases:"

(equal? (list 'A 'B '_ 'D '_ '_ 'G)

        (filterAbyB_bad (list 1 4 -2 3 -1 -3 2) (list 'A 'B 'C 'D 'E 'F 'G)))

Obviously this function works just fine, but what if we wanted to change the way it filtered? -- without losing this function, of course --We'd have to write a whole new function, changing just a small piece of the original code because this function mixes variant and invariant code together.

Warning Will Robinson!! Severe lack of abstraction encountered! Warning! Warning!

Let's try that again, shall we? But this time, let's focus on the fact that we really don't care how the filtering is being done, just that it is and what is done with (first listB) depends on (first lonA):

;; filterAbyB: list-of-num list-of-any (function: num --> (function: symbol --> sym)) --> 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))

filterAbyB is now pure invariant code which does not depend on what filtering is being done. The filtering decision has been abstracted out into an abstract function contract, and expressed in terms the function stepFn. Note in particular, that the second cond statement in filterAbyB_bad is gone--it never had anything to do with processing the two lists. It's now in the stepFn where it belongs.

And now if we want to change the filtering, we don't touch filterAbyB, instead we merely write a new gate function:

;;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)

    (local 

        [(define (f1 s) (string->symbol (string-append "-" (symbol->string s))))

         (define (f2 s) (string->symbol (string-append "+" (symbol->string s))))]

        (cond

            [(>= 0 a) f1]

            [else f2])))



"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))

Once again, we have decoupled the variant code from the invariant code.

Life is good again.

Pushing the Envelope...

So, let's mess that our pretty picture of the computing universe--let's try something a little more sophisticated:

Can we extend the op function above so that it works on lists?

;; opList: function non-empty-list-of-numbers --> number

;; recursively applies func to every element of a list and the 

;; accumulated result for the list.

;; func must have the following contract:

;; func:  num num --> num

;; and header:

;; (func first acc)

;; where first is the data in first and acc is the accumulated

;; result so far.

(define (opList func nelon)

    (local

        [(define (helper lon acc)

             (cond

                 [(empty? lon) acc]

                 [(cons? lon) (helper (rest lon) 

                                      (func (first lon) acc))]))]

        (cond

            [(cons? nelon) (helper (rest nelon) (first nelon))])))



"opList test cases:"
(= 15 (opList + (list 1 2 3 4 5)))
(= 120(opList * (list 1 2 3 4 5)))

Can we write our own input functions? Of course we can:

;;sumSq:  num num --> num 
;;sums the acc with the square of x
(define (sumSq x acc) (+ acc (* x x))) ;;getMin: num num --> num
;; the result is the smaller of acc and x.
(define (getMin x acc) (cond [(< x acc) x] [else acc])) "more opList test cases:" (= 55 (opList sumSq (list 1 2 3 4 5))) (= 1 (opList getMin (list 5 3 6 1 4 2)))

Works like a charm -- well, almost. It is restricted to non-empty lists and there are some problems with the empty case because it is restricted to returnin the accumulator.

Let's give this another try, by adding a more general handling of the empty (base) and cons (inductive) cases:

;; opList2:  function function list-of-any --> any

;; f0 is executed on the base case.

;; abstract contract for f0:

;; f0: --> any

;; f1 is executed on the inductive case, where the first input is (first a-list)

;; and the second input is (opList2 f0 f1 (rest a-list))

;; abstract contract for f1:

;; f1: num lon --> any

(define (opList2 f0 f1 a-list)

    (cond

        [(empty? a-list) (f0)]

        [(cons? a-list) (f1 (first a-list) (opList2 f0 f1 (rest a-list)))]))

So we now need to define two functions: one for the base case and one for the inductive case:

;;orderedInsert: num lon  --> lon

;; does an ordered insert of x into the presumed ordered lon.

(define (orderedInsert x lon)

    (cond

        [(empty? lon) (cons x empty)]

        [(cons? lon) 

         (cond

             [(< x (first lon)) (cons x (cons (first lon) (rest lon)))]

             [else (cons (first lon) (orderedInsert x (rest lon)))])]))





;; orderedInsert0: --> empty

;; The base case

(define (orderedInsert0)

    empty)

Put these the above base case and inductive case functions together with our opList2 "framework" and Presto! Voila! we can sort a list of numbers:

"opList2 test case, orderedInsert:"

(equal? (list 1 2 3 4 5 6 7) 

        (opList2 orderedInsert0 orderedInsert (list 4 2 7 3 5 6 1)))

We still can't use opList2 for every processing we want done on a list, but we're a lot closer now.

Can you see what is going on here? If we push the abstraction far enough and manage to isolate just the pure invariant code needed to process a list, what will we end up with?

We haven't got all the technologies yet to get the encapsulations correct, so

STAY TUNED!!

Download today's code: oplist.scm

 


Last Revised Tuesday, 24-Aug-2004 13:49:03 CDT

©2004 Stephen Wong and Dung Nguyen