Differences
This shows you the differences between two versions of the page.
| 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[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 $). |
| + | </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 lecture) are: \\ | + | 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's 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 $ 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 ^ | ||
| + | |<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 \ (n \ \phi \ (PAIR \ N0 \ N0)) $ \\ | + | You can test **7.2.6.** and **7.2.7.** with the following snippet, if your function is $ f $: |
| + | <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 $ a $ is a ''Int'' and $ b $ a ''Double'', and $ f $ 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 a function that computes the factorial of a number **n**. | + | **7.3.3.** Implement ''foldl'' using ''foldr''. |
| + | <code haskell> | ||
| + | myfoldl :: (a -> b -> a) -> 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> | ||