Lab 7. Lambda Calculus. Intro to Haskell

Lambda Calculus represent a axiomatic system that can be used for very basic proofs.
Given a set of variables VARS, a expression under lambda calculus can be:

variable $ x $ $ x \in VARS $
function $ \lambda x.e $ $ x \in VARS $, $ e $ is a $ \lambda $-expression
application $ (e_1 \ e_2) $ $ e_1, e_2 $ are $ \lambda $-expressions

To evaluate $\lambda$-expressions, there are two types of reduction operations:

  • $\alpha$-conversion: given a expression: $ \lambda x.e $, you can rename all occurences of x in e with y (used for avoiding name collisions).
  • $\beta$-reduction: given a expression: $ \lambda x.body \ param$, you can replace all occurences of x in body with param (We will denote this action with: $ body[x \ / \ param] $).
Be careful about the difference between $ E_1 = \lambda x.e_1 \ e_2 $ and $ E_2 = e_1[x \ / \ e_2] $. The former denotes a expression made from a application between a function and a expression , while the latter is the expression obtained applying $ \beta $-reduction to the former. We say $ E_1 $ is reducible to $ E_2 $ ($ E_1 \Rightarrow E_2 $).
We say two expressions are equal, if it is possible to get one of them from the other using only $\alpha$-conversion.

If a expression cannot be reduced further using $ \beta $-reductions, we say the expression is in $ \beta $-normal form.

Free and bound variables

Take the following Scala snippet as an example:

def f(x: Int) = x + y

We can say that the second occurence of $ x $ is bounded by the $ x $ that appears as a function parameter. When we call the function, the occurence of $ x $ is replaced by the argument that was provided to $ f $. In contrast, $ y $ is a free variable.
This code might look weird, where does $ y $ come from? What does it do? Why would we use a variable that we don't instantiate (i.e. is not bound to anything)? Well, the snippet actually comes from a broader context:

def g(x: Int, y: Int) = {
  def f(x: Int) = x + y
  f(x * y)
}

In this new snippet we can see that all variables are bounded, and the free variable from before is bounded by the outer function, but only the free variable, notice that $ x $ is still bounded by the inner function, and the $ x $ parameter of $ g $ is ignored inside $ f $.
The importance of free variables is that only free occurences of a sub-expression can be bounded by the outer expression.

Translating to lambda calculus, when reducing $ \lambda x.e_1 \ e_2 $ to $ e_1[x \ / \ e_2] $, only free occurences of $ x $ in $ e_1 $ will be replaced by $ e_2 $.

More generally, we say that:

  • if all occurences of a variable in a expressions are bounded , the variable is said to be bounded
  • if one occurence of a variable in a expressions is free , the variable is said to be free


Exercise

7.1.1. For every variable occurence, mention if it's a free or a bounded occurence:

  1. $ \lambda y.(\lambda x.x \ (x \ y)) $
  2. $ \lambda x.(x \ \lambda y.(x \ y \ z)) \ (x \ \lambda y.x) $
  3. $ \lambda f.((\lambda x.(f \ (x \ x))) \ (\lambda x.(f \ (x \ x)))) $

Reduction rules

Using what we learned from free and bounded variables, we can define a algorithm for $\beta$-reduction, given a expression $ e_1[x \ / \ e_2] $:

$ e_1 $ $ e_1[x \ / \ e_2] $ condition
$ x $ $ e_2 $
$ y $ $ y $ $ x \neq y $
$ E_1 \ E_2 $ $ E_1[x \ / \ e_2] \ E_2[x \ / \ e_2] $
$ \lambda x.e $ $ \lambda x.e $
$ \lambda y.e $ $ \lambda y.e[x \ / \ e_2] $ $ x \neq y $, $ y $ does not appear free in $ e_2 $
$ \lambda y.e $ $ \{\lambda z.e[y \ / \ z]\}[x \ / \ e_2] $ $ x \neq y $, appears free in $ e_2 $ ( $ z $ is a new variable that is not free in $ e $ or $ e_2 $ )

Evaluation order

Q: If we have multiple redexes in a expression, which one do we evaluate?

A: We can evaluate any of them, and it is guaranteed by Church-Rosser theorem that if the expression is reducible, we will eventually get the same $ \beta $-normal form.

To not just randomly choose redexes, there exist reduction strategies, from which we will use the Normal Order and Applicative Order:

  • Normal Order evaluation consist of always reducing the leftmost, outermost redex (whenever possible, subsitute the arguments into the function body)
  • Applicative Order evaluation consist of always reducing the leftmost, innermost redex (always reduce the function argument before the function itself)
A expression of the form $ \lambda x.e_1 \ e_2 $ is also called a redex (reducible expression)

Exercise

7.1.2. Evaluate in both Normal Order and Applicative Order the following expressions:

  1. $ \lambda x.\lambda y.(x \ y \ x) \ \lambda x.\lambda y.x \ (\lambda x.\lambda y.\lambda z.(x \ z \ y) \ \lambda x.\lambda y.y)$
  2. $ \lambda x.y \ (\lambda x.(x \ x) \ \lambda x.(x \ x))$

Lambda calculus as a programming language (optional)

The Church-Turing thesis asserts that any computable function can be computed using lambda calculus (or Turing Machines or equivalent models).
For the curious, a series of additional exercises covering this topic can be found here: Lambda Calculus as a programming language.

Prequisites: having a working haskell environment (Haskell Environment)

Haskell is a general-purpose, purely functional programming language, that we will use for the rest of the semester to showcase functional patterns and programming styles.

This section is designed for us to get comfortable with haskell syntax, we will use several concept that we learned in Scala, such as tail-recursion, folds and maps, but this time in a purely functional context.

A trip through time

Remember: Lab 1. Introduction to Scala

7.2.1. Implement a tail-recursive function that computes the factorial of a natural number.

fact :: Int -> Int
fact = undefined

7.2.2. Implement a tail-recursive function that computes the greatest common divisor of two natural numbers.

mygcd :: Int -> Int -> Int
mygcd a b = undefined

7.2.3. Implement the function mySqrt which computes the square root of an integer $ a $.

Lists

The following Scala syntax for working with lists, can be translated to Haskell as follows:

Scala Haskell cases Haskell pattern matching Haskell guards
def f(l: List[Int]) = l match {
  case Nil => ...
  case (x::xs) => ...
}
f l = case l of
  [] -> ...
  (x:xs) -> ...
f [] = ...
f (x:xs) = ...
f l | l == [] = ...
    | otherwise = ...

7.2.4. Implement funtions mymin and mymax that take a list of ints, and return the smallest/biggest value in the list.

7.2.5. Implement a function unique that takes a list of ints, and removes all duplicates.

7.2.6. Given a list of ints, return a list of strings where for each element, return:

  • 'Fizz' if the number is divisible by 3
  • 'Buzz' if the number is divisible by 5
  • 'FizzBuzz' if the number is divisible by 3 and 5
  • a string representation of the number otherwise

7.2.7. Extend the function from 7.2.6. with the following rules:

  • 'Bazz' if the number is divisible by 7
  • 'FizzBazz' if the number is divisible by 21
  • 'BuzzBazz' if the number is divisible by 35
  • 'FizzBuzzBazz' if the number is divisible by 105


Click to display ⇲

Click to hide ⇱

You can test 7.2.6. and 7.2.7. with the following snippet, if your function is $ f $:

f [1..n]
In Haskell, the list data type is denote by the type the list holds surrounded by square paranthesis.
[Int] -- list of ints
[Double] -- list of doubles
[[Int]] -- list of lists of ints (matrices)
 
[] -- !!! not a data type, represents the empty list (Nil in Scala)

Types in Haskell

In Haskell, functions are curried by default, i.e. a function:

f a b = ...

is the same as:

f = \a -> \b -> ...

So, if $ a $ is a Int and $ b $ a Double, and $ f $ returns a Char, it would have the following type:

f :: Int -> Double -> Char

7.2.8. Check the type signature of the following functions:

  • foldl
  • foldr
  • filter
  • map
If a function is not ambigous, ghc can infer the type signature, for educational purposes, going forward you will have to write signatures for all functions you define, this is considered good practice and helps prevent bugs.
In ghci, you can check the type of a expression with: :t

7.3.1. Implement map using foldl and foldr.

mymapl :: (a -> b) -> [a] -> [b]
mymapr :: (a -> b) -> [a] -> [b]

7.3.2. Implement filter using foldl and foldr.

myfilterl :: (a -> Bool) -> [a] -> [a]
myfilterr :: (a -> Bool) -> [a] -> [a]

7.3.3. Implement foldl using foldr.

myfoldl :: (a -> b -> a) -> a -> [b] -> a

7.3.4. Implement bubbleSort.

bubbleSort :: [Int] -> [Int]

7.3.5. Implement quickSort.

quickSort :: [Int] -> [Int]