Okay. In this segment I want to start talking about the last big language feature we need before we sort of understand the basics of ML programming. And that's the ability to introduce local variables which we're going to do with let-expressions. But before we get to that, let's just review kind of where we are and how far we've come. We've seen a bunch of different types of data, ints and bools. We've seen tuples built out of smaller things, lists. We've seen functions that take arguments and return a result. We understand how environments work, at least that top level and with function bindings. And for each of functions and tuples and lists, we know how to build them using some language constructs. We know how to use them, either to call a function or to access the pieces of either a tuple or a list. So what we don't know how to do yet is how to define local variables, to put variables inside of a function that can only be used in that function. And that can be very good style and extremely convenient. And in this segment, we're going to go over the basics of how to do that. And then in the next segment, we're going to show that you can use the exact same language construct to put one function inside of another, which is a great idea that unfortunately you see in far too few programming languages. Then the segment after that, we'll discuss efficiency and learn that there are situations where you really need local variables and let bindings, in order to write a reasonable algorithm. But through all of this, the thing I'm going to emphasise is that all we're going to add to our language is a single new kind of expression. It's just an expression. We don't have to add anything else to our language and it's going to capture all the ideas we need for all of these purposes. So without further ado, let me show you the expression. I'll give you the formal definition first and then we'll write some code. Just some silly examples showing how to use it. So the syntax of a let expression involves three keywords: let, in and end. And between the let and the in, you can put any number of bindings. So these aren't expressions, they're actually bindings. Just like we've been putting at the top level of our program, we're now going to be able to put those in any let expression. And since they're an expression that means we, let expressions are expressions. That means we have a way to put bindings pretty much anywhere in our program. And then between in and end, we have one more expression which I'll call the body of the let expression. Let me talk about the evaluation rules next then, we'll come back to type checking. The idea is that we're just going to evaluate each binding in order, just like we would if those bindings were at the top level of the program. So each binding will be usable in the bindings that follow it, but not the earlier ones. And then, they are all usable in the body. So we evaluate each binding in order, then the body e. And the result of that body e will be the result of the entire let-expression. And those bindings will have no effect on any environment except in this let-expression. So given those evaluation rules, type checking works pretty much the same way. We're going to type check each binding in order, use that new static environment for type checking the other bindings. We'll allow all those bindings to be used in type checking the body, and then the type of the body e will be the type of the entire let-expression. Okay. So that's really all there is to it. How about we write some code to sort of make this concrete and understand it. I don't claim that this code is going to do anything. So how about I just call it silly_one since I'll write a couple of functions here. Just write a little function that takes an int. And here as we know, this function body can be any expression e. How about I make it a let expression? Okay. So it's going to have this form: let, in, end. And I'm going to have some bindings here. How about 'val x equals if z greater than zero'? So when I go to evaluate this expression here, type check it, I can use all the bindings that are already in the environment here. So z or anything that came earlier in the file would be fine. And then when I do another binding, say val y, I can use x. I can use z. I can use whatever expression I want. And so that seems like a perfectly reasonable expression for binding to y. So when we evaluate this, we'll evaluate x to the result of if z greater than zero then x else 34'. Then we'll evaluate y to be, x plus z plus nine. And then our body can use x and y and z and everything else so it could just be something like this. And now the type of this whole thing. So we want to end up having type 'int arrow int' because its body has type int and that's because the body of the let-expression has type int. And that's because the body is an if expression, and both the Then branch and the Else branch have type int. All right, so that's an example of a let-expression. Let's do one more. So let's again have our function body be a let-expression. But now let's really emphasise that we can put let-expressions anywhere. So what if this body were an addition expression? So we know addition is going to take two arguments and these could be let-expressions if we want. You can put a let-expression anywhere you can put an expression. So what if I did something like, val x equal two in x plus one end? How is that going to work? Well, I'm going to be in an environment where x is bound to one. But then when I evaluate this, I'll create an inner environment where this x shadows the outer x. So x will be two when I go to evaluate this body. So, x plus one' will be three and this entire let-expression will evaluate to three. The outer x will simply be irrelevant because I shadowed it. What if I had a different let-expression over here, where I had x plus two in y plus one? Well, now, there's no shadowing. So this x, in this x plus two is going to refer to this outer one. So I'll get three here, y will be bound to three. And so this body will be four. Notice that the let-expression over here only affects the bindings and expression in that let expression. That x is completely irrelevant. Over here on the right, where I have a different let-expression and the x refers to whichever one is in the environment when I go to evaluate this let-expression, so x will be one there. All right. So we can try this out real quickly. Let's try "let_expressions_dot_sml. And if I want to run, call silly_two, just pass it zero arguments, pass it a unit value there. And I get seven which is what I expected. All right. So that's let-expressions. As you can see, I'm really emphasizing that they're just expressions but they do introduce a really new thing for us and that is this idea of scope - of when is a binding in a particular environment, where in the program can we use that binding? When we only had top level bindings, we said that a binding was in the environment for the rest of the file unless it was shadowed. But now with these let-expressions, when we have bindings there, they're in scope for the later bindings and the body of that let expression and nowhere else. So it really does give us a powerful idea of local variables. But anywhere we have an expression, we can introduce a local scope with a let-expression to introduce bindings for just there. And other than that, we haven't added anything new to our language. We're reusing the whole idea of bindings and type checking and evaluation rules like we did at top level. We just are now doing it in a local way, where it only affects that let-expression and nothing outside of it. So that's your basic introduction of let-expressions and now we're going to use it, the idea, in the next couple of segments to do some additional important things.