New Language

We are going to be using Intermediate Stundet with lambda (ISL) as the language in DrRacket from this point onwards in the semester.

Local

One of the features in ISL is local. local is an expression that

  • allows us to specify definitions, and,

  • specify an expression that has access to these definitions

The definitions created inside a local expression are only accessible from the expression inside local. No other expression outside of local has access to these definitions.

local syntax

The syntax for local can be split into three parts

(local (<DEFINITION> ...)
 <EXPR>)

where

  • <DEFINITION> can be

    1. a variable definition, (define WIDTH 100)

    2. a structure definition, (define-struct pair (first second))

    3. a function definition, (define (helper x) (+ x 1))

  • <EXPR> is an expression in the ISL

local then allows <EXPR> to use variables and functions that are

  1. defined globally in the file

  2. defined in local 's <DEFINITION>

Let’s see a simple example,

(define X 10)  (1)

(local ((define Y 20) (2)
        (define Z 30)) (3)
 (+ X Y Z)) (4)
1 A global variable X.
2 A local variable Y.
3 Another local variable Z.
4 The expression (+ X Y Z) can access global variables, X, as well as local variables Y and Z

The result of the preceding expression is 60.

So local allows us to create definitions that are accessible only to an expression—​the expression that is part of local.

To show that Y and Z are only accessible inside `local’s expression try the following in DrRacket

(define X 10)

(local ((define Y 20)
        (define Z 30))
 (+ X Y Z))

(+ X Y Z)

You will see that DrRacket complains that Y is not defined and highlights the Y found in the expression found in the last line.

Scope

For every definition (variable, structure or function) we use the word scope to denote the expression(s) that the definition is accessible. For example

(define X 10)

(local ((define Y 20)
        (define Z 30))
 (+ X Y Z))
  • X is a variable and it’s scope is the whole file. We also say that X has global scope.

  • Y and Z are variables and their scope is the expression (+ X Y Z). We also say that Y and Z have local scope.

For any expression in our source code, we can determine the value of a variable/function by inspecting our code and paying attention to any local expressions (which can be nested).

The process is rather simple

  1. Start at the expression that uses the variable.

  2. Start moving from the expression outwards, to find a define for the variable/function that you are looking for

    1. if we are in a nested expression, outwards means move to the enclosing expression

    2. if we are not in a nested expression then look at the files global definitions

These scoping rules in ISL is called lexical scoping (or static scoping).

Using our running example if we are looking at (+ X Y Z) and we want to find the value bound to X

  1. the expression (+ X Y Z) is nested inside a local. Check if X is defined as part of local. It is not so we move outwards

  2. moving outside the local expression we are no longer in a nested expression. So we check our global defines and we see that there is a global definition for X. That is the value for X in our original expression (+ X Y Z)

Let’s do that again for Y

  1. the expression (+ X Y Z) is nested inside a local. Check if Y is defined as part of local. It is so we use the value 20 for Y

Click on DrRacket’s Check Syntax button. This will process your code in the definitions window and once it is done and there are no errors move your mouse over a variable name and arrows appear to indicate where it is being used.

Shadowing

So if we can create local definitions with local then what happens if we create a local definition that has the same variable/function name as another defintion (either global or an enclosing, nested, local definition?

The rules on scoping remain the same, we keep looking outward until we find the first definition for our variable/function name. This means that the inner most define wins! Let’s see this in action

(define X 10)

(local ((define Y 20)
        (define X 100))
 (local ((define Z 30))
  (+ X Y Z)))

Let’s use our process to figure out the result of this example

  1. starting at (+ X Y Z) we first need to figure out the value for X. So we move outward.

  2. The immediately enclosing local defines Z but not X. So we keep moving outward.

  3. The next enclosing expression is a local and it defines X and Y. X is defined as 100 so we replace X with 100

  4. Now we need to repeat the process for Y and Z

  5. Y is defined in the outermost local expression as 20

  6. Z is defined in the innermost local expression as 30

  7. so the expression (+ X Y Z) becomes (+ 100 20 30) which evaluates to 150

What this code example shows is a case of variable shadowing, the outermost local creates a new definition for X and binds it to 100. This local definition shadows the global definition of X that binds X to 10. Notice that the code does not update the global definition of X. Instead it creates two definitions for X with one definition taking precedence (or shadowing) the other. Our scoping rules dictate which definition is visible at each expression.

local definitions order of evaluation

It is useful sometimes to allow for the definitions inside of a local to depend on each other. The ISL language will read the definitions inside a local in order, i.e., top to bottom.

Consider the following example

(define X 100)

(local ((define X 10)
        (define Y (+ X X))
        (define Z (+ Y X)))
 Z)

The result of the evaluating the preceding code snippet is 30. Each define will be evaluated top to bottom allowing for a define to access variables in preceding define expressions. Observe that this does not violate our scoping rules.

Uses of local: encapsulate a collection of functions

One of the common uses of local is to encapsulate a collection of functions that serve one purpose. For example, helper functions that we create in order to use in one of our functions can be defined using local.

Let’s look at an old example with list of Posn.

;;;; Data Definition

;; A ListOfPosn (LoP) is one of
;; - empty
;; - (cons Pons LoP)
;; INTERP: represents a list of cartesian points

;;;; Examples:
(define LOP-MT empty)
(define LOP1 (cons (make-posn 1 1) LOP-MT))
(define LOP (cons (make-posn 10 10)
                  (cons (make-posn 20 20)
                        LOP1)))




;; Deconstructor Template:
;; lop-fn: LoP -> ???
#; (define (lop-fn lop)
     (cond
       [(empty? lop) ...]
       [(cons? lop) ... (posn-fn (first lop)) ...
                    ... (lop-fn (rest lop) )...]))



;;;; Signature:
;; lop-move-x: LoP Real -> LoP
;;;; Purpose:
;; GIVEN: a list of points and a value
;; RETURNS: a new list with all points moved by value units in the x-axis

;;;; Example:
;; (lop-move-x LOP-MT 100) => empty
;; (lop-move-x LOP1 100) => (cons (make-posn 101 1) empty)
;; (lop-move-x LOP 100) =>  (cons (make-posn 110 10)
;;                                (cons (make-posn 120 20)
;;                                      (cons (make-posn 101 1) empty)))

;;;; Function Definition:
(define (lop-move-x lop dx)
  (cond
      [(empty? lop) empty]
       [(cons? lop)  (cons (posn-move-x (first lop) dx)
                           (lop-move-x (rest lop) dx))]))

;;;; Tests:
(check-expect (lop-move-x LOP-MT 100) empty)
(check-expect (lop-move-x LOP1 100) (cons (make-posn 101 1) empty))
(check-expect (lop-move-x LOP 100) (cons (make-posn 110 10)
                                         (cons (make-posn 120 20)
                                               (cons (make-posn 101 1) empty))))


;;;; Signature:
;; posn-move-x: Posn Real -> Posn
;;;; Purpose:
;; GIVEN: a cartesian point and a value
;; RETURNS: a new cartesian point with x coordinate increased by the given value

;;;; Examples:
;; (posn-move-x (make-posn 1 1) 2) => (make-posn 3 1)

;;;; Function Definition:
(define (posn-move-x posn dx)
  (make-posn (+ (posn-x posn) dx)
             (posn-y posn)))

;;;; Tests:
(check-expect (posn-move-x (make-posn 1 1) 2) (make-posn 3 1))

We created the helper function posn-move-x as a separate function with global scope. Assuming that no other part of our code is using posn-move-x then we can make it a local definition of lop-move-x.

;;;; Signature:
;; lop-move-x: LoP Real -> LoP
;;;; Purpose:
;; GIVEN: a list of points and a value
;; RETURNS: a new list with all points moved by value units in the x-axis

;;;; Example:
;; (lop-move-x LOP-MT 100) => empty
;; (lop-move-x LOP1 100) => (cons (make-posn 101 1) empty)
;; (lop-move-x LOP 100) =>  (cons (make-posn 110 10)
;;                                (cons (make-posn 120 20)
;;                                      (cons (make-posn 101 1) empty)))

;;;; Function Definition:
(define (lop-move-x lop dx)
  (local (;;;; Signature:
          ;; posn-move-x: Posn Real -> Posn
          ;;;; Purpose:
          ;; GIVEN: a cartesian point and a value
          ;; RETURNS: a new cartesian point with x coordinate increased by the given value

          ;;;; Function Definition:
          (define (posn-move-x posn dx)
            (make-posn (+ (posn-x posn) dx)
                       (posn-y posn))))
    (cond
      [(empty? lop) empty]
      [(cons? lop)  (cons (posn-move-x (first lop) dx)
                          (lop-move-x (rest lop) dx))])))

Uses of local: expressive power

Looking at our lop-move-x example that uses local for its helper function we see something interesting.

The second argument to our local function posn-move-x seems to be redundant. Now that the function posn-move-x is a local function inside lop-move-x it has access to the formal argument dx of lop-move-x. So we can simply use it in our posn-move-x instead of having it as a formal argument again.

;;;; Function Definition:
(define (lop-move-x lop dx)
  (local (;;;; Signature:
          ;; posn-move-x: Posn -> Posn
          ;;;; Purpose:
          ;; GIVEN: a cartesian point
          ;; RETURNS: a new cartesian point with x coordinate increased by dx

          ;;;; Function Definition:
          (define (posn-move-x posn)
            (make-posn (+ (posn-x posn) dx)
                       (posn-y posn))))
    (cond
      [(empty? lop) empty]
      [(cons? lop)  (cons (posn-move-x (first lop))
                          (lop-move-x (rest lop) dx))])))

local is an expression

local is an expression and it can appear at any location where an expression can appear.

For example we can rewrite are preceding example as

;;;; Function Definition:
(define (lop-move-x lop dx)
  (cond
    [(empty? lop) empty]
    [(cons? lop)  (local (;;;; Signature:
                          ;; posn-move-x: Posn Real -> Posn
                          ;;;; Purpose:
                          ;; GIVEN: a cartesian point and a value
                          ;; RETURNS: a new cartesian point with x coordinate increased by dx

                          ;;;; Examples:
                          ;; (posn-move-x (make-posn 1 1) 2) => (make-posn 3 1)

                          ;;;; Function Definition:
                          (define (posn-move-x posn)
                            (make-posn (+ (posn-x posn) dx)
                                       (posn-y posn))))
                    (cons (posn-move-x (first lop))
                          (lop-move-x (rest lop) dx)))]))

Uses of local: intermediary results

Another common use for local is to capture intermediate results of our computation so that

  • we can give them meaningful names and make our sub-expressions easier to read and understand

  • avoid re-calculating a sub-expression twice

Consider the problem of translating a number represented in any base from 1 to 10 to base 10. For example the number 3 is represented in base 2 (or binary numbers) as 11. We would like to build a function that takes a number in any base between 1 and 10 as a list of digits and returns the corresponding number in base 10.

;; A Digit is an Integer
;; WHERE: 0 <= Digit <= 9

;; A ListOfDigits (LoD) is one of
;; - empty
;; - (cons Digit LoD)

;;;; Signature
;; to-base10: LoD Integer -> Number
;;;; Purpose
;; GIVEN: a list of digits and their base
;; RETURNS: the corresponding base 10 number
(define (to-base10 lod base)
  (local (;;;; Signature
          ;; sum : LoN -> Number
          ;;;; Purpose
          ;; GIVEN: a list of numbers
          ;; RETURNS: their sum
          (define (sum lop)
            (cond
              [(empty? lop) 0]
              [(cons? lop) (+ (first lop)
                              (sum (rest lop)))]))

          ;;;; Signature:
          ;; to-integers: LoD Integer -> LoN
          ;;;; Purpose
          ;; GIVEN: a list of digits and their base
          ;; RETURNS: a list of base 10 integers for each digit
          (define (to-integers lod base)
            (cond
              [(empty? lod) empty]
              [else (cons (* (expt base (length (rest lod))) (first lod))
                          (to-integers (rest lod) base))])))
    (sum (to-integers lod base))))

Let’s focus on the helper function to-integers. The else clause contains the expression

(* (expt base (length (rest lod))) (first lod))

which is rather long and not as easy to read. We could create more meaningful names for some of the intermediary results that will allow us to re-write this expression in a way that is shorter and more meaningful

;; A Digit is an Integer
;; WHERE: 0 <= Digit <= 9

;; A ListOfDigits (LoD) is one of
;; - empty
;; - (cons Digit LoD)

;;;; Signature
;; to-base10: LoD Integer -> Number
;;;; Purpose
;; GIVEN: a list of digits and their base
;; RETURNS: the corresponding base 10 number
(define (to-base10 lod base)
  (local (;;;; Signature
          ;; sum : LoN -> Number
          ;;;; Purpose
          ;; GIVEN: a list of numbers
          ;; RETURNS: their sum

          (define (sum lop)
            (cond
              [(empty? lop) 0]
              [(cons? lop) (+ (first lop)
                              (sum (rest lop)))]))

          ;;;; Signature:
          ;; to-integers: LoD Integer -> LoN
          ;;;; Purpose:
          ;; GIVEN: a list of digits and their base
          ;; RETURNS: a list of base 10 integers for each digit
          (define (to-integers lod base)    ;; ISL does not allow no-argument functions
            (cond
              [(empty? lod) empty]
              [else (local ((define a-digit (first lod))   (1)
                            (define rest-digits (rest lod)) (2)
                            (define size (length rest-digits))) (3)
              (cons (* (expt base size) a-digit)  (4)
                          (to-integers rest-digits base)))]))) (5)
    (sum (to-integers lod base))))
1 we create a name for the result of (first lod), a-digit
2 we create a name for the result of (rest lod), rest-digits
3 we create a name for the result of (length rest-digits), size. Here we have reused one of your new names rest-digits.
4 now we can rewrite our original sub-expression to be (* (expt base size) a-digit) which is shorted and more intuitive
5 finally, we have the opportunity to reuse for a second time rest-digits for our recursive call. Observe that the new version of our code calls (rest lod) once while our previous version called (rest lod) twice. The new version of our code has given us some computation savings by performing a computation once rather than twice

Functions are values

Our ISL language provides another powerful feature.

Recall our definition of what is a value

A value is anything in our program that can be

  1. Given as an input to a function.

  2. Returned as a result of a function call.

  3. Stored

In ISL functions are values. This means

  1. we can pass functions as inputs to other functions (technically we have been doing that with big-bang)

  2. we can return a function as the result of a function call

  3. we can store a function inside a struct or list

lambda (λ)

ISL allows us to create anonymous functions. Up to know all of our functions were defined using the format

(define (function-name arg1 arg2 ...) ...)

and our globals were defined using the format

(define NAME EXPRESSION)

For example, we defined globals and provided names to anonymous values like

;; A Person is a (make-person String String)
;; INTERP: represents a person with the first and last name

(define-struct person (first last))

(define MARY (make-person "Mary" "Andrews")) ;; a named value

(make-person "John" "Doe") ;; an anonymous (no name) value

Well we can do the same thing with functions by using lambda (or λ the Greek character called lambda) to create anonymous functions.

For example the two definitions below are equivalent.

(define (add5 x)
 (+ 5 x))

(define add5 (lambda (x) (+ 5 x)))

;; (define add5 (λ (x) (+ 5 x)))

So (lambda (x) (+ 5 x)) is an anonymous function. It is the body of the function wrapped in a lambda and (x) to denote the argument. We can then give it a name using the same define syntax as we have been doing for variables.

Let’s call an anonymous function

(define add5 (lambda (x) (+ 5 x)))

(add5 10) ;; evaluates to 15

((lambda (x) (+ 5 x))
 10) ;; evaluates to 15

Another way to think about it, is that we essentially performed the first step of the stepper by replacing the name of the function add5 with the value bound to the name add5 which is (lambda (x) (+ 5 x)).

Abstraction

The word abstraction here refers to the process and the results of the process that takes code or data definitions that are similar and creates new code or data definitions that eliminate these similarities.

In this regard, programs are like essays. We first write a draft and drafts require editing. This is also the reason why our Design Recipe has as its last step a review.

Similarities in Data Definitions

Recall the numerous version of lists that we have defined thus far in this semester. Here are a few to refresh your memory

;; A ListOfInteger (LoI) is one of
;; - empty
;; - (cons Integer LoI)

;; A ListOfString (LoS) is one of
;; - empty
;; - (cons String LoS)

;; A ListOfBoolean (LoB) is one of
;; - empty
;; - (cons Boolean LoB)

;; A ListOfPosn (LoP) is one of
;; - empty
;; - (cons Posn LoP)
  1. Can you spot the similarities?

  2. Can you spot the differences?

  3. Do you see a pattern here?

The process of abstracting starts by detecting the similar and dissimilar parts of our program. Then we need a way to capture the similarities in one location and allow for the dissimilarities to be provided each time. This is very similar to a function and its formal arguments. The formal arguments are the parts of the function that change or different on every function call. The parts of the function body that are not formal arguments are fixed they do not change with every function call.

We perform a similar re-write here to get a more general data definition that has something similar to a formal argument that we pass along every time we use it.

A generic list data definition
;; A List<X> is one of  (1)
;; - empty  (2)
;; - (cons X List<X>)  (3)
1 Read the text List<X> as list of X. The X here acts as a formal argument. We are defining a list of X and we do not know what X is at the moment but we know that something is going to take the place of X when we use this data definition.
2 this is just the empty case which was identical on all list data definitions
3 now here we have some parts that are identical on all list data definitions and some that are not. (cons and ) are identical in all list data definitions. The differences was on the data definition name of the element and of course the data definition name of the list we are defining. We captured this differences with X for the data definition of the list element and List<X> for our new list.

So now that we have a generic list data definition let’s see how to use it. Recall sum from our previous section that takes a list of numbers and returns their sum. We can now re-write `sum’s signature to be

;; sum : List<Number> -> Number

In this use of List<X> we have instantiated X to be Number

There are however some functions that do not really care about the data definition of the elements. For example, length which is the build-in function that returns the size of a list does not really operate on the elements of the list. The implementation of length only cares if there is an element or not, it does not perform any operations on the elements itself. This is why we can call length with any list and length will work as expected. We cannot say the same for sum however.

So let’s write the signature for length

;; length: List<X> -> NonNegInteger

Similarities in function definitions

Lets recall some of the functions that we did write over lists.

;;;; Signature
;; add-n: List<Number> Number -> List<Number>
;;;; Purpose
;; GIVEN: a list of numbers and a number (dx)
;; RETURNS: a list with each element is added dx

;;;; Function Definition

(define (add-n lon dx)
  (cond
    [(empty? lon) empty]
    [(cons? lon) (cons (+ dx (first lon))
                       (add-n (rest lon) dx))]))

;;; Tests ...


;;;; Signature:
;; lop-move-x: LoP Real -> LoP
;;;; Purpose:
;; GIVEN: a list of points and a value
;; RETURNS: a new list with all points moved by value units in the x-axis

;;;; Example:
;; (lop-move-x LOP-MT 100) => empty
;; (lop-move-x LOP1 100) => (cons (make-posn 101 1) empty)
;; (lop-move-x LOP 100) =>  (cons (make-posn 110 10)
;;                                (cons (make-posn 120 20)
;;                                      (cons (make-posn 101 1) empty)))

;;;; Function Definition:
(define (lop-move-x lop dx)
  (cond
      [(empty? lop) empty]
       [(cons? lop)  (cons (posn-move-x (first lop) dx)
                           (lop-move-x (rest lop) dx))]))

;;;; Tests:
(check-expect (lop-move-x LOP-MT 100) empty)
(check-expect (lop-move-x LOP1 100) (cons (make-posn 101 1) empty))
(check-expect (lop-move-x LOP 100) (cons (make-posn 110 10)
                                         (cons (make-posn 120 20)
                                               (cons (make-posn 101 1) empty))))


;; For definition of posn-move-x see previous sections
  1. Can you spot the similarities?

  2. Can you spot the differences?

  3. Do you see a pattern here?

Let’s extract the similarities first. We will use ellipsis …​ for the places in the code that are dissimilar.

;;;; Function Definition:
(define (mymap lop ...)
  (cond
      [(empty? lop) empty]
       [(cons? lop)  (cons (... (first lop) ...)
                           (mymap (rest lop) ...))]))
;;;; Signature:
;; mymap: [X,Y] : List<X> [X -> Y] -> List<Y>
(define (mymap lop op)
  (cond
      [(empty? lop) empty]
       [(cons? lop)  (cons (op (first lop))
                           (mymap (rest lop) op))]))

There is new notation here so we will look at each part

  1. The signature has 2 new elements

  2. [X,Y] appears right after the name of the function in the signature. These are arguments, in the same way that X is an argument in the generic data definition of List<X>. We are specifying that are signature is generic and it requires 2 arguments that will be known when we call mymap.

  3. [X → Y] appears as the signature for the second argument to mymap. The syntax [ → ] is used to specify that this argument is going to be a function. The specific signature [X → Y] is specifying that we expect the second argument to be a function that accepts 1 argument which needs to be an X and should return one value which needs to be a Y

Now that we have our mymap generic function lets try to use it in order to achieve the same behaviour as (add-n (list 1 2 3) 10).

(mymap (list 1 2 3)
 ;; Number -> Number
 (lambda (x) (+ x 10)))

In this call to mymap we have provided (list 1 2 3) as the first argument. The signature of mymap states that the first argument to mymap is List<X> we have therefore instantiated X to Number in this call.

Similarly the anonymous function we used as the second argument to mymap has a signature Number → Number, and the signature of mymap states that the second argument has signature [X → Y]. We already instantiated X as Number so our second argument should also use X as Number but we have now also instantiated Y as Number.

Can you describe the result of evaluating the following function call to mymap?

(mymap (list "aa" "bb" "cc") string->symbol)

What about this one?

(mymap (list 1 2 3) string->symbol)
(define (add-n lon dx)
 (local ((define (adder element)
          (+ element dx)))
  (mymap lon adder)))

Let’s consider another set of functions and their similarities.

(define (sum lon)
  (cond
    [(empty? lon) 0]
    [(cons? lon) (+ (first lon)
                    (sum (rest lon)))]))


(define (los-append los)
  (cond
    [(empty? los) ""]
    [(cons? los) (string-append (first los)
                                (los-append (rest los)))]))
  1. Can you spot the similarities?

  2. Can you spot the differences?

  3. Do you see a pattern here?

Let’s extract the similarities first. We will use ellipsis …​ for the places in the code that are dissimilar. We will name our new abstracted function reduce.

;;;; Function Definition:
(define (reduce lox ...)
  (cond
      [(empty? lox) ... ] (1)
      [(cons? lox)  (... (first lox)   (2)
                         (reduce (rest lox) x ...))]))
1 The two functions return something different in the case of an empty list
2 The two functions call a some other function and pass the first element of the list as the first argument and the result of recursively evaluating the rest of the list

Let’s give names to the differences and add them as arguments to reduce. We are going to add a signature to our reduce but leave it incomplete for now. We will fill in the signature of reduce shortly

;;;; Signature:
;; reduce: ... :  ... -> ...
(define (reduce lox base op)
  (cond
    [(empty? lox) base] (1)
    [(cons? lox) (op (first lox) (2)
                     (reduce (rest lox) base op))]))
1 the value that we return when given (or reaching) the empty list we will call base and also add it as a formal argument to reduce
2 the function that we will call on the first of the list and the result of recursively evaluating the rest of the list we will call op

Now that we filled in everything let’s figure out the signature of reduce. We will use examples of inputs, specifically our list lox and pretend to be the stepper only this time we are going to pay attention on the results for reduce and match them to arguments in our signatures.

  1. The first input to reduce is a list. Since we are abstracting over sum and los-append we know that our lists can hold different values per invocation. So let’s go with List<X> for the first input and fill in our signature

    ;; reduce: [X ..] :  List<X> ... -> ...
  2. Now let’s figure out the second input to reduce, base. base is what reduce returns when we have the empty list. We can get the empty list through 2 use cases

    1. the input given to reduce was empty. In this case the result of reduce will be base

    2. the input given to reduce was not empty but the recursive call in the second cond-clause will eventually hit empty. So the second input to op will be base when the recursion hits the end of the list.

      Now we need to decide if we are going to use X again in our signature or if we need a new argument. Even though our sum and los-append return the same kind of data as their input (sum takes a list of numbers and returns a number, los-append takes a list of strings and returns a string) this does not mean it is always the case. We could take sum and instead of adding all the numbers in the list, turn each number into a string and append the strings together. The fact that reduce will allow us to do that means that we should indicate that base and the second input to op are not always the same kind of data as the elements of our list. So we pick Y for our second argument. Let’s update our signature with this new information

      ;; reduce: [X Y..] :  List<X> Y [ ... Y -> ...] -> Y
  3. We need to figure out the signature for op. So let’s focus on that part of our expression. We already know that the second argument to op recurs and thus will hit empty to which reduce returns base which is a Y. We also know that op takes as first input (first lox) and we know that lox is List<X>. Therefore (first lox) is an X. So the first input to op must be an X. Let’s update the signature with this new information

    ;; reduce: [X Y..] :  List<X> Y [ X Y -> ...] -> Y

    Now we are left with the return value for op 's signature. Based on our expression, if the lox is non-empty we return whatever op returns. So the return value of op becomes the return value for reduce. We already know that reduce returns a Y due to the case when the lox is empty so op must also return a Y. Another part of our expression, the recursion found as the second input to op also indicates that for a non-empty list the intermediate results of the recursive call must also be Y. So our final signature then is

    ;; reduce: [X Y] :  List<X> Y [X Y -> Y] -> Y

mymap and reduce are two examples of abstraction. We will use the name Higher Order Functions (HOF) for functions that either

  1. take function(s) as inputs and,or

  2. return function(s) as output

mymap and reduce are HOFs.

Build in HOFs

mymap and reduce are common and popular HOFs and the exist as build in functions in ISL, although with a slightly different signature, as map and foldr For the list of build in HOFs see figure 95 and figure 96 in the second edition of the book.