====== Lab 7. Lambda Calculus. Intro to Haskell ======
===== 7.1 Lambda Calculus =====
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 => 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:
- $ \lambda y.(\lambda x.x \ (x \ y)) $
- $ \lambda x.(x \ \lambda y.(x \ y \ z)) \ (x \ \lambda y.x) $
- $ \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 [[https://en.wikipedia.org/wiki/Church%E2%80%93Rosser_theorem | 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:
- $ \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)$
- $ \lambda x.y \ (\lambda x.(x \ x) \ \lambda x.(x \ x))$
==== Lambda calculus as a programming language (optional) ====
The [[https://en.wikipedia.org/wiki/Church%E2%80%93Turing_thesis | 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: [[pp:2023:haskell:l07-extra|Lambda Calculus as a programming language]]. \\
===== 7.2 Intro to Haskell =====
**Prequisites**: having a working haskell environment ([[pp: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. \\
{{ :pp:2023:haskell:haskell.png?nolink&200 |}}
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: [[pp:2023:scala:l01|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
\\
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 Brain Twisters =====
**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]