Recursive Structures

MAC0316/5754
Concepts of Programming Languages

Or: To make recursion, is it necessary to know how to make recursion?

Renato Cordeiro Ferreira

Waterfall,
M. C. Escher, 1961

Recursion in programming

  • For data structures, it may happen between objects of the same type, or with the same object
    • In the first case, it's simple: there is no self-reference
      (e.g., for trees and lists, which have nodes of the same type)
    • In the second case, traversals require extra care
      (e.g., for cyclic graphs and circular lists, where there is a circuit that passes through the node)
  • For functions, it happens when a function calls itself
    • In this situation, the problem is to refer to the identifier of the function while it is still being defined

Recursion with state

The command letrec, which we implemented in our last assignment, uses state to create recursion. The code:

(let ([fac '()])
  (begin (set! fac
           (lambda (n) (if (= n 0) 1 (* n (fac (- n 1))))))
         (fac 5)))
(letrec ([fac (lambda (n)
                (if (= n 0) 1 (* n (fac (- n 1)))))])
  (fac 5))

Is equivalent to binding the name of the function with a dummy value (let), then rebinding it (set!) with the closure of the function body, then finally calling the function (begin):

Recursion with state

In our interpreter, this code can be implemented with boxes, which are a special value that can be manipulated with the operations box, unbox and set-box!

Instead of binding ids with values in an environment:

id

value

bind

The interpreter binds ids with locations in an environment, which then make cells with values in a store:

id

value

location

environment

bind

cell

environment

store

Recursion with state

Recursion with state has a problem: what does it happen if the value of a box / variable is used before its value is set? We have a leak of the temporary (uninitialized) value.

Some possible solutions for this problem include:

  • Allow the use only to define functions (limiting)
  • Make an explicit test on each reference (expensive)
  • Use a special invalid value, such as undefined or NaN
    (most common solution for real programming languages)

Is it possible to make recursion without state?

Recursion without state

Haskell Brooks Curry
(September 12, 1900 – September 1, 1982)

American mathematician and logician who worked with combinatorics logic.

He gives name to the currying technique and 3 (functional) programming languages:

Recursion without state

Curry was the first to describe a technique that could be used to generate recursion without state, the Y combinator, which can be defined as:

(define Y (
  λ (p) (
    (λ (f) (p (f f)))
    (λ (f) (p (f f)))
  )
))
(define Fact
  (Y (λ(fact) (λ(n) (if (zero? n) 1 (* n (fact (- n 1))))))))

And can be applied as:

Recursion without state

The Y combinator is a fixed-point operator of lambda calculus. It is mathematically described as:

Y = \lambda p.\ (\lambda f.\ p (f\ f))\ (\lambda f.\ p (f\ f))

If we expand the Y combinator with a function \( g \) (an operation known as β-reduction in lambda calculus), then we have:

Y g = \lambda p.\ (\lambda f.\ p (f\ f))\ (\lambda f.\ p (f\ f))\ g
= (\lambda f.\ g (f\ f))\ (\lambda f.\ g (f\ f))
= g ((\lambda f.\ g (f\ f))\ (\lambda f.\ g (f\ f)))
= g\ (Y\ g) = g\ (\ldots (Y\ g)\ldots)

Recursion without state

We can try to deduce the Y combinator from the implementation of the factorial:

(define Fact
  (λ (n)
    (if (zero? n) 1
      (* n (??? (- n 1)))
  ))
)

We would like to call Fact in the gap, but it is not possible since it is not defined in that point.

What if we replaced the gap by an auxiliary function?

Recursion without state

The problem is that mk-fact receives a function as a parameter, so we cannot call it this way.

(define mk-fact
  (λ (f)
    (λ (n)
      (if (zero? n) 1
        (* n (f (- n 1)))
  )))
)

(define Fact
  (λ (n)
    (if (zero? n) 1
      (* n (mk-fact (- n 1)))
  ))
)

Recursion without state

We need to pass something else to mk-fact so that, after applied, we get the body. What if we use mk-fact itself?

(define mk-fact
  (λ (f)
    (λ (n)
      (if (zero? n) 1
        (* n ((f ???) (- n 1)))
  )))
)

(define Fact
  (λ (n)
    (if (zero? n) 1
      (* n ((mk-fact ???) (- n 1)))
  ))
)

Recursion without state

It works! The argument is duplicated on each call, so that a new body of the function is created. But the code is duplicated.

(define mk-fact
  (λ (f)
    (λ (n)
      (if (zero? n) 1
        (* n ((f f) (- n 1)))
  )))
)

(define Fact
  (λ (n)
    (if (zero? n) 1
      (* n ((mk-fact mk-fact) (- n 1)))
  ))
)

Recursion without state

Fact can be deduced directly from mk-fact. It does not worth to create an auxiliary function. We can reduce it further.

(define mk-fact
  (λ (f)
    (λ (n)
      (if (zero? n) 1
        (* n ((f f) (- n 1)))
  )))
)

(define Fact
  (mk-fact mk-fact))

Recursion without state

The logic that does the duplication is mixed with the logic of the factorial function. Can we extract the duplication logic?

(define Fact
  (
    (λ (mk-fact) (mk-fact mk-fact))
    (λ (f)
      (λ (n)
        (if (zero? n) 1
          (* n ((f f) (- n 1)))
    )))
  )
)

Recursion without state

Now the body of the factorial is isolated from the rest. We can define a general function that makes the recursion.

(define Fact
  (
    (λ (mk-fact) (mk-fact mk-fact))
    (λ (f)
      (
        (λ (fact)
          (λ (n)
            (if (zero? n) 1
              (* n (fact (- n 1)))
        )))
        (f f)
      )
    )
  )
)

Recursion without state

Yay!. We finalmy have the Y Combinator. This form is slightly different from our first slide, but can be β-reduced to it.

(define Y
  (λ (p) (
    (λ (f) (f f))
    (λ (f) (p (f f)))
  ))
)

(define Fact
  (Y (λ (fact)
    (λ (n)
      (if (zero? n) 1
        (* n (fact (- n 1)))
  ))))
)

Recursion without state

There is only one problem: (f f) is calculated before the body, so there is infinite recursion. We need to make it lazy.

(define Y
  (λ (p) (
    (λ (f) (p (f f)))
    (λ (f) (p (f f)))
  ))
)

(define Fact
  (Y (λ (fact)
    (λ (n)
      (if (zero? n) 1
        (* n (fact (- n 1)))
  ))))
)

Recursion without state

Adding another lambda solves this problem. The new function is called a Turing combinator.

(define Turing
  (λ (p) (
    (λ (f) (p (λ (a) ((f f) a))))
    (λ (f) (p (λ (a) ((f f) a))))
  ))
)

(define Fact
  (Turing (λ (fact)
    (λ (n)
      (if (zero? n) 1
        (* n (fact (- n 1)))
  ))))
)

Recursion without state

Y and Turing combinators show that languages can be made only with function definitions and function applications.

(define Y
  (λ (p) (
    (λ (f) (p (f f)))
    (λ (f) (p (f f)))
  ))
)

(define Turing
  (λ (p) (
    (λ (f) (p (λ (a) ((f f) a))))
    (λ (f) (p (λ (a) ((f f) a))))
  ))
)

Recursive Structures

By Renato Cordeiro Ferreira

Recursive Structures

Presentation about recursive strutures for the course MAC0316 (Fundamental Concepts in Programming Languages) at IME-USP

  • 781