Differences

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

Link to this comparison view

Next revision
Previous revision
pp:2024:l07 [2024/04/09 19:02]
tpruteanu created
pp:2024:l07 [2024/05/23 12:09] (current)
tpruteanu [7.4. Natural Numbers - Church numerals]
Line 1: Line 1:
 ====== Lab 7. Lambda Calculus ====== ====== Lab 7. Lambda Calculus ======
  
-Lambda Calculus ​represent ​axiomatic system ​that can be used for very basic proofs. \\+===== 7.0. What? Why? ===== 
 + 
 +**Lambda Calculus** is universal model of computation (can be used to simulate any Turing Machine) based on function //​abstraction//​ and //​application//​. It has a very simple semantic ​that can be used to study properties of comput---- 
 +ation. \\ 
 + 
 +The first thing to take note of is that **EVERYTHING** is a function (a algorithm, the input and the output are all functions). \\ 
 + 
 +Let's start with a very simple example in Scala. 
 +<code scala> 
 +def apply(f: Int => Int, x: Int): Int = f(x) 
 +</​code>​ 
 + 
 +The first thing we need to adjust for the function to look more like Lambda Calculus is that since everything is a function, everything will be untyped. 
 +<code scala> 
 +def apply(f, x) = f(x) 
 +</​code>​ 
 + 
 +A abstraction Lambda Calculus does is that it treats all functions as //​anonymous//​ (it doesn'​t give them explicit names). 
 +<code scala> 
 +(f, x) => f(x) 
 +</​code>​ 
 + 
 +Another abstraction is that all functions are //curried// (only take 1 input and return 1 output). 
 +<code scala> 
 +f => (x => f(x)) 
 +</​code>​ 
 + 
 +Now we can re-write the function using lambda calculus syntax (instead of Scala) and we will get a valid lambda expression. \\ 
 +$ \lambda f.\lambda x.(f \ x) $ 
 + 
 +==== Formal Definition ==== 
 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: \\
 | // variable //    | $ x $           | $ x \in VARS $ | | // variable //    | $ x $           | $ x \in VARS $ |
Line 20: Line 51:
 </​note>​ </​note>​
  
-==== Free and bound variables ====+===== 7.1. Free and bound variables ​=====
 Take the following Scala snippet as an example: Take the following Scala snippet as an example:
 <code scala> <code scala>
-def f(x: Int) = x + y+def add(x: Int) = x + y
 </​code>​ </​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 $ $. In contrast, $ y $ is a // free // variable. \\+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 $ add $. 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: 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> <code scala>
-def g(x: Int, y: Int) = { +def add_all(x: List[Int], y: Int) = { 
-  def f(x: Int) = x + y +  def add(x: Int) = x + y 
-  ​f(x * y)+  ​x.map(add)
 } }
 </​code>​ </​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 $ $ is ignored ​inside $ $. \\+In this new snippet we can see that all variables are //​bounded//,​ 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 $ add\_all ​(that is a ''​List[Int]''​) ​is '​invisible' ​inside $ add $. \\
 The importance of // free // variables is that only // free occurences // of a sub-expression can be bounded by the outer expression. \\ 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 $.+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: More generally, we say that:
-  * if all occurences of a variable in a expressions ​are // bounded //, the variable is said to be // bounded // +  * if all occurences of a variable in a expression ​are // bounded //, the variable is said to be // bounded // in that expression 
-  * if one occurence of a variable in a expressions ​is // free //, the variable is said to be // free //+  * if one occurence of a variable in a expression ​is // free //, the variable is said to be // free // in that expression
  
 \\ \\
Line 47: Line 78:
 **7.1.1. ** For every variable occurence, mention if it's a // free // or a // bounded // occurence: **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 y.(\lambda x.x \ (x \ y)) $
-  - $ \lambda x.(x \ \lambda y.(x \ y \ z)) \ (x \ \lambda y.x) $ +  - $ (\lambda x.(x \ \lambda y.((x \ y\ z)) \ (x \ \lambda y.x)) $ 
-  - $ \lambda f.((\lambda x.(f \ (x \ x))) \ (\lambda x.(f \ (x \ x)))) $+  - $ \lambda f.(\lambda x.f \ (x \ x)) \ (\lambda x.f \ (x \ x)) $
  
-==== Reduction rules ====+===== 7.2. 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] $: 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] $:
Line 59: Line 90:
 | $ \lambda x.e $ | $ \lambda x.e $ | | | $ \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 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 $ ) |+| $ \lambda y.e $ | $ \{\lambda z.e[y \ / \ z]\}[x \ / \ e_2] $ | $ x \neq y $, $ y $ appears // free // in $ e_2 $| ( $ z $ is a new variable that is not free in $ e $ or $ e_2 $ ) |
  
 ==== Evaluation order ==== ==== Evaluation order ====
Line 72: Line 103:
  
 <note important>​ <note important>​
-A expression of the form $ \lambda x.e_1 \ e_2 $ is also called a **redex** (reducible expression)+A expression of the form $(\lambda x.e_1 \ e_2)$ is also called a **redex** (reducible expression)
 </​note>​ </​note>​
  
 **Exercise** \\ **Exercise** \\
  
-**7.1.2. ** Evaluate in both **Normal Order** and **Applicative Order** the following expressions:​ +**7.2.1. ** 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.\lambda y.\lambda z.((x \ z) \ y) \ \lambda x.\lambda y.x) $ 
-  - $ \lambda x.y \ (\lambda x.(x \ x) \ \lambda x.(x \ x))$+  - $ ((\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) ​====+===== Lambda calculus as a programming language ​=====
  
 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). \\ 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]]. \\ 
  
 +How can this be? Everything in Lambda Calculus is a function, there are no numbers to compute //stuff// with. Well, while there are not the numbers we are used to, we can define **higher-order functions** that are analogs for concepts we are familiar with and use them instead. \\
 +
 +The representations we are going to present further are also called **Church encodings**,​ because they were first used by Alonzo Church, the inventor of Lambda Calculus.
 +
 +==== 7.3. Booleans ====
 +
 +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. \\
 +
 +$ TRUE = \lambda x.\lambda y.x$ \\
 +$ FALSE = \lambda x.\lambda y.y$ \\
 +
 +<​note>​
 +As we defined it, **TRUE** is sometimes called the **K**-Combinator (or //​Kestrel//​),​ and **FALSE** the **KI**-Combinator (or //Kite//). \\
 +{{:​pp:​2024:​kestrel.jpg?​nolink&​200|}}
 +{{:​pp:​2024:​kite.jpg?​nolink&​200|}}
 +</​note>​
 +
 +Some common operation on booleans (that were discussed during the lecture) are: \\
 +\\
 +$ AND = \lambda x.\lambda y.((x \ y) \ x) $ \\
 +$ OR = \lambda x.\lambda y.((x \ x) \ y) $ \\
 +$ NOT = \lambda x.((x \ FALSE) \ TRUE) $ \\
 +
 +<​hidden>​
 +<​note> ​
 +**NOT** can also be written as: \\
 +\\
 +$ NOT = \lambda x.\lambda a.\lambda b.((x \ b) \ a) $ \\
 +\\
 +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//​). \\
 +{{:​pp:​2024:​cardinal.jpg?​nolink&​200|}}
 +</​note>​
 +</​hidden>​
 +
 +----
 +
 +**Exercises** \\
 +**7.3.1.** Define the $ XOR $ operations over booleans.
 +
 +**7.3.2.** Define the $ NAND $ operations over booleans.
 +
 +**7.3.3.** Define the $ NOR $ operations over booleans.
 +
 +==== Pairs - Lecture Reminder ====
 +We can also encode // data structures //. We will only look at one of the simpler ones, the **pair**. \\
 +A pair encapsulates two variables together, that we can later access using $ FIRST $ and $ SECOND $ . \\
 +\\
 +$ PAIR = \lambda a.\lambda b.\lambda z.((z \ a )\ b) $ \\
 +$ FIRST = \lambda p.(p \ TRUE) $ \\
 +$ SECOND = \lambda p.(p \ FALSE) $ \\
 +
 +<​note>​
 +The $ PAIR $ higher-order function we defined is also called the **V**-Combinator (or //Vireo//). \\
 +{{:​pp:​2024:​vireo.jpg?​nolink&​200|}}
 +</​note>​
 +
 +==== 7.4. Natural Numbers - Church numerals ====
 +
 +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**. \\
 +\\
 +$ 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>​
 +
 +You can also define operation on church numerals, some (that were discussed during the lecture) are: \\
 +\\
 +$ 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)) $ \\
 +\\
 +
 +----
 +**Exercises** \\
 +
 +**7.4.1.** Define multiplication under church numerals: $ MULT = \lambda n.\lambda m. \ ... $ (**Hint:** you can do it without the **Y**-Combinator)
 +
 +**7.4.2.** Define exponentiation under church numerals: $ EXP = \lambda n.\lambda m. \ ... $
 +
 +**7.4.3. (*)** Define the predecessor operator, that takes a number and returns the number prior to it. What's the predecessor of 0? Evaluate $ (PRED \ N0) $.
 +
 +**Solution:​** \\
 +<​hidden>​
 +Let's start with defining a // shift-and-increment // operator: \\
 +$ \phi' = \lambda x.(PAIR \ x \ (SUCC \ x)) $ \\
 +\\
 +This takes a number $ n $, and returns a pair made up of the number and it's succesor ( $ n $ , $ (SUCC \ n) $ ). \\
 +\\
 +To make this function be able to be iterated multiple times (on itself), we make the input another pair, where the second value is the '​real'​ input: \\
 +$ \phi = \lambda p.((PAIR \ (SECOND \ p)) \ (SUCC \ (SECOND \ p))) $ \\
 +\\
 +This takes a pair ( $ n $, $ (SUCC \ n) $) and returns another pair ($ (SUCC \ n) $, $ (SUCC \ (SUCC \ n)) $
 +\\
 +Now we can just iterate this **n** times starting with $ N0 $, and we get a pair ($ n - 1 $, $ n $), where the first value is our predecesor: \\
 +$ PRED = \lambda n.(FIRST \ ((n \ \phi) \ (PAIR \ N0 \ N0))) $ \\
 +\\
 +An alternative solution, that uses a value container is the following (unfortunately,​ we will not explain this in further detail here): \\
 +$ PRED = \lambda n.\lambda f.\lambda x.(((n \ (\lambda g.\lambda h.(h \ (g \ f)))) \ \lambda u.x) \ \lambda v.v) $ \\
 +\\
 +</​hidden>​
 +
 +\\
 +
 +**7.4.4.** Define substraction under church numerals: $ SUB = \lambda n.\lambda m. \ ... $ (**Hint**: use $ PRED $). What happens if you try to substract a bigger number from a smaller one? Evaluate $ (SUB \ N1 \ N2 )$.
 +
 +**7.4.5.** Define $ LEQ $ (less or equal). $ LEQ \ n \ m $ should return **TRUE** if $ n \leq m $ and **FALSE** if $ n > m $.
 +
 +**7.4.6.** Define $ EQ $ (equality). $ EQ \ n \ m $ should return **TRUE** if $ n = m $ and **FALSE** otherwise.
 +
 +==== 7.5. Recursion and the Sage Bird ====
 +
 +In lambda calculus, recursion is achieved using the fixed-point combinator (or **Y** combinator, // "​Why"​ // bird or //Sage bird//). 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 $ . And by repeated application:​ $ fix \ f = f \ (f \ (... f \ (fix \ f)...)) $ \\
 +\\
 +The **Y**-combinator in lambda calculus looks like this: \\
 +\\
 +$ FIX = \lambda f.(\lambda x.f \ (x \ x)) \ (\lambda x.f \ (x \ x)) $
 +
 +----
 +**Exercises** \\
 +
 +**7.5.1. (*)** Using the **Y**-Combinator,​ define a function that computes the factorial of a number **n**.
 +
 +**7.5.2. (*)** Using the **Y**-Combinator,​ define a function $ FIB $ that computes the **n**-th fibonacci number.