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-
a variable definition,
(define WIDTH 100)
-
a structure definition,
(define-struct pair (first second))
-
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
-
defined globally in the file
-
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 thatX
has global scope. -
Y
andZ
are variables and their scope is the expression(+ X Y Z)
. We also say thatY
andZ
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
-
Start at the expression that uses the variable.
-
Start moving from the expression outwards, to find a
define
for the variable/function that you are looking for-
if we are in a nested expression, outwards means move to the enclosing expression
-
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
-
the expression
(+ X Y Z)
is nested inside alocal
. Check ifX
is defined as part oflocal
. It is not so we move outwards -
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 forX
. That is the value forX
in our original expression(+ X Y Z)
Let’s do that again for Y
-
the expression
(+ X Y Z)
is nested inside alocal
. Check ifY
is defined as part oflocal
. It is so we use the value20
forY
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
-
starting at
(+ X Y Z)
we first need to figure out the value forX
. So we move outward. -
The immediately enclosing
local
definesZ
but notX
. So we keep moving outward. -
The next enclosing expression is a
local
and it definesX
andY
.X
is defined as100
so we replaceX
with100
-
Now we need to repeat the process for
Y
andZ
-
Y
is defined in the outermostlocal
expression as20
-
Z
is defined in the innermostlocal
expression as30
-
so the expression
(+ X Y Z)
becomes(+ 100 20 30)
which evaluates to150
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
-
Given as an input to a function.
-
Returned as a result of a function call.
-
Stored
In ISL functions are values. This means
-
we can pass functions as inputs to other functions (technically we have been doing that with
big-bang
) -
we can return a function as the result of a function call
-
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)
-
Can you spot the similarities?
-
Can you spot the differences?
-
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 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
-
Can you spot the similarities?
-
Can you spot the differences?
-
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
-
The signature has 2 new elements
-
[X,Y]
appears right after the name of the function in the signature. These are arguments, in the same way thatX
is an argument in the generic data definition ofList<X>
. We are specifying that are signature is generic and it requires 2 arguments that will be known when we callmymap
. -
[X → Y]
appears as the signature for the second argument tomymap
. 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 anX
and should return one value which needs to be aY
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)))]))
-
Can you spot the similarities?
-
Can you spot the differences?
-
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.
-
The first input to
reduce
is a list. Since we are abstracting oversum
andlos-append
we know that our lists can hold different values per invocation. So let’s go withList<X>
for the first input and fill in our signature;; reduce: [X ..] : List<X> ... -> ...
-
Now let’s figure out the second input to
reduce
,base
.base
is whatreduce
returns when we have the empty list. We can get the empty list through 2 use cases-
the input given to
reduce
wasempty
. In this case the result ofreduce
will bebase
-
the input given to
reduce
was notempty
but the recursive call in the second cond-clause will eventually hitempty
. So the second input toop
will bebase
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 oursum
andlos-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 takesum
and instead of adding all the numbers in the list, turn each number into a string and append the strings together. The fact thatreduce
will allow us to do that means that we should indicate thatbase
and the second input toop
are not always the same kind of data as the elements of our list. So we pickY
for our second argument. Let’s update our signature with this new information;; reduce: [X Y..] : List<X> Y [ ... Y -> ...] -> Y
-
-
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 toop
recurs and thus will hitempty
to which reduce returnsbase
which is aY
. We also know thatop
takes as first input(first lox)
and we know thatlox
isList<X>
. Therefore(first lox)
is anX
. So the first input toop
must be anX
. 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 thelox
is non-empty we return whateverop
returns. So the return value ofop
becomes the return value forreduce
. We already know thatreduce
returns aY
due to the case when thelox
isempty
soop
must also return aY
. Another part of our expression, the recursion found as the second input toop
also indicates that for a non-empty list the intermediate results of the recursive call must also beY
. 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
-
take function(s) as inputs and,or
-
return function(s) as output
mymap
and reduce
are HOFs.