Part 1: Go Basics + Higher-order functions + Lambda calculus

Martin Sulzmann

Overview

Hello World

package main

import "fmt"

var x int

func hi(y int) {
        fmt.Printf("hi %d\n",y)
}

func main() {
    x= 1
    hi(x)
    fmt.Printf("hello, world\n")
}

Printf similar to printf in C, see here for brief description of the C printf variant.

Go Toolchain

In most cases, our programs consist of a single file.

Simple form of type inference

package main

import "fmt"

func main() {
    var x int
    x = 1
    y := x + 1
    fmt.Printf("y = %d", y)

}

The type of y is inferred by the right-hand side.

Pretty convenient!

Languages like Haskell support full type inference where the types of functions can be inferred.

Control structures - for loop

The only control structure.

    for i := 0; i < 5; i++ {
        fmt.Printf("Value of i is now: %d \n", i)
    }

Infinite loop with break

    for {
        if j > 5 {
            break
        }
        fmt.Printf("Value of j is now: %d \n", j)
        j++

    }

Complete example

package main

import "fmt"

func main() {

    for i := 0; i < 5; i++ {
        fmt.Printf("Value of i is now: %d \n", i)
    }

    j := 0
    for {
        if j > 5 {
            break
        }
        fmt.Printf("Value of j is now: %d \n", j)
        j++

    }

}

Strings

Unlike in C, Go strings are not null terminated. In Go, strings are represented by a struct consisting of

  1. The length of the string.

  2. A pointer to the first byte.

These implementation details are hidden from the user. The user can make use of array notation to access a specific position in the string. There is a primitive len that computes the length of a string. Strings are immutable. This means they cannot be updated (but copied of course).

Next, we discuss a few examples that involve strings.

Printing a string.

    var x string = "Hello"
    y := "Hi"

    fmt.Println(x)
    fmt.Printf("%s \n", y)

Several ways to iterate over a string.

    for i := 0; i < len(x); i++ {
        fmt.Printf("%c", x[i])

    }

    for i, e := range x {
        fmt.Printf("%c", e)
        fmt.Printf("%c", x[i])

    }

// Don't care notation _
    for i, _ := range x {
        fmt.Printf("%c", x[i])

    }

    for _, e := range x {
        fmt.Printf("%c", e)

    }

Via range we obtain current position and element. The notation _ indicates that we don't care about the value.

Arrays

Out of bounds check.

    var s1 [3]string
    s1[0] = "one"
    s1[1] = "two"
    s1[2] = "three"
    // s1[3] = "four"

Short-hand array initialization

    s2 := [3]string{"one", "two", "three"}

Iteration

    for index, elem := range s1 {
        fmt.Printf("%d %s \n", index, elem)
    }

Complete example

package main

import "fmt"

func main() {

    var s1 [3]string
    s1[0] = "one"
    s1[1] = "two"
    s1[2] = "three"

    for index, elem := range s1 {
        fmt.Printf("%d %s \n", index, elem)
    }

    s2 := [3]string{"one", "two", "three"}

    fmt.Printf("%s", s2[0])
}

Slices

More flexible arrays (length may change)

    s1 := make([]string, 3)

Short-hand

    s2 := []string{"a", "b"}

Functions on slices, e.g. append

    s2 := []string{"a", "b"}
    s3 := append(s2, "c", "d", "e")

Complete example

package main

import "fmt"

func printSlice(s []string) {
    for _, elem := range s {
        fmt.Printf("%s \n", elem)
    }

}

func main() {

    s1 := make([]string, 3)

    s1[0] = "one"
    s1[1] = "two"
    s1[2] = "three"

    printSlice(s1)

    s2 := []string{"a", "b"}

    s3 := append(s2, "c", "d", "e")

    printSlice(s3)
}

Functions - Return values

func inc(i int) int
func myDiv2(x int, y int) (int, bool)
package main

import "fmt"

func inc(i int) int { return i + 1 }

// Direct reference to return values by name
// Only applies to tuple return values
func myDiv(x int, y int) (res int, status bool) {
    status = false
    if y == 0 {
        return
    }
    res = x / y
    status = true
    return
}

func myDiv2(x int, y int) (int, bool) {
    if y == 0 {
        return 0, false
    }
    return x / y, true
}

func main() {
    var res int
    var status bool
    res, status = myDiv(inc(3), 2)
    fmt.Printf("Result = %d \n", res)
    fmt.Printf("Status = %t \n", status)

    res, status = myDiv2(1, 0)
    fmt.Printf("Result = %d \n", res)
    fmt.Printf("Status = %t \n", status)
}

Strings again

We write a function to split a string into a prefix and a suffix once we see given byte.

func splitAt(x string, b byte) (string, string) {
    var r1, r2 []byte
    suffix := false

    for i, _ := range x {
        if !suffix {
            if x[i] == b {
                suffix = true
            } else {
                r1 = append(r1, x[i])
            }

        } else {
            r2 = append(r2, x[i])
        }
    }

    s1 := (string)(r1)
    s2 := (string)(r2)

    return s1, s2

}

We use slices r1 and r2 to collect the prefix and suffix. We check if we have seen the given element b yet, and then append elements in the original string to either r1 or 2. The element b is not added.

We require slices as append only operates on slices. As we wish to retrieve the result as strings, we need to include a type conversion.

Function splitAt returns two results, the prefix and the suffix. Multiple return results can be retrieved as follows.

    r1, r2 := splitAt("hel:lo", ':')

    fmt.Printf("%s %s", r1, r2)

Higher-order functions

Simple (increment) function.

func inc(i int) int { return i + 1 }

Functions can be arguments.

func apply(i int, f func(int) int) int {
    return f(i)
    }


func mapInt(f func(int) int, xs []int) []int {
    for i, e := range xs {
        xs[i] = f(e)
    }

    return xs
}

Functions can be return values.

func h1() {
    fmt.Printf("Hi\n")
}

func hello2b(x int) func() {
    return h1
}
func hello2(x int) func() {
    return func() { fmt.Printf("Hello %d \n", x) }
}

Point to note. We introduce here an annonymous function (a "lambda") via the keyword func.

Complete example

package main

import "fmt"

func inc(i int) int { return i + 1 }

func apply(i int, f func(int) int) int {
    return f(i)
}

func execute(f func()) {
    f()
}

func hello() {
    fmt.Print("Hello \n")
}

func hello2(x int) func() {
    return func() { fmt.Printf("Hello %d \n", x) }
}

func mapInt(f func(int) int, xs []int) []int {
    for i, e := range xs {
        xs[i] = f(e)
    }

    return xs
}

func main() {
    execute(hello)

    execute(hello2(2))

    fmt.Printf("%d \n", apply(1, inc))

    xs := []int{1, 2, 3}
    ys := mapInt(inc, xs)                                   // P1
    zs := mapInt(func(i int) int { return i + 1 }, xs)      // P2

// Slices are internally represented via a pointer.
// At P1, ys and xs refer effectively to the same slice.
// Hence, at P2, updating xs also updates ys

    fmt.Printf("%d %d \n", ys[0], zs[0])

}

Partial function application

Consider

func plus(x int, y int) int {
    return x+y
}

The arguments to plus must be both present.

In Go, it's possible to 'incrementally' supply addition with its arguments.

func add(x int) func(int) int {
    return func(y int) int { return x + y }
}

Function add expects an integer argument (left operand) and yields a function. This function expects another integer argument (right operand) and then yields the expected result.

Being able to supply function arguments incrementally gives us more flexibility. Here is a neat way to define the increment function.

func inc(x int) int {
    return add(1)(x)
}

Slight variation of the above.

    plus := func(x int) func(int) int {
        return func(y int) int { return x + y }
    }


    inc := plus(1)

Connection to OO

In Go functions are first-class (that is, they can appear anywwhere). This is similar to OO where objects are first-class.

For example, in an OO language, we can call a method m1 on some object o1. The result is an object on which we call another method m2.

o1.m1().m2()

Currying

Is there a difference?

func plus1(x int, y int) int {
    return x + y
}

func plus2(x int) func(int) int {
    return func(y int) int {
        return x + y
    }
}

How to transform one function into the other?

Complete source code

package main

import "fmt"

func plus1(x int, y int) int {
    return x + y
}

func plus2(x int) func(int) int {
    return func(y int) int {
        return x + y
    }
}

func curry(f func(int, int) int) func(int) func(int) int {
    return func(x int) func(int) int {
        return func(y int) int {
            return f(x, y)
        }
    }

}

func uncurry(g func(int) func(int) int) func(int, int) int {
    return func(x int, y int) int {
        return (g(x))(y)
    }
}

func main() {

    x1 := plus1(1, 2)
    x2 := (plus2(1))(2)
    p := uncurry(plus2)
    x3 := p(1, 2)
    p2 := curry(plus1)
    x4 := (p2(1))(2)

    fmt.Printf("%d %d %d %d", x1, x2, x3, x4)

}

Function types and function calls

Recall

func add(x int) func(int) int {
    return func(y int) int { return x + y }
}

We define the following.

    var f func(int) func(int) int

    var g func(int) (func(int) int)

f is a function of type func(int) func(int) int.

The type func(int) func(int) int states: Given a value of type int, we return a (function) value of type func(int) int.

In the type declaration of g we wrap the return type with parantheses. That is, we find (func(int) int). There is no harm in adding these extra parantheses but they are not strictly necessary. Hence, f and g are of the same type.

Consider

    g = add

    fmt.Printf("%d", (g(1))(2))
    fmt.Printf("%d", g(1)(2))

We find the function calls (g(1))(2) and g(1)(2). Both calls are equivalent.

The call (g(1))(2) is to be interpreted as follows:

  1. First execute the subcall g(1).

  2. As we know, g(1) yields a function value of type func(int) int.

  3. Hence, we call this function with parameter 2.

g(1) is an example of a partial function application (because we obtain another function as the result).

To avoid clutter, the convention adopted in Go (and other languages that support higher-order functions) is to allow removal of parantheses by assuming that function application is left-associative.

This means, that expressions (g(1))(2) and g(1)(2) have the same meaning.

Function closures

What's the output of the following program?

package main

import "fmt"

type Type0 func()

func main() {
    var fn Type0
    var x int = 2

    fn = func() { fmt.Printf("%d \n", x) }

    fn()

    x = 3
    fn()
}

For each call to fn there is a different result!

Unlike pure functional programming languages, the Go language is impure. Pure means that for each function call and the same input arguments, we obtain the same output. This is also referred to as referential transparency.

Type definitions and structs (named types)

Type definitions

type myint int

Introduces a new type named myint. Values of type int and myint cannot be mixed.

The type myint is structurally isomorphic to int, so we can (type) convert one into the other.

Here is the complete example.

package main

import "fmt"

type myint int

func main() {
    var i int
    var j myint

    j = 1

    i = (int)(j)
    // i = j
        // yields a type error

    j = (myint)(i)

    fmt.Printf("%d", i)
}

Structs

type rectangle struct {
    length int
    width  int
}

Defines a named type rectangle that is structurally isomorphic to a struct of two ints where both ints can be accessed via field labels.

Construction of values.

    var r1 rectangle = rectangle{1, 2}
    var r2 rectangle = rectangle{width: 2, length: 1}
    r3 := rectangle{width: 2, length: 1}

Either by position or by field reference.

Intermediate Summary

func add(x int) func(int) int {
    return func(y int) int { return x + y }
}

// func add ...            Function declaration, I'm a function with name add
// func(int) int           Function type, from int to int
// func(y int) int { ...}  Annonymous function, lambda binder and body

Some (simple) exercises

Play with the examples provided.

any, map, filter, ...

Implement the following functions. Sample solutions below.

// Yields the first element which satisfies the predicate.
// Otherwise, we return a dummy element.
// The first component of the resulting tuple indicates
// if any element has satisfied the predicate.
func any(p func(int) bool,xs []int) (bool,int)


// Map f over a list of integer values which then yields
// a list of booleans
func map(f func(int) bool,xs []int) []bool


// Filter out all elemements which satisfy the predicate.
func filter(p func(int) bool,xs []int) []int

Sample solution

package main

import "fmt"

// Yields the first element which satisfies the predicate.
// Otherwise, we return a dummy element.
// The first component of the resulting tuple indicates
// if any element has satisfied the predicate.
func any(p func(int) bool, xs []int) (bool, int) {
    for _, x := range xs {
        if p(x) {
            return true, x
        }
    }
    return false, -1
}

// Map f over a list of integer values which then yields
// a list of booleans
// 'map' predefined so use mapMy
func mapMy(f func(int) bool, xs []int) []bool {
    var ys []bool

    for _, x := range xs {
        ys = append(ys, f(x))

    }
    return ys
}

// Filter out all elemements which satisfy the predicate.
func filter(p func(int) bool, xs []int) []int {
    var ys []int

    for _, x := range xs {
        if p(x) {
            ys = append(ys, x)
        }
    }
    return ys
}

func main() {
    b, x := any(func(x int) bool { return x > 1 }, []int{0, 1, 2, 3})

    fmt.Printf("%b %d \n", b, x)

    xs := filter(func(x int) bool { return x > 1 }, []int{0, 1, 2, 3})

    for _, x := range xs {
        fmt.Printf("%d \n", x)
    }

}

Lambda calculus

The lambda calculus represents a minimal core language just consisting of functions.

Functions are first-class (can appear as arguments and as return values). Think of first-class objects!

Lambda calculus: Syntax and semantics

Terms (expressions/programs) in the lambda calculus are defined by the following (abstract) EBNF syntax.

t::=xλx.ttt t \ \ ::= \ \ x \ \ \mid \ \ \lambda x.t \ \ \mid \ \ t \ t

where

The above EBNF is ambiguous. For example, consider expression λx.xx\lambda x.x \ x. Based on the above EBNF rules there are two possible interpretations.

  1. A lambda function with formal parameter xx where xx is applied to itself in the function body.

  2. The identity function λx.x\lambda x.x applied to xx.

The first interpretation can be explained via the following (parse tree) derivation tλx.tλx.ttλx.xtλx.xx t \rightarrow \lambda x.t \rightarrow \lambda x. t \ t \rightarrow \lambda x. x \ t \rightarrow \lambda x.x \ x

whereas for the second derivation we find ttt(λx.t)t(λx.x)t(λx.x)x t \rightarrow t \ t \rightarrow (\lambda x. t) \ t \rightarrow (\lambda x.x) \ t \rightarrow (\lambda x.x) \ x

In the above derivation, parantheses "(..)" are meta-symbols to highlight the difference among the two derivations.

In examples, we therefore may make use of parantheses to avoid ambiguities, for example (λx.x)x(\lambda x.x) \ x.

The operational semantics of the lambda calculus is defined via a single rule, commonly referred to as β\beta-reduction.

(λx.t1)t2[t2/x]t1 (\lambda x. t_1) \ t_2 \rightarrow [t_2/x]t_1

In [t2/x]t1[t_2/x]t_1, we apply the substitution [t2/x][t_2/x]: In the body t1t_1, replace each occurence of the formal parameter xx by the argument t2t_2.

Commonly, we refer to λx.e\lambda x.e as a lambda-abstraction and to λx\lambda x as the lambda-binding.

For example, (λx.(λy.y))z(\lambda x. (\lambda y. y)) \ z reduces to λy.y\lambda y.y.

As it is common in programming languages, we can reuse the same variable names and apply the common rule that each use of a variable name refers to the closest binding site. For example, consider λx.(λx.x)\lambda x. (\lambda x. x) where the innermost occurence of xx refers to the inner lambda-binding.

Via some simply renaming we can always guarantee that variables are distinct. For example, λx.(λx.x)\lambda x. (\lambda x. x) can be renamed to λx.(λy.y)\lambda x. (\lambda y. y).

We generally assume that lambda-bindings always introduce fresh, distinct variables.

Details

Syntactic sugar

Function application is left associative. Hence, the term (λu.u)zx(\lambda u.u) \ z \ x is a short-hand for (((λu.u)z)x)(((\lambda u.u) \ z) \ x).

The body of a lambda abstractions extends to the right as long as possible. Hence, the term λz.(λu.u)zx\lambda z. (\lambda u.u) \ z \ x is a short-hand for λz.(((λu.u)z)x)\lambda z. (((\lambda u.u) \ z) \ x).

Free variables

fv(t)fv(t) computes all free variables in tt, i.e. those variables which are not bound by a lambda abstraction.

The definition by induction over the structure of tt is as follows:

In case of name clashes we must rename bound variables. Consider [y(λx.x)/x]λy.(λx.x)yx[y \ (\lambda x.x)/x] \lambda y.(\lambda x.x) \ y \ x:

  1. Renaming yields: [y(λv.v)/x]λz.(λu.u)zx[y \ (\lambda v.v)/x] \lambda z. (\lambda u.u) \ z \ x
  2. Substitution without name clashes yields: λz.(λu.u)z(y(λv.v))\lambda z. (\lambda u.u) \ z(y \ (\lambda v.v))

Substitution

Examples (where ee refers to a term/expression)

Evaluation strategies

There are two principles ways how to evaluate terms:

Innermost

Call-by value (CBV) = AOR with the exception that we don't evaluate under "lambda" (i.e. inside function bodies)

Difference between CBV and AOR illustrated via an example. Instead of the plain lambda calculus we make use of Go.

func inc(x int) int { return x+1 }

func f(x int) int {
    return inc(1+1) + x
}

func main() {
   f(1+2)
}

Under AOR, we can reduce f(1+2) to f(3) and inc(1+1) to inc(2).

Under CBV, we can only reduce f(1+2) to f(3) because inc(1+1) is inside a function body.

Hence, evalution under CBV proceeds as follows.

      f(1+2)
 -->  f(3)
 -->  inc(1+1) + 3
 -->  inc(2) + 3
 -->  3 + 3
 -->  6

In comparison, evaluation under AOR.

      f(1+2)
=    (\lambda x. inc(1+1) + x) (1+2)
                     ^^^
               innermost redex
 -->  (\lambda x. inc(2) + x) (1+2)
 -->  (\lambda x. inc(2) + x) 3
 --> inc(2) + 3
 ...
 -->  6

The motivation for CBV is to avoid evaluation of arguments unless they are needed to make progress during evaluation.

Consider the following variant.

func inc(x int) int { return x+1 }

func f(x int) int {
    if x == 3 {
         return 0
    }
    return inc(1+1) + x
}

func main() {
   f(1+2)
}

For this case and the call f(1+2) reducing inc(1+1) to inc(2) is unnecessary (and therefore wastefull) as we don't reach this program text during evaluation.

Outermost

Outermost, leftmost, also called Normal Order Reduction (NOR)

Call-by name (CBN) = NOR with the exception that we don't evaluate under "lambda" (i.e. inside function bodies)

Examples

We consider evaluation of (λf.λx.f(fx))((λx.x)(λx.x))(\lambda f.\lambda x. f \ (f \ x)) ((\lambda x.x) (\lambda x.x)) where \rightarrow denotes an evaluation step.

We omit the alpha-renaming step (we should actually rename bound variables but keeping the bound variables as they are is harmless for this example).

Call-by-name (CBN)

(λf.λx.f(fx))((λx.x)(λx.x))(\lambda f.\lambda x. f \ (f \ x)) ((\lambda x.x) (\lambda x.x))

λx.(λx.x)(λx.x)(((λx.x)(λx.x))x)\rightarrow \lambda x.(\lambda x.x)(\lambda x.x)(((\lambda x.x)(\lambda x.x)) x)

λx.(λx.x)(((λx.x)(λx.x))x)\rightarrow \lambda x.(\lambda x.x)(((\lambda x.x)(\lambda x.x)) x)

λx.(λx.x)((λx.x)x)\rightarrow \lambda x.(\lambda x.x)((\lambda x.x)x)

λx.(λx.x)x\rightarrow \lambda x.(\lambda x.x)x

λx.x\rightarrow \lambda x.x

Call-by-value (CBV):

(λf.λx.f(fx))((λx.x)(λx.x))(\lambda f.\lambda x. f \ (f \ x)) ((\lambda x.x) (\lambda x.x))

(λf.λx.f(fx))(λx.x)\rightarrow (\lambda f.\lambda x. f \ (f \ x)) (\lambda x.x)

λx.(λx.x)((λx.x)x)\rightarrow \lambda x.(\lambda x.x) ((\lambda x.x) x)

λx.(λx.x)x\rightarrow \lambda x.(\lambda x.x)x

λx.x\rightarrow \lambda x.x

(Non)Termination

There are lambda terms whose evaluation will not terminate:

(λx.(xx))(λx.(xx))(\lambda x. (x \ x)) (\lambda x. (x \ x))

(λx.(xx))(λx.(xx))\rightarrow (\lambda x. (x \ x)) (\lambda x. (x \ x))

(λx.(xx))(λx.(xx))\rightarrow (\lambda x. (x \ x)) (\lambda x. (x \ x))

...\rightarrow ...

Termination under CBN is more likely than termination under CBV.

Let's abbreviate (λx.(xx))(λx.(xx))(\lambda x. (x \ x)) (\lambda x. (x \ x)) by Ω\Omega. Then, the lambda term (λx.λy.x)Ω(\lambda x. \lambda y. x) \Omega terminates under CBN but not under CBV.

Short summary

CBV:

(λx.x+x)(1+3)(\lambda x. x + x) \ (1 + 3)

(λx.x+x)4\rightarrow (\lambda x. x + x) \ 4

4+4\rightarrow 4 + 4

8\rightarrow 8

CBN:

(λx.x+x)(1+3)(\lambda x. x + x) \ (1 + 3)

(1+3)+(1+3)\rightarrow (1+3) + (1+3)

4+(1+3)\rightarrow 4 + (1+3)

4+4\rightarrow 4 + 4

8\rightarrow 8

call-by-need:

(λx.x+x)(1+3)(\lambda x. x + x) \ (1 + 3)

(1+3)+(1+3)\rightarrow (1+3) + (1+3)

4+4\rightarrow 4 + 4

The difference to CBN is that once we evaluate 1+31+3 and obtain the result 44, we immediately replace all other occurrences of 1+31+3 by 44 without actually evaluating these other occurrences.

8\rightarrow 8

Further examples

Consider (λx.xy)(λx.x)(λx.x)(\lambda x. x \ y) \ (\lambda x. x) \ (\lambda x. x).

Function application is left-associative, therefore, the meaning of the above lambda term is ((λx.xy)(λx.x))(λx.x)((\lambda x. x \ y) \ (\lambda x. x)) \ (\lambda x. x).

Let's carry out the evaluation steps.

((λx.xy)(λx.x))(λx.x)((\lambda x. x \ y) \ (\lambda x. x)) \ (\lambda x. x)

=Renaming((λx1.x1y)(λx2.x2))(λx3.x3)=_{Renaming} \ \ ((\lambda x_1. x_1 \ y) \ (\lambda x_2. x_2)) \ (\lambda x_3. x_3)

((λx2.x2)y)(λx3.x3)\rightarrow \ \ ( (\lambda x_2. x_2) \ y) (\lambda x_3. x_3) because (λx1.x1y)(λx2.x2)[(λx2.x2)/x1](x1y)=(λx2.x2)y(\lambda x_1. x_1 \ y) \ (\lambda x_2. x_2) \ \rightarrow \ [(\lambda x_2. x_2) / x_1 ] (x_1 \ y) \ = \ (\lambda x_2. x_2) \ y

y(λx3.x3)\rightarrow \ \ y \ (\lambda x_3. x_3) because (λx2.x2)y[y/x2]x2=y(\lambda x_2. x_2) \ y \ \rightarrow \ [y / x_2] x_2 \ = \ y

On the other hand, the lambda term (λx.xy)((λx.x)(λx.x))(\lambda x. x \ y) \ ((\lambda x. x) \ (\lambda x. x)) evaluates to yy. Briefly, the evaluation steps are

(λx.xy)((λx.x)(λx.x))(\lambda x. x \ y) \ ((\lambda x. x) \ (\lambda x. x))

Renaming(λx1.x1y)((λx2.x2)(λx3.x3))-_{Renaming} \ (\lambda x_1. x_1 \ y) \ ((\lambda x_2. x_2) \ (\lambda x_3. x_3))

(λx1.x1y)(λx3.x3)\rightarrow \ (\lambda x_1. x_1 \ y) \ (\lambda x_3. x_3)

(λx3.x3)y\rightarrow \ (\lambda x_3. x_3) \ y

y\rightarrow \ y

Expressiveness/Church Encoding

How expressive is the lambda calculus? As expressive as a Turing machine?

Yes! But how? The lambda calculus looks rather simple. There are no numbers, booleans, no conditional, looping constructs.

Indeed, but as we will show next, we can encode data types (booleans, ...), conditional expressions, recursion.

The idea of the Church encoding (named after Alonzo Church):

Boolean operations

The idea:

The actual encoding:

which written in 'lambda notation' is

ite = λe1.λe2.λe3.e1e2e3\lambda e_1. \lambda e_2. \lambda e_3. e_1 \ e_2 \ e_3

Example: if true then e2e_2 else e3e_3 equals (λx.λy.x)e2e3(\lambda x. \lambda y. x) \ e_2 \ e_3

(λx.λy.x)e2e3(\lambda x. \lambda y. x) \ e_2 \ e_3

e2\rightarrow e_2

In detail:

(λx.λy.x)e2e3(\lambda x. \lambda y. x) \ e_2 \ e_3

= ((λx.λy.x)e2)e3((\lambda x. \lambda y. x) \ e_2) \ e_3

by adding parentheses (recall that function application is left associative)

(λy.e2)e3\rightarrow (\lambda y.e_2) \ e_3

e2\rightarrow e_2

Boolean operations example

We evaluate itetruexyite \ true \ x \ y. The expect result is xx. Let's see!

  1. Replace short-hands by actual definitions.

itetruexy=(λx.λy.λz.xyz)(λx.λy.x)xyite \ true \ x \ y = (\lambda x. \lambda y. \lambda z. x \ y \ z) \ (\lambda x. \lambda y. x) \ x \ y

  1. Rename variables such that all bound variables are distinct

(λx1.λy1.λz1.x1y1z1)(λx2.λy2.x2)xy(\lambda x_1. \lambda y_1. \lambda z_1. x_1 \ y_1 \ z_1) \ (\lambda x_2. \lambda y_2. x_2) \ x \ y

  1. Introduce parantheses

(((λx1.λy1.λz1.(x1y1)z1)(λx2.λy2.x2))x)y(((\lambda x_1. \lambda y_1. \lambda z_1. (x_1 \ y_1) \ z_1) \ (\lambda x_2. \lambda y_2. x_2)) \ x) \ y

  1. Carry out evaluation steps

(((λx1.λy1.λz1.(x1y1)z1)(λx2.λy2.x2))x)y(((\lambda x_1. \lambda y_1. \lambda z_1. (x_1 \ y_1) \ z_1) \ (\lambda x_2. \lambda y_2. x_2)) \ x) \ y

(([(λx2.λy2.x2)/x1](λy1.λz1.(x1y1)z1))x)y\rightarrow (([(\lambda x_2. \lambda y_2. x_2) / x_1] (\lambda y_1. \lambda z_1. (x_1 \ y_1) \ z_1)) \ x) \ y

=(((λy1.λz1.((λx2.λy2.x2)y1)z1))x)y= (((\lambda y_1. \lambda z_1. ((\lambda x_2. \lambda y_2. x_2) \ y_1) \ z_1)) \ x) \ y

Several redexes. We choose the outermost redex

([x/y1](λz1.((λx2.λy2.x2)y1)z1))y\rightarrow ([x / y_1] (\lambda z_1. ((\lambda x_2. \lambda y_2. x_2) \ y_1) \ z_1)) \ y

=(λz1.((λx2.λy2.x2)x)z1)y= (\lambda z_1. ((\lambda x_2. \lambda y_2. x_2) \ x) \ z_1) \ y

[y/z1](((λx2.λy2.x2)x)z1)\rightarrow [y / z_1] (((\lambda x_2. \lambda y_2. x_2) \ x) \ z_1)

=((λx2.λy2.x2)x)y= ((\lambda x_2. \lambda y_2. x_2) \ x) \ y

([x/x2](λy2.x2))y\rightarrow ([x / x_2] (\lambda y_2. x_2)) \ y

=(λy2.x)y= (\lambda y_2. x) \ y

[y/y2]x\rightarrow [y / y_2] x

=x= x

Numerals

The idea.

0=λf.λx.x0 = \lambda f. \lambda x.x

1=λf.λx.fx1 = \lambda f. \lambda x. f \ x

2=λf.λx.f(fx)2 = \lambda f. \lambda x. f \ (f \ x)

...

The successor function.

succ=λn.λf.λx.f(nfx)succ = \lambda n. \lambda f. \lambda x. f \ (n \ f \ x)

The addition function.

add=λm.λn.λf.λx.mf(nfx)add = \lambda m. \lambda n. \lambda f. \lambda x. m \ f \ (n \ f \ x)

Recursion

What about recursion?

     factorial n = if n == 0 then 1
                   else n * (factorial (n-1))

Idea:

This works!

factorial1factorial \ 1

(def)(YFac)1\rightarrow (def) \ (Y \ Fac) \ 1

(fix)(Fac(YFac))1\rightarrow (fix) \ (Fac \ (Y \ Fac)) \ 1

(def)((λfac.λn.if(n==0)then1elsen*(fac(n1))))(YFac))1\rightarrow (def) \ ((\lambda fac. \lambda n. if \ (n == 0) \ then \ 1 \ else \ n*(fac (n-1)))) (Y Fac)) \ 1

(λn.if(n==0)then1elsen*((YFac)(n1)))1\rightarrow \ (\lambda n. if \ (n==0) \ then \ 1 \ else \ n*((Y \ Fac) \ (n-1))) \ 1

1*((YFac)0)\rightarrow \ 1 * ((Y \ Fac) \ 0)

(fix)1*((Fac(YFac))0)\rightarrow (fix) \ 1 * ((Fac \ (Y \ Fac)) \ 0)

1*((λn.if(n==0)then1elsen*((YFac)(n1)))0)\rightarrow \ 1 * ((\lambda n. if \ (n == 0) \ then \ 1 \ else \ n*((Y \ Fac) \ (n-1))) \ 0)

1*1\rightarrow \ 1 * 1

1\rightarrow \ 1

More encodings (pairs, natural numbers) are possible. See Church encoding.