 Yeah, so as we approach finals, in the past we approached the different midterms you had to be able to study guys, and my question was, will there be anything like that for the finals? Nope. So there will be, for the final, everything is fair game for the final. Okay, so just reference the other stuff. Yeah, so what we will be doing, I believe, in recitation sections, the TAs, and we've just started talking about this, but they're going to go over all of the midterms that you've had, because there are some in there that people do not do well on, so they will go through those problems from the actual midterms that you took. In addition, the undergraduate TAs have gone through the previous finals of Spring 2015 and Fall 2015, and they're going through and solving them and recording and posting those videos. So when I get those, I will post them up and post links and send them out, so you will also have access to all of that. So you get, there will be a lot of material and lots of ways to study. Okay. Which will, in turn, be graded? Before the final. It's actually in pretty good shape right now, but I think we're, I don't know if the total number is, but it's hard to do different versions, but hey guys, it's, I wouldn't say, it's like 30% graded right now? So it's actually pretty good. Any other questions? Class related? Alright, let's get back into Lambda Calculus. Alright, so on Wednesday, okay. On Wednesday, we were talking about Lambda Calculus, and we're specifically showing how we could use functions in Lambda Calculus in order to represent various concepts in programs. So specifically, we were looking at Boolean logic. So could we mimic and develop Boolean logic using only Lambda Calculus? So what did we start with? We wanted to develop Boolean logic. What did we start with? True and false. What were true and, I mean, not specifically what were they, but how did we define true and false? Somebody else. Did we define them as a 1 and a 0, like in Boolean logic? Yeah, we defined them as functions, right? So we actually defined them as functions. So we can go here and quickly review. So we have true is basically a function that takes in two parameters and always returns its first parameter. False is a function that takes in two parameters and always returns its second parameter. So this is just a definition. And another important thing in here that we haven't talked about is when we say true is equal to this, we're not giving this function a name, true. We have functions with no names. All we're doing is simple, like mathematically quality, where everywhere we see true, we can completely replace it with this and it's exactly the same thing. So the true, the t and the f symbols are really just short hands for us to express this function here. And then we saw that we can use this true and this false to build up logical functions. So here we have an and function. So we know when we did the truth table, we know that and should only return true, both parameters are true. And so we saw that by constructing and defining our and function like this, that's something that takes in two parameters. And where these two parameters are either going to be the true or the false that we defined, we can then define and. And we actually went out and saw exactly how we calculate that yes, and true and true returns true. The same with and true and false and and false true and and false false. So we walked through all of these, then we defined a not function, which takes in one true value, true or false and returns. If it's true, it returns false. If it's false, it returns true. It's exactly the not function that we would think. So that brings us up to now we have logic, right? Using and and not, we can define any of the other Boolean logics that we can do ors, XORs, whatever, any type of thing that we want, using these functions, even using these functions, or we can define new functions. But that kind of idea there is it shows you a little taste of where we are and then where we're going. So now that we have this logic, we can express complicated Boolean equations, which is what we want. But in programming, do we just want Boolean expressions? What do we use Boolean expressions for? Conditions. What do conditions do? Yeah, so they allow you to change the execution flow of your program depending on the result of some Boolean expression. So how did how does how do Turing machines do branches? How are they represented? Yeah, so Turing machine, right? Each state that the Turing machine is in depending on what the current, what is currently pointed to by the head if it's a 1 or a 0, it can do a different action. So it can take a different transition. So it can essentially read and then decide which way to move depending on that. So we need something similar here. We need some kind of branches, right? We want to be able to say, hey, if this expression is true, execute this true branch, if this expression is false, execute the false branch. So our if statement, if we think about our if statement as a function, what should it take in? A Boolean logic, so some kind of Boolean, what else? A true branch and a false branch. This actually has a lot of parallels with your project 5 that you're doing, right? You are creating these data structures that represent if statements, right? This is a very similar thing here we need to do with if branches. So we want to be able to essentially say if some condition C, then do A, otherwise do B. So it's very basic. The idea is, and so in the kind of language we looked at for Hingley-Milner, what would this if condition, what would it return? Which expression? A or B, depending on the condition, right? So lambda calculus is going to use similar ideas. So here we don't necessarily just want to execute A or B. We're going to return either the function A or the function B, whichever one it is. So what we want is some function like this. If C, then A, otherwise B. So if in this case it should be taking about three parameters, right? And depending on the condition, if the condition is true, it should return which parameter? The second one, A in this case. And if the parameter is false, which one should it take in? The third. So we want essentially if C is true, then return A. And if C is false, return B, right? This is the basic logic that we want from our hit branches. And so the beautiful thing here is that, well, our condition. So what was the semantics behind the way we defined true? True takes in is a function that takes in how many parameters? True takes in two parameters and which one does it return? The first. So what is this branch? So if this is true, which one does it return? The first one. The first one, if it's false, which one does it return? Which is basically the way we've defined true. So then we can actually define our if function essentially as the identity function. So it just takes that first argument and returns it. So we'll use that condition, whatever that condition is, if it's true, we'll choose A. If it's false, we'll choose B, based on the way we've defined true and false. Let's walk through an example. We have if true AB. So our if we're going to substitute out for lambda A dot A, just the identity function, right? We've seen this function many times. So we have a beta reduction here, right? We're going to substitute in this body, substitute all three As in this body with T. What's that going to give us? T. T. So T, true AB, what's this going to return? A. A, based on the way we defined true. And is that exactly what we wanted from the semantics here of if true AB? Similarly with false, if false AB, well we substitute if with the identity function. So we're going to substitute inside here all three As with false. We do that, we have false AB with this return. B. So we basically, so I think a couple of people asked some questions after class on Wednesday about why specifically true and false would define this way. And I kind of said it's arbitrary in some sense, right? You just have to, you have to define what is true or false and you have to use that to build everything up. But some of the benefits are here you have true that's basically doing the if function shot, right? So this means your if function doesn't have to be complicated at all. Questions here? So that's it, branching. You got branching for free. Now things are going to get a little bit weird if they weren't already. Now we get into the numbers. Something that seems so simple that you've been doing it all your life, right? We're dealing with numbers, adding things. And yet here, now we have a language. Do we have, so think back to the syntax on lambda calculus. Do we have tokens for numbers? Can we write 10, 1, 0? So an ID, so we're using ID very similarly to what we think about ID, how we've been defining it all throughout class. So it starts with an alphabetic character and then any number of alphanumeric. So even just a simple 10, right? We can't define our current lambda calculus. So earlier when we were saying plus, let's see, 10, 20, right? We can't even write this. This is not a valid lambda expression. And yet, I'm going to try to convince you that we can still do mathematical operations and we can still represent integers. But we'll probably use the tricks that we did in Boolean expressions and we'll represent them using functions. So basically we're going to create a one-to-one mapping from every number to a function. And it will be very clear by looking at the function, which number it is, and by looking at a number and figuring out which function it is. And so I believe this person is Alonzo. Alonzo Church is the one who created these numerals. So just like with Boolean logic, how you define your numbers, like how you define true and false impacts how you define ands, ors, nots, if conditions. Similar thing here. We have to define numbers and then we have to define operations on those numbers. How do we define addition? How do we define multiplication using these numbers? And so there may be other ways to represent numerals and numbers, but we're going to go with the way that Church defined it. I don't have the exact date, I want to say sometimes in the 40s, but we can look that up. So, okay, there we go. So all of our numbers will be defined as functions that take in two things. Two parameters, an F and an X. So zero will just be X, which is the second parameter. So what does this make zero equivalent to alpha equivalent to in what we've seen so far? Just X, false. So this is the alpha equivalent to false, right? False took in two parameters and returned in second parameter. This zero takes in two parameters and returns in second parameter. Which actually when we think about it makes a lot of sense when we think about how we use zero in programming languages, right? A lot of programs you can do if with a number, even in C, right? You can say if zero and zero is false and any non-zero value is true. I don't know whether that's the case. I don't think any non-zero value here will be true, but we have that nice kind of symmetry here, which is pretty cool. Okay, so zero is false. I'm going to kind of show you some and we'll build up. We'll see the pattern. So first you want to see the pattern of what the numbers mean, and then later we'll try to go with why this actually works. It's one of those things that's all interconnected. The numbers, the way they're defined, work because we can define addition and multiplication and subtraction. Subtractions look very difficult, but hey. So one is going to be X, X, so two takes in two parameters and applies the first parameter to the second parameter. Two is going to call f of X and then apply that to f. Three is going to do this three times, only that you're starting to see the pattern. Four is going to be four. f of X applied to f, applied to f, applied to f. So this is how we're defining numbers. So this is zero through four, and you can see that you give me any number. So I guess I should also preface here. We're not doing negative numbers. We're just focusing on zero and above. So you give me any number, can you construct the function that represents that number? Yeah? You just keep doing lambda f.x, and you keep applying f power many times. It's 10, it'll be 10, it's 100, it'll be 100 of those f-falls. It's going to get very messy. So now that we have that pattern, then we have to think the other way. If I give you a function, and then go back to the number, so if it's in this form, could you go back and figure out which number that represents? So that's, and it should never be the case, right? That two numbers map to the same, so we want a one-to-one mapping. So it should never be the case that two numbers map to the same function and two functions map to the same number. And I think you can convince yourself that yes, this is the case, because you keep adding fs, they will be distinct functions. Functions on here? Okay, numbers, functions, numbers. That's right. Cool, I did change these. These slides were wrong for like a good two semesters. I think it was only until the very last part of last semester that somebody mentioned that I had the parentheses wrong here. Which is a good point of disambiguation rules. If you leave this as f, f, f, f, f, x, then that would mean these s are grouped together, and then you're together with the other f and the other f and the other f, that would be left dissociative. But we don't want that, that's why we have to put the parentheses here. Cool, okay. So intuitively, what does this kind of mean? So if we have four ab, what this is going to be is basically a, use a, pass it b, the result of that pass to a, the result of that pass to a, the result of that pass to a. So we're applying first b to a, and then continually applying that result back to a, however many times the number is. As to why this works, this is something that, like I said, is all connected. If everybody get the numbers, how they look like at least, we're going to walk through this type of thing. So the very first thing, when we build up addition, well, we're building up numbers, we're going to build up first a successor function. So we want to say, give me one number, what's the successor to that? So that would be, what's the next number? It's basically, you think there's a plus one function. What does it do is it adds one. So it should be that we give it zero, it should return one. And we give it one, it should return two. We give it two, it should return three. And so we'll see we can actually use that as a building block to then build up addition. So we need a function that does this. So when we think about this, successor is going to take in one parameter, where that parameter is a number. So it'll be one of those zero through whatever. So n here is the number. So the way it's defined here, it looks like it takes in how many parameters, based on how we've talked about this. So it looks like it takes in three, but if it looks like it takes in three and it takes in one parameter, then what's it going to return? A function that takes in two parameters, and what types of functions are numbers? Functions that take in two parameters, which makes sense, because it should be taking in a number and returning a new number. So even though it looks like it's three, we'll see that what it returns is going to be a number, where a number is a function that takes in two parameters. And so the way this function is written, we have f applied to n, what's n? What was that? It is an id. In the context of this function, what does this parameter n represent? The number that we're trying to add one to. So we first take that number, we're going to pass in f and x. And so you can... So what is a number? What is one f of fx? So one is a function like this, and it's going to take return f of x. So then we have in here... Wait, I'm going to parse these out. So if I have one, doesn't matter, f of x, what's this going to return? First we're going to substitute in here all the three f's with f, so this will be f, then inside here we'll replace all the three x's with x. So it'll return... If I take this f of x, so this is one, this will be f of x. If I add another f around it in the body, what's this now the body of? Two. So the way I've defined these one functions inherently means that this successor, because a successor function just basically wraps whatever it was around another call to f. Which will then take us from one to two, because the only difference here is f, two to three, three to four. So we can walk through this starting with zero. I think that's always good. You want to start with the base case to make sure your base case works. So we have zero, lambda f dot lambda x dot x. So the successor of zero, expanding that out, is zero, lambda n dot lambda f dot x of f of n of f of x. And so I'm going to substitute into this body all three n's with zero. Do I need to worry about renaming any of these f's or x's when I'm doing my substitution here? Why not? There's no free variables in zero. In the zero function, we know in here there are no free variables here. Therefore we don't have to worry about that at all. That's why I can leave it as this zero. Because I know I can just pop that in and at any point I can expand this zero out. Okay. So now we have lambda f dot lambda x dot f of zero fx. Now the question is, do we still have more beta reductions to do here? How do we know if we have a beta reduction? We have an application, and what's important about that application? The left side is a lambda expression. So we have an application. We have zero applied with f. Zero is a lambda expression. An abstraction. So we have lambda f dot lambda x dot x. So we have a possible application here. We have a beta reduction we can make. We can apply f to zero. So let's expand that out first. So we have lambda f dot lambda x dot x. And so we're going to substitute inside here all three f's with f. There's no free f's in there. That's going to go away. And then we will substitute this x for this x. And so we'll get lambda f dot lambda x dot fx. Which is what? One. This is exactly what we wanted. And so we've just shown that the successor of zero is one. And we can keep doing this. We can do this with one. We can do successor of one. We have the same thing. We substitute in for this n with one. And then expand out the one. And we say this is lambda f dot lambda x f of x. So apply that to fx. We'll return fx. And so the result here will be lambda f dot lambda x dot f of f of x. Which is two. So the successor of one is two. And so this function defines give me any number. I'll give you that number plus one. So this actually shows that by starting with zero you can actually just keep applying the successor function to generate all of the numbers. So using this we can define all the numbers. We can define them exactly this way. And we don't even have to use the kind of hand wavy logic I did where you say you can see the pattern in here. It tells you how to find the pattern. I give you the lambda expression for zero. I give you the successor function. You can now go build any number you want. Pretty crazy, right? We can do the fun thing of addition. So we can have numbers. We can do numbers plus one. Now we want to add two numbers together. So what are some of the cases we want? We want adding zero and one to be one. So our add function and we want, whatever you want, add one and two is three. So let's think about types of this add function. So what should this add function take in? How many parameters? Two. Two. It should take in two parameters and return what? What specifically should it return? A number. A number, right? From thinking about types. It should take in two parameters. Each of those parameters should be a number defined as we define the numbers. And it should return a number defined as we define the numbers. So again, it's going to look very weird because it has to return a function that takes in two parameters, which is how we define numbers. So you can see that it's going to return a lambda f dot lambda x, which is how we've been doing numbers. So it's going to take in two numbers, an n and an m. And then it's going to return a lambda f dot lambda x, so that's all the number. And it's going to be n of f of m of f of x. So we've already kind of seen if n is one, then what's this going to return? Just f x. And so if n is zero then, in that case, then we have zero f and f of x. And so if n is zero, which what's it going to return? Well, let's just walk through it. I wanted to kind of high level that, but that's fine. Add zero and one. So we can expand out our add function, lambda n dot lambda n, lambda m f of x, n f and f of x. Very quick. Okay, so we're going to do these substitutions, so we're going to call this replacing all the n's in here with zero and only n's with one. So we have the zero, we have the one. So now how many beta reductions do we have in here? That we could do. Two, we have two. We have the one with the f and the x and the zero with the f and the other one. It doesn't matter which order we do these in, as we've said in this class, the order won't matter. Let's do the middle one, the inner one here. So we have one of f of x. So we know what that's going to return is the first one applied that many times to the second parameter. So that's going to be one. So it'll be f of x. And now we have here zero f of f of x. So we said zero was the same as what? False. So false takes in two parameters and returns what? The second one. The second one. So what's this? What's zero f of f of x going to return? f of x. f of x. And what's lambda f dot lambda x dot fx? So we just did adding zero and one is one. Isn't that crazy? Let's do add one and two. We want to make sure that that worked. So we have our numbers. Remember these are standing in for the lambda expressions of one and two. Again, we're going to substitute the n and the m. So the n's are going to go here. The n's are going to go here. So we'll have one f, two f of x. So what's two f of x going to return? And so one of f of f of x is going to return what? Yeah, so let's step through that because that may not be clear. So this should be three. It's not negative three. This is just... So we can substitute out one. We have here. Let's expand out one. So here we have... We're going to replace inside here all the f's with f to call this first parameter. Correct? And in fact, since we're just focusing on this in here, let's completely ignore this part. Let's just look at it like this. Everybody good with that? So we have first parameter and then second parameter. But we know we're left-associated. So we always do the first parameter. So we're going to substitute inside here every f with f. Cool. Now in here... So now we're going to replace inside here what? x with f of f of x. Right? So let's do this one a little bit more straightforward. So we have this. We're going to substitute x with this. And we apply that to both sides. So we apply it to the f. It doesn't change anything. And we apply it to the f. What do we do? Yeah, we just substitute it, right? There's nothing complicated here that we're doing. So that will be f applied to... And so the final result is... Which is which number? Three. We had one plus two. Let's go back to the addition. So why does this work? Cool. So let's think about this inside one. Right? So m is a number. We know a number is going to return its first parameter applied to its second parameter that many times. So if this m is two, it will be f of f of x. If it's three, it will be f of f of f of x. If it's four, it will be four f's of f of f of x. Right? And then when I apply here with this n, what is this one doing? What does this mean? So we have n of f of, let's say, whatever alpha we can call this alpha or something. Apply f that many times to alpha. So I've already applied f this many times to x. And then I'm going to apply f and more times, which is just going to add to it. So you can think of this m of f of x is going to return some f of f of x. Right? Whatever m is, in this case, it's two. And so this n, because we're using this fixed f here, says apply f and number of times to this. So when we first get this inner result, how many f's are we going to have? m number of f's. And then now I'm applying how many more f's? n. n. So my final result will be m plus n number of f's applied to x. Isn't that crazy? Yeah. I still look at this and it's like, how in the world did this person come up with this? I don't know how many days in white boards, or probably chalkboards back then, you had to have filled with this notation. Then you make one mistake on things, you got to go back and change it all and don't even have a cell phone to take out and take a picture of it. Yeah, it's crazy. But once you start kind of pulling it apart, and that's part of what we're trying to do here and what these numbers are and what they mean and how they're used and how that's used then here to do addition. So subtraction is something we're not going to cover because apparently church couldn't figure out how to do subtraction. He thought it may not have been possible. And then it took somebody else to come up with how to actually do subtraction. But we can do something else that's really fun. Multiply. Questions on addition before we go on? So multiplication, as far as types go, is going to be similar to which function. It takes in two numbers and returns a number. So we should expect a similar form. So when we see at, whatever this one was, lambda n dot lambda m dot f of x, we should expect to see a similar type of definition. And so we want to verify, we'll have the similar base cases. We want to multiply 0 and 1 to get 0. We want to multiply 1 and 2 to get 2. And we want to multiply, let's say, 2 and 5 to get 10. And it should be the case. So the other way to think about it when we look at addition, did it matter what n and m were if we swapped the parameters? No. No, because we're applying f this n many times to x, and then we're applying f n more times. So that doesn't matter which order you do that in. You'll total end up with the same number of f's of f's of f's. Cool. So this is actually going to be kind of cool. So we're going to define multiplication in terms of addition that we already have. And the essence here is, now it's kind of nice. Now we actually don't have an inner x and y. We're just saying take in two numbers, n and m. That makes this thing a little bit easier to understand. And so m do essentially, so what's this inner thing going to return? So this will add n, which will return a function that will add n to whatever its first argument is. And m times, we're going to add n first to 0. And then the result of that will be added to add n. And then the result of that will be added to add n. And the result of that will be added to add n. So we will add n how many number of times? M times, which is going to be m times n. What's another way to do it? You just add, so multiplying 2 plus 5, another way to say that is add 5 to itself twice. Add 5 to 0, you could say. So you have 5. And then take that result, add 5 to it again. You have 10. That's 5 times 2. Add 5 again. That's 15, 5 times 3. You can keep doing that, right? So we can step through this. We have multiply 0 and 1. So we know this should return 0. So we have lambda n dot lambda m, m add n 0 with 0 and 1. So we're going to replace in here all the free n's with 0 and all the free m's with 1. So we have 1 add 0, 0. And so with 1, 1 is a function that takes in 2 parameters and returns the first parameter applied once to the second parameter. So this will be add 0 to 0, which doesn't return 0. This is in there? Yes. If you said, OK, write a multiplication function you must have write out the 12 characters. So you can use it like this as long as you're doing it correctly, right? Yeah, you should always, you know, you can use these symbols that makes it a lot easier to understand semantically what you mean. But if you're using them incorrectly then... OK, let's look at a more interesting example. Multiply 1 and 2. So we have our same thing in here. We have the definition of multiplication. And we can see substituting 1 in for the n and substituting 2 in there. So we're basically going to do first add 1. So remember, the way we define numbers applies the first one f of x and then f of that result. So this is the same as add 1 to 0 and then add 1 to that result. So we have add 1 and 0, which is going to return us 1. And we have add 1 and 1, which is going to return us 2. So now we get to 2. So, and like I said you can build up subtraction and then I think when you have subtraction you can do division, which is pretty crazy. So now with this we have pretty much the basics of a very hard to write programming language but we still have, what do we have here? What have we defined in this language? We have functions, application, we need call functions and then using those two primitives, right? That is all the language provides. What have we been able to construct on top of that? Arithmetic, what else? Yeah, Boolean logic. Branches, we have branches, right? So using all these, we've been able to construct all of these things. The question is that, the question I want you to think about and I want to talk about now at the end is, is that enough? Can you do everything you can do in a program now using what we have? Another way to think about that is, is it Turing complete, right? Can you do everything you can do on a Turing machine in plan of calculus with what we've defined? What can't you do and why? Looping, so what's the purpose of looping? Yeah, it allows us to kind of go back to an earlier instruction, right? And say, start doing this. So why doesn't our language, why doesn't lambda calculus have loops? Yeah, right now we don't have any recursion, right? We don't have any way for a function to call itself, right? Which is also a form of looping. Why don't we have a way for functions to call themselves? What's lacking in our language that we're used to? Basically, yeah. Let's have y'all set it up. What was that? No names. No names, our functions don't have names, right? Normally the way we do recursion in our programming language is, we say define some function foo, and then that means that inside foo we can call itself, right? We can say call foo again, right? But here in this language we have, we have no names. Now am I lying to you because I just defined an add and a multiply function? What was that? What is it? Yeah, it's a definition, right? That's the key difference. I'm defining mult as whatever the definition is here, right? I'm defining multiply as this, and I've defined add as that, but part of recursion, or part of self-referential, is we can't use that same definition as the definition, right? Here I'm saying, what I'm essentially saying is, anywhere you see multiply, you can completely replace it with the thing on the right. Exactly, it's kind of like a macro you could fill in, right? So if that macro itself had multiply in it, you would never be able to write out the final lambda expression. So we do not, so this, even though it seems a little bit counterintuitive, where I say there's no names, but I'm giving things names, I mean that in the language itself there is no way to say, define this function foo, and then call foo inside of itself. So we have Boolean logic, we have true and false branches, we have arithmetic, and so the problem is, like we've talked about, we have no way to do loops, right? We have no way to do recursion yet. So we can't write something like the factorial function. We can't do something that is like factorial zero is one, factorial n is n times factorial n minus one, right? One of the most basic recursive definitions that we study in computer science, right? Is this is how you calculate the factorial function, and in code it's very easy because we can call factorial from the inside of its own body. And part of that is that this function, this lambda expression has a name, right? This function has a name that we can use inside of itself. So the key question to think about is, does this mean that lambda calculus now has completely failed? Do we need names? Are names an inherent part of being able to do recursion in a language that has only functions? So, where are we at? I think we're still got a couple minutes. Okay, so the idea is, how would we write this factorial function in lambda calculus? So we already have the basic building blocks, right? We already have ifs, right? We have if statements. One thing we need, we already have multiply. We'd also need subtract, and we need equality. So let's say we have a function is zero. So we have some function that will check if something's zero. If it's zero, it returns true. If it's false, it returns false. That's something we could easily write. We also, let's say we have a predecessor. So like a successor is plus one, predecessor is minus one. Let's assume that we have that. Then we want something like this, right? This would be a direct translation of that programming to lambda calculus. So factorial is lambda n. If n is zero, return one. Otherwise, multiply n with factorial of the predecessor of n. So multiply n times factorial, multiply n times factorial of n minus one. But the problem is we cannot write this function even though this seems incredibly simple because of this factorial. We're using factorial inside there. So this is not a definition, right? This is a recursive definition. So it's going to come down to, the super cool thing we'll get into on Wednesday is the y-combinator. So it turns out there is a way to do recursion in a language without names, which is crazy. So if you thought the numeric stuff was crazy, that's going to be a weird thing.