Lambda calculus

gillchristian

Christian Gill

Posted on February 25, 2019

Lambda calculus

I figured for some of the days of #100DaysOfFP it makes more sense to write something longer than a tweet. So every now and then I'll post in dev.to instead of only a tweet. 😄

This week the Delft Haskell Study Group started again. I attended last year but didn't put in as much focus as I wanted, so I decided to give it another shot this year.

The goal of the study group is to read some chapter/s of Haskell Programming from First Principles and then go through the exercices together.

My goal for these series is to share a few things from the chapter. More than a blogpost this is a collection of notes that could serve as a recap of the chapter.

Back to the roots

Even the greatest mathematicians, the ones that we would put into our mythology of great mathematicians, had to do a great deal of leg work in order to get to the solution in the end.

― Daniel Tammett

The first chapter of the book doesn't go directly into Haskell and programming but instead introduces us to Lambda calculus. Why? Because it's the foundation on top of what Functional Programming, and thus Haskell, is based.

Lambda calculus is "a model of computation devised in the 1930s by Alonzo Church. [...] Functional programming languages are all based on the lambda
calculus" 1.

Functional programming: "a computer programming paradigm that relies on functions modeled on mathematical functions [...] programs are a combination of expressions".

Expressions: "include concrete values, variables, and also functions".

Functions: are "expressions that are applied to an argument or input, and once applied, can be reduced or evaluated". And they define a "relation between a set of possible inputs and a set of possible outputs".

Lambda expressions

In lambda calculus we have: expressions, variables and abstractions. Where expressions are a superset as they can be variables, abstractions or a combination of them.

Variables: names for potential inputs of functions.

Abstractions: functions. A lambda term consisting of a head (the lamda) and a body, and it's applied to an argument.

Argument: a value.

Application: applying a lambda function to an argument.

λx.x
Enter fullscreen mode Exit fullscreen mode

Let's break that down 2:

λ x . x
^─┬─^
  └────── extent of the head of the lambda.

λ x . x
  ^────── the single parameter of the
          function. This binds any
          variables with the same name
          in the body of the function.

λ x . x
      ^── body, the expression the lambda
          returns when applied. This is a
          bound variable.
Enter fullscreen mode Exit fullscreen mode

Functions in lambda calculus are anonymous. But we still call them abstractions as they generalize a problem and it's resolution.

Alpha equivalence:

λx.x
λd.d
λz.z
Enter fullscreen mode Exit fullscreen mode

Beta reduction

"When we apply a function to an argument, we substitute the input expression for all instances of bound variables within the body of the abstraction. You also eliminate the head of the abstraction, since its only purpose was to bind a variable." 3

(λx.x) 2
[x ∶= 2]
2
Enter fullscreen mode Exit fullscreen mode

The argument can be anything, even other lamda:

(λx.x)(λy.y)
[x ∶= (λy.y)]
λy.y
Enter fullscreen mode Exit fullscreen mode

Applications in the lambda calculus are left associative:

(λx.x)(λy.y)z
[x := (λy.y)]
(λy.y)z
[y := z]
z
Enter fullscreen mode Exit fullscreen mode

Some variables might be free, meaning they aren't bound in the head:

λx.xy
Enter fullscreen mode Exit fullscreen mode

If we apply such lambda:

(λx.xy)z
[x := z]
zy
Enter fullscreen mode Exit fullscreen mode

Multiple arguments

"Each lambda can only bind one parameter and can only accept one argument. Functions that require multiple arguments have multiple, nested heads. When you apply it once and eliminate the first (leftmost) head, the next one is applied and so on" 4. This is called currying.

λxy.xy === λx.(λy.xy)
Enter fullscreen mode Exit fullscreen mode

Beta normal form: when you cannot beta reduce the terms any further.

λx.x    -- beta reduced, no arguments to apply

(λx.x)z -- not beta reduced, `z` hasn't been applied
Enter fullscreen mode Exit fullscreen mode

Combinator: lambda term with no free variables.

λxy.x       -- Combinator
λxyz.xz(yz) -- Combinator

λy.x        -- Not combinator
λx.xz       -- Not combinator
Enter fullscreen mode Exit fullscreen mode

Divergence: lambdas that never reduce to a normal form are said to diverge.

(λx.xx)(λx.xx)
[x := (λx.xx)]
(λx.xx)(λx.xx)
[x := (λx.xx)]
(λx.xx)(λx.xx)
[x := (λx.xx)]
(λx.xx)(λx.xx)
...
Enter fullscreen mode Exit fullscreen mode

In programming, terms that diverge don't produce an actual answer, at least not a meaningful one.

But why?

After reading this chapter the first time it was not clear to me how all of this applied to Haskell, or Functional Programming in general.

Turns out that the process of reducing the expressions (beta reduction) is how Haskell code evaluates.

The following lambda expression that reduces to z

(λabc.cba)yz(λwv.w)
[a := y]
(λbc.cby)z(λwv.w)
[b := z]
(λc.czy)(λwv.w)
[c := (λwv.w)]
(λwv.w)zy
[w := z]
(λv.z)y
[v := y]
z
Enter fullscreen mode Exit fullscreen mode

It's the equivalent of this in Haskell:

-- y = 1
-- z = 2

(\a b c -> c b a) 1 2 (\w v -> w)
(\b c -> c b 1) 2 (\w v -> w)
(\c -> c 2 1) (\w v -> w)
(\w v -> w) 2 1
(\v -> 2) 1
2
Enter fullscreen mode Exit fullscreen mode

If we run it in GHCi we get the same result:

λ> (\a b c -> c b a) 1 2 (\w v -> w)
2
Enter fullscreen mode Exit fullscreen mode

  1. Haskell Programming from First Principles, page 2. 

  2. Haskell Programming from First Principles, page 6. 

  3. Haskell Programming from First Principles, page 7. 

  4. Haskell Programming from First Principles, page 10. 

💖 💪 🙅 🚩
gillchristian
Christian Gill

Posted on February 25, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related