cs-4400-sp26

Lecture 4: Lambda Calculus

Today we are going to talk about a tiny programming language that introduces a notion of function into our language.

Functions in programming

[TPS:] From a programmer’s perspective, what is a function? What purpose does it serve?

A possible answer is that functions allow us to abstract over data. Instead of writing some repetitive code like

(define l (list 1 2 3 4))
(define l' (list (+ 1 (list-ref l 0)) 
				 (+ 1 (list-ref l 1)) 
				 (+ 1 (list-ref l 2)) 
				 (+ 1 (list-ref l 3))))

We can write a function like

(define (increment-list l)
 (type-case (Listof Number) l
  (empty empty)
  ((cons x rest) (cons (+ x 1) (increment-list rest)))
 )
)

This increment-list function binds the variable l. Another way to say this is that it abstracts over l, so that it works for all lists, not just a specific one. Note that this is yet another use of the idea of a bound variable.

When we call a function, this is sometimes also called function application, as in applying the function increment-list to the argument (list 1 2 3 4).

Plait also lets us write something (as do many other languages) that is sometimes called an “anonymous function”, but what we will call a “lambda abstraction” – a function without a name. It uses the keyword lambda for this.

(lambda (l)
  (type-case (Listof Number) l
  (empty empty)
  ((cons x rest) (cons (+ x 1) (increment-list rest)))
 ))

Sometimes this is convenient if we only want to use a function once, such as in a call to map:

> (map (\lambda (x) (+ x 1)) (list 1 2 3 4))
- (Listof Number)
'(2 3 4 5)

We can also directly apply a lambda abstraction:

> ((\lambda (x) (+ x 1)) 4)
- Number
5

However, we actually already have a different syntax for this!

(let [(x 4)] (+ x 1))

Directly writing a lambda abstraction and then applying it to a specific argument is just another way of writing a let expression. In this sense, lambda is strictly more general than let, so once we add it to our language, we won’t really need let anymore.

Discussion: This philosophy of only including constructs in our language if they aren’t expressible in terms of existing constructs is called language minimalism. Why might it be a value for studying programming languages? How does this idea interact with the notion that, in practice, we generally want to give users nice syntax for specific uses of the underlying constructs, such as let? (In other words, why do language in practice give us a let construct even though we also have lambda?)

$\lambda$ Calculus

We’re now going to step back from the practical concerns of programming to a very abstract perspective (pun intended): a tiny language called the $\lambda$ calculus. This language is, however, going to turn out to be much more expressive, and have much more complex semantics, than the first tiny language we saw (CalcLang).

The syntax is as follows: \(\Large e ::= x \mid e\;e \mid \lambda{x}.e\)

There are just three things: lambda, application, and variables. We can think of this as a super-concise syntax where things like Plait’s (lambda (x) x) are written $\lambda{x}.\,x$.

Note that we don’t have any base types like numbers, booleans, or strings – yet, shockingly, this language will be enough to give us the most general form of computation.

A few historical notes:

  1. $`\lambda`$-calculus was first invented (or discovered, if you prefer) by Alonzo Church and J.B. Rosser in 1936 [1]. This was before digital computers, studied as an “on paper” system.
  2. Another such system you might have heard of is Turing machines, also discovered in 1936 [2]. You might have also heard that Turing machines give us a universal notion of computation: anything you can do on a digital computer can also be expressed with a Turing machine.
  3. The same is also true for lambda calculus, and they were independently (and nearly simultaneously!) discovered. These discoveries are of great significance to the foundations of computing, leading to the Church-Turing Thesis, which states that the effectively computable (partial) functions are exactly those that can be implemented by Turing Machines or, equivalently, in the λ-calculus.
Semantics

We have a syntax, so what about semantics? It’s also very simple to state in terms of concepts we already understand:

  1. $\lambda{x}.\; e$ is a value (no more work to do)
  2. $(\lambda{x}.\;e)\ e’$ (applying a lambda to an argument) first evaluates its argument $e’$ to a value $v$, then substitutes $v$ for $x$ in the body $e$ of the lambda ($[v/x]e$ in our previous notation) and continues running.

Although this is simple to state, it’s a little bit tricky to see how it works in practice. Let’s talk through some examples.

Examples

(First, draw parse trees. Then, develop intuition for the meaning of the term, and what we expect it to evaluate to. Finally, step through evaluation.)

  1. $\lambda{x}.\;x$
    1. (abbreviate this function $\mathsf{id}$.)
  2. $(\lambda{x}.\;x)\ \mathsf{id}$
  3. $(\lambda{x}.\;\lambda{y}.\;x)$
    1. (abbreviate this function $\mathsf{k}$)
  4. $\mathsf{k}\ \mathsf{id}\ \mathsf{k}$
  5. $\lambda{f}.\lambda{x}.f\ x$
  6. $(\lambda{f}.\lambda{x}.f\ x)\ \mathsf{id}$

Note that lambda abstractions only “accept one argument” at a time. How can we simulate functions that accept multiple arguments?

Terms to define:

A $\lambda$-calculus interpreter in Plait

(interp : (Lambda -> Value))
(define (interp e)
  (type-case Lambda e
    [(varE x) (error 'runtime "unbound variable")]
    [(lamE x b) (lamV x b)]
    [(appE e1 e2)
      (type-case Value (interp e1)
        [(lamV x b) (let [(v (interp e2))] (interp (subst x v b)))]
        )
     ]
  )
)

Recursion

What happens if we try to run this expression?

\[\Large (\lambda{x}.\;x\ x)\ (\lambda{x}.\;x\ x)\]

Our interpreter might help us understand that this expression won’t terminate if run, but it doesn’t tell us any other useful information, so let’s write a stepper to show us one step at a time.

(step : (Lambda -> Lambda))
(define (step e)
  (type-case Lambda e
    [(varE X) (error 'runtime "unbound variable")]
    [(lamE x b) (lamE x b)]
    [(appE f arg)
     (type-case Lambda f
       [(lamE x b) (type-case Lambda arg
                     [(lamE x2 b2) (subst x (lamV x2 b2) b)]
                     [else (appE f (step arg))])]
       [else (appE (step f) arg)])]
    )
  )

This lets us see that the self-application function applied to itself results in the same thing we started with:

> (step omega)
- Lambda
(appE (lamE 'x (appE (varE 'x) (varE 'x))) (lamE 'x (appE (varE 'x) (varE 'x))))
> omega
- Lambda
(appE (lamE 'x (appE (varE 'x) (varE 'x))) (lamE 'x (appE (varE 'x) (varE 'x))))

Function composition

In mathematics, we sometimes write \(\Large (f\circ g)(x) = f(g(x))\) to mean composing the functions $f$ and $g$. With $\lambda-$notation, we can explicitly define the composition without explicit reference to its argument as:

\(\Large (f\circ g) = \lambda{x}.\;f(g(x))\) We can then notice that $\circ$ itself is a function, taking two functions as arguments and returning another function!

\(\Large \circ = \lambda{f}.\lambda{g}.\lambda{x}.\;f\ (g\ x)\) Example: $\circ\ f\ \mathsf{id}$

Preview: Encoding data

Since there are no base types, it may not be obvious how we can do any kind of “standard” programming with this language. Next time, we will show how $\lambda$-calculus lets us encode data such as Booleans, natural numbers, and pairs.

References

  1. Alonzo Church and J.B. Rosser. Some properties of conversion. Transactions of the American Mathematical Society, 39(3):472–482, May 1936.
  2. Alan Turing. On computable numbers, with an application to the entscheidungsproblem. Proceedings of the London Mathematical Society, 42:230–265, 1936. Published 1937.