Skip to main content

Lec 4: higher-order procedures 2

Why is function as data amazing?

You can build a programming language with the ability to create a lambda function and a way to call a lambda.

Even building arithmetic is possible with a language which only has lambda.

How do you make a recursive procedure only using lambda and no define? Answered further down.

Generalizing patterns of control

You have a collection of data and you want to filter it. There are many ways to filter data which will follow the same pattern. So a collection of procedures may have the sam underlying pattern.

Procedures as data can create any mechanism for control within a program.

First Class Data-types

A datatype is first class if it can:

  • Be the value of a variable
  • Be an argument to a procedure
  • Be the value returned by a procedure
  • Be a member of an aggregate (like an array)
  • Be anonymous

Scheme makes almost everything first class.

The issue this creates is that the language becomes more difficult to compile.

What is first class

  • Numbers
  • Character strings (some languages)

What is not first class

  • Data aggregates

A recursive function which calls itself

; creating a recursive function with lambdas only

((lambda (f n l) (f f n l))
(lambda (f n l)
(if (equal? n l)
n
(f f (+ n 1) l))) 1 10)

Returning Functions

; returning a function

(define (make-adder n)
(lambda (x)
(+ x n)))

Here we have a procedure which returns a function using the a lambda which has not been called.

(define add3 (make-adder 3))

Now we can create the adder using the generalized function pattern which make-adder provides.

In the same way that we saw procedure definitions are just a shorthand for defining a lambda, we can use the general higher-order adder function to define a procedure. This is because the procedure make-adder returns a lambda procedure as data.

(add3 10)
13

Compose - Example

; compose example

(define (compose f g)
(lambda (x) (f (g x))))

((compose add3 add3) 3)

The procedure is made entirely of functions, takes functions as arguments and returns functions.

There is a reliance on both functions domain and range.

The range of function g needs to be the same as the domain of function f. Because f takes the return value of the function g.

Tangent

Scheme has been used as a prototyping language because you can write programs fast using it.

This is useful because a client may want something, but describe it in a completely different way. So you go through 5 iterations of creating a program until they get what they want. At which point they say it's too slow. So you turn to C or another language which will compile into a faster program.

Reusing Computed Values

; roots procedure

(define (roots a b c)
(se (/ (+ (- b) (sqrt (- (* b b) (* 4 a c)))) (* 2 a))
(/ (- (- b) (sqrt (- (* b b) (* 4 a c)))) (* 2 a))))

(roots 1 -5 6)

This issue with the procedure above is that it recomputes the same expression in many cases. There are vast swathes of the expressions which are the same.

We can fix this with a helper function.

; roots procedure with helper

(define (roots_2 a b c)
(define (roots1 d)
(se (/ (+ (- b) (sqrt (- (* b b) (* 4 a c)))) (* 2 a))
(/ (- (- b) (sqrt (- (* b b) (* 4 a c)))) (* 2 a))))
(roots1 (sqrt (- (* b b) (* 4 a c)))))

(roots_2 1 -5 6)

This has removed the part of the expression (sqrt (- (* b b) (* 4 a c)).

We can remove the definition of a helper function which will only be used once. It can be replaced by an anonymous function.

; roots procedure with lambda

(define (roots_3 a b c)
((lambda (d)
(se (/ (+ (- b) (sqrt (- (* b b) (* 4 a c)))) (* 2 a))
(/ (- (- b) (sqrt (- (* b b) (* 4 a c)))) (* 2 a))))
(sqrt (- (* b b) (* 4 a c)))))

(roots_3 1 -5 6)

The issue with this is that it's ugly to read.

Let Bindings Body

To improve this we use a let special form.

(let bindings body)
(binding)
(name value-expr)

The previous quadratic example can be improved with the use of let.

; roots procedure with let

(define (roots_4 a b c)
(let ((d (sqrt (- (* b b) (* 4 a c)))))
(se (/ (+ (- b) d) (* 2 a))
(/ (- (- b) d) (* 2 a)))))

(roots_4 1 -5 6)

There are a few more terms in the expression which are being computed more than once.

; roots procedure with let and more optimization

(define (roots_5 a b c)
(let ((d (sqrt (- (* b b) (* 4 a c))))
(-b (- b))
(2a (* 2 a)))
(se (/ (+ -b d) 2a)
(/ (- -b d) 2a))))

(roots_5 1 -5 6)

Let and Applicative Order

Question: Can a variables expression depend on the variable of another expression?

The answer is no. The reason is applicative order.

If you use a variable which relies on another variable within the function body before the other variable has been used, then the other variable which you rely on hasn't been computed yet.

Let*

This allows you to have variables which rely on the other variables.

; let* star

(define (let-star a)
(let* ((b (+ a 5)) (a 3))
(* a b)))

(let-star 3)

What this does is nest let expressions.

; let* star with nested let

(define (let-star-nest a)
(let ((a 3))
(let ((b (+ a 5)))
(* a b))))

(let-star-nest 3)

You cannot use let to make a recursive function. There is another variant of let which is called let rec which allows you to build recursive calls in a let statement.