Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
pp:2023:haskell:l07 [2023/04/15 15:11]
tpruteanu
pp:2023:haskell:l07 [2023/04/27 01:17] (current)
mihai.udubasa [Lambda calculus as a programming language (optional)] fix typo
Line 3: Line 3:
 ===== 7.1 Lambda Calculus ===== ===== 7.1 Lambda Calculus =====
  
-**Short RECAP:** \\ 
 Lambda Calculus represent a axiomatic system that can be used for very basic proofs. \\ 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: \\ Given a set of variables **VARS**, a expression under lambda calculus can be: \\
Line 11: Line 10:
 To evaluate $\lambda$-expressions,​ there are two types of reduction operations: 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**).   * **$\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**//​.+  * **$\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] $).
  
-==== Booleans ==== +<note warning>​ 
-We can encode boolean values **TRUE** and **FALSE** in lambda calculus as functions that take 2 values, **x** and **y**and return ​the first (for **TRUE**) or second ​(for **FALSE**value\\+Be careful about the difference between $ E_1 \lambda x.e_1 \ e_2 $ **and** ​$ E_2 = e_1[\ / \ 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 $). 
 +</​note>​
  
-TRUE = \lambda x.\lambda y.y\\ +<note important>​ 
-$ FALSE = \lambda x.\lambda y.x$ \\+We say two // expressions // are equal, if it is possible to get one of them from the other using only **$\alpha$-conversion**.
  
-<​note>​As we defined it, **TRUE** is sometimes called the **K**-Combinator (or //Kestrel//), and **FALSE** the **KI**-Combinator (or //Kite//). </​note>​+If a // expression ​// cannot be reduced further using $ \beta $**-reductions**, we say the expression is in $ \beta $**-normal form**. 
 +</​note>​
  
-<hidden+==== Free and bound variables ==== 
-Some common operation on booleans ​(that were discussed during ​the lectureare: \\+Take the following Scala snippet as an example: 
 +<code scala
 +def f(x: Int) = x + y 
 +</​code>​ 
 +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: 
 +<code scala> 
 +def g(x: Int, y: Int) = { 
 +  def f(x: Int) = x + y 
 +  f(x * y) 
 +
 +</​code>​ 
 +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. ​\\
 \\ \\
-AND = \lambda x.\lambda y.((x \ y) x) \\ +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 $. 
-OR = \lambda ​x.\lambda y.((x \ x) \ y) \\ + 
-NOT = \lambda x.((x \ FALSE) \ TRUE) $ \\+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 //
  
-<​note> ​ 
-**NOT** can also be written as: \\ 
 \\ \\
-NOT = \lambda x.\lambda ​a.\lambda ​b.((x \ b) \ a) $ \\ +**Exercise** \\ 
-\\ + 
-You can convince yourself ​that this works by evaluating ​**NOT TRUE** and **NOT FALSE**. This way of writting ​**NOT** is also called ​the **C**-Combinator ​(or //Cardinal//).+**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\\ 
 + 
 +<note important>​ 
 +A expression of the form $ \lambda x.e_1 \ e_2 $ is also called a **redex** (reducible expression)
 </​note>​ </​note>​
  
-</​hidden>​+**Exercise** \\
  
-==== Natural Numbers ====+**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))$
  
-Church numerals represent natural numbers as **higher-order functions**. Under this representation,​ the number //**n**// is a function that maps **f** to its **n-fold composition**. \\ +==== Lambda calculus as a programming language ​(optional====
-\\ +
-$ N0 \lambda f.\lambda x. x $ \\ +
-$ N1 \lambda f.\lambda x. (f \ x) $ \\ +
-$ N2 \lambda f.\lambda x. (f \ (f \ x)) $ \\ +
-...+
  
-<​note>​ Does **N0** look familiar? It's the same as **FALSE** if you rename the variables (using $\alpha$-reduction)</​note>​+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]]\\
  
-You can also define operation on church numerals, some (that were discussed during the lecture) are: \\ +===== 7.2 Intro to Haskell =====
-\\ +
-$ SUCC \lambda n.\lambda f.\lambda x.(f \ ((n \ f) \ x)) $ \\ +
-$ ISZERO ​\lambda n.((n \lambda x.FALSE) \ TRUE) $ \\ +
-$ ADD \lambda n.\lambda m.\lambda f.\lambda x.((n \ f) ((m \ f) \ x)) $ \\ +
-\\ +
-**7.1.1** Define multiplication under church numerals: $ MULT \lambda n.\lambda m. \ ... $ (without using the **Y**-combinator)+
  
-**7.1.2** Define exponentiation under church numerals$ EXP = \lambda n.\lambda m. \ ... $+**Prequisites**: having a working haskell environment ([[pp:​haskell-environment|Haskell Environment]])
  
-**7.1.3** (*) Define the predecessor operator, that takes a number and returns ​the number prior to it.+**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\\
  
-What'the predecessor ​of 0? Evaluate ​PRED \ N0 $.+{{ :​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. 
 +<code haskell>​ 
 +fact :: Int -> Int 
 +fact = undefined 
 +</​code>​ 
 +**7.2.2.** Implement a tail-recursive function that computes the greatest common divisor of two natural numbers. 
 +<code haskell>​ 
 +mygcd :: Int -> Int -> Int 
 +mygcd a b = undefined 
 +</​code>​ 
 +**7.2.3.** Implement the function ​''​mySqrt'' ​ which computes ​the square root of an integer ​$. 
 + 
 +==== Lists ==== 
 +The following Scala syntax for working with lists, can be translated to Haskell as follows: 
 +^ Scala ^ Haskell cases ^ Haskell pattern matching ^ Haskell guards ^ 
 +|<code scala> 
 +def f(l: List[Int]) = l match { 
 +  case Nil => ... 
 +  case (x::xs) => ... 
 +
 +</​code>​|<​code haskell>  
 +f l = case l of 
 +  [] -> ... 
 +  (x:xs) -> ... 
 +</​code>​ | <code haskell>​ 
 +f [] = ... 
 +f (x:xs) = ... 
 +</​code>​ | <code haskell>​ 
 +f l | l == [] = ... 
 +    | otherwise = ... 
 +</​code>​ | 
 + 
 +**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
  
-<​hidden>​ 
-$ PRED = \lambda n.\lambda f.\lambda x.(((n \ (\lambda g.\lambda h.h \ (g \ f))) \ (\lambda u.x)) \ (\lambda v.v)) $ \\ 
 \\ \\
-$ \phi = \lambda p.PAIR \ (SND \ p) \ (SUCC \ (SND \ p)) \\ +<​hidden>​ 
-PRED = \lambda.n.FST \ (\ \phi \ (PAIR \ N0 \ N0)) $ \\+You can test **7.2.6.** and **7.2.7.** with the following snippet, if your function is $
 +<code haskell>​ 
 +f [1..n
 +</​code>​
 </​hidden>​ </​hidden>​
  
-\\+<​note>​ 
 +In Haskell, the list data type is denote by the type the list holds surrounded by square paranthesis. 
 +<code haskell>​ 
 +[Int] -- list of ints 
 +[Double] -- list of doubles 
 +[[Int]] -- list of lists of ints (matrices)
  
-**7.1.4** Define substraction under church numerals: $ SUB = \lambda n.\lambda m. \ ... $ (**Hint**: use $ PRED $).+[] -- !!! not a data type, represents the empty list (Nil in Scala) 
 +</​code>​ 
 +</​note>​
  
-What happens if you try to substract a bigger number from a smaller one? Evaluate $ SUB \ N1 \ N2 $.+==== Types in Haskell ====
  
-**7.1.5** Define $ LEQ $ (less or equal)$ LEQ n m $ should return **TRUE** if $ n \leq m $ and **FALSE** if $ n m $.+In Haskell, functions are curried by default, ​**i.e.** a function: 
 +<code haskell>​ 
 +f a b = ... 
 +</​code>​ 
 +is the same as: 
 +<code haskell>​ 
 +f = \a -> \b -> ... 
 +</​code>​
  
-**7.1.6** Define ​EQ (equality). ​EQ n m should return **TRUE** if n = m and **FALSE** otherwise.+So, if is a ''​Int''​ and a ''​Double'',​ and returns a ''​Char'',​ it would have the following type: 
 +<code haskell>​ 
 +f :: Int -> Double -> Char 
 +</​code>​
  
-===== 7.2 Recursion and the Y-Combinator =====+**7.2.8.** Check the type signature of the following functions:​ 
 +  * ''​foldl''​ 
 +  * ''​foldr''​ 
 +  * ''​filter''​ 
 +  * ''​map''​
  
-In lambda calculus, recursion ​is achieved using the fixed-point combinator (or **Y** combinator)\\ +<note important>​ 
-A fixed-point combinator is a **higher-order** function that returns some fixed point of it's argument function (**x** is a fixed pointed for a function **f** if $ f(x) = x $)That means$ f \ (fix \ f= fix \ f $ \\ +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
-And by repeated application$ fix \ f = f \ (f \ (... f \ (fix \ f)...)) $ \\ +</​note>​ 
-Our combinator in lambda calculus looks like this: \\ + 
-\\ +<note tip> 
-$ FIX = \lambda f.(\lambda x.f \ (x \ x)) (\lambda x.f \ (x \ x)) $+In ''​ghci'',​ you can check the type of a expression with: '':​t''​ 
 +</​note>​ 
 + 
 +===== 7.3 Brain Twisters ===== 
 + 
 +**7.3.1.** Implement ''​map''​ using ''​foldl''​ and ''​foldr''​. 
 +<code haskell>​ 
 +mymapl :: (a -> b-> [a] -> [b] 
 +mymapr :: (a -> b-> [a] -> [b] 
 +</​code>​ 
 + 
 +**7.3.2.** Implement ''​filter''​ using ''​foldl''​ and ''​foldr''​
 +<code haskell> 
 +myfilterl :: (a -> Bool-> [a] -> [a] 
 +myfilterr :: (a -> Bool-> [a] -> [a] 
 +</​code>​
  
-**7.2.1** Using the **Y**-Combinator, define ​function that computes the factorial of number **n**.+**7.3.3.** Implement ''​foldl''​ using ''​foldr''​. 
 +<code haskell>​ 
 +myfoldl :: (a -> b -a) -> -> [b] -> a 
 +</​code>​
  
-**7.2.2** Using the **Y**-Combinator, define a function $ FIB $ that computes the **n**-th fibonacci number.+**7.3.4.** Implement ''​bubbleSort''​. 
 +<code haskell>​ 
 +bubbleSort :: [Int] -> [Int] 
 +</​code>​
  
-===== 7.3 Intro to Haskell =====+**7.3.5.** Implement ''​quickSort''​. 
 +<code haskell>​ 
 +quickSort :: [Int] -> [Int] 
 +</​code>​