0:00
[MUSIC]
In this segment, I want to introduce Racket's local bindings.
This is what in ML, we use let-expressions for.
Racket also has let-expressions, but it actually has a variety of them.
And that'll let us understand some interesting,
different semantics between the different kinds of let-expressions.
But before we get that let me show you an initial example,
it's one of the same examples I showed you in ML.
Let's compute the maximum number in a list, we'll assume we're past the list,
and assume that lost only has numbers.
Let's use cond which we now know.
And let's ask first, if we have an empty list.
There's no such thing as the maximum of an empty list.
So there is a built in feature in racket called error, that we can pass a string.
And if this evaluates execution will stop with this error, so that seems reasonable.
Otherwise, if we have the one element list, how do you check for
a one element list?
You check if the cdr is empty, that makes sense.
Then let's just return the car because the largest element of one element list is
that element itself.
Otherwise, we know we better not make multiple recursive calls on the same list.
We saw an exponential blow up when we did this sort of thing in ML.
So let's create a local variable to hold
1:21
the max of the cdr and then we will compete with that here.
Now before I go on, let me show you since I just introduced syntax,
let is the new special form I'm showing you.
Then you have the starting right preferences and that will eventually be
matched by this close preferences and then you have the body.
And then you close the let, okay?
And what goes here on the dot, dot, dot is one or more variable bindings.
Each one goes in parenthesis as a matter of style use square brackets,
and then it's x1 expression 1, x2 expression 2 and so on.
You can have as many of these as you want.
So this is just like in ML having a number of val bindings in the same
let-expression.
And so in this particular case, I didn't need en of them, right?
I only needed one, and so I'll just put that back up here.
And so when you have one, it's often easy to forget,
you need this extra pair of a parenthesis around it.
But you do need those extra ones, otherwise,
it just won't work syntactically.
So now I have the place where I bound my variables.
There was one here tlans, and now I just need the body.
And my body will just be,
if the tlans is greater than the first element on the list, the car of xs.
Then return the tlans, otherwise return the head of the list and
that should work as a definition of max list.
So this is just an example of showing you this let-expression, right?
Where the body is this if, and then the list of bindings is here.
And this one had one binding in it, which is right there.
So that's our example of a let expression, but now let me
do the rest of this in the slides and show you we have actually more to understand.
3:02
So it turns out racket has four different ways to define local variables.
Let, let*, letrec, and local defines.
And these varieties is actually good, they have different semantics.
And so with that different semantics,
we can use the best one that's most convenient for the code we're writing.
And which helps communicate to the person reading our code,
which of the different semantics we mean here.
And is most convenient for that computation we're doing, okay?
So this will help us learn things about scope and
environments better by seeing the two actually can get three
different reasonable definitions to the meaning of the let-expression.
Let versus let* versus letrec.
So I'll just go through of these in order, we'll start with the let.
We know that a let-expression can bind any number of local variables.
I showed that to you in Dr. Racket, but the expressions are all evaluated
in the environment from before the let-expression, okay?
So given a let-expression, we of course the body of the let-expression uses
all the variables we defined.
But it turns out the expressions that initialized
those variables do not use the earlier variables.
They are all evaluated in the environment from before the let-expression.
This is not how ML let-expressions work.
Different languages have different semantics for things.
Let's look at the example here on the slide,
this is a function that takes in a number x and doubles it.
Just does it in a silly way to emphasize the semantics that I want to.
So here when I say let x be x + 3,
this x on the right hand side is going to be the x from before the let-expression,
the parameter to the function.
So this is parameter + 3.
y with + x 2, this x will also be the parameter.
This is not how ML works.
So since it is also the parameter, y is parameter + 2.
So if I take these together, I end up with 2 times the parameter + 5.
So if I subtract 5 back out, I get twice the parameter and
that's why it's a silly double function.
So why do you want this semantics?
Well, if you're not reusing variable names here, the way this x is shadowing that.
This doesn't really matter and so if you're not doing any shadowing,
it's best style in racket to just use let.
But it's also convenient in certain other situations.
Like if you wanted to have some body where x was y and
y was x, you can write it like this.
And under ML semantics this does not work.
That x would be bound to the outer y, and then y would also be bound to the outer y.
Because under ML semantics the second x would be bound to the first one.
But under Racket's let, let x be y and y be x would properly change the bindings
of x and y to each other for the body of the let-expression, so that's let.
Now, let me show you let*.
So syntactically let* works exactly like let, it just has one more character,
it has a * after the t, okay?
And this is ML's let, okay?
The expressions are all evaluated in the environment produced from
the previous bindings.
6:31
Here, when I say + x 3, that will be the x that is the parameter to the function.
And now for
y, when I say + x 2, I use the environment created by all the earlier bindings.
So I will use the shadowed x, so y will in fact be bound to the parameter + 5.
So if I have parameter + 3, and parameter + 5, when I add those together and
subtract 8, I end up doubling the argument, so that is let*.
It's much more like ML's let-expression, okay?
Another one, so that's let and let*.
Now we do letrec syntactically it's just like the others,
except instead of running let or let*, we write L-E-T-R-E-C, letrec.
This is for, the rec is short for recursion and the expressions
are evaluated in an environment that includes all the bindings.
The earlier ones and the later ones.
7:46
Then when you call it, uses it's argument z, but also uses y, w, and x.
And x because we don't have any shadowing anywhere here,
will be the parameter to the function.
y will be the earlier binding which we could have also gotten with let*.
We could not have done that with let, and w will be the later binding.
And this is what you have to use letrec for, it will not work with let or
with let*.
So when we call f-9, z will -9,
y will be the parameter + 2, w will be the parameter + 7.
And x will be the parameter as we put it all together you end up
tripling your argument.
So why does the language have letrec?
Well you need this sort of thing for
mutually recursive functions, f calls g, g calls f.
All defined within a define like this, but we have to be careful here.
I recommend only using letrec for when you have mutually recursive functions.
It's generally not so useful otherwise.
Because the expressions are still evaluated in order.
You have to be really careful here.
What if y, instead of saying + x 2 had said + w 2?
Well w is in the environment, that's the rule of letrec.
But we have not evaluated it yet because at runtime,
when we evaluate these bindings, we do still evaluate them in order.
So Racket would not like this very much.
It turns out not to raise an error instead when it hits that x, sorry, that w.
It will return some funny undefined thing and then we will
get an error because we call plus on undefined, that causes an error, okay?
So this would be bad style, it would surely be a bug.
The reason why it works for f is because the use of w,
the forward reference, the use of the later binding is inside a function body.
And we know that when we evaluate this expression, this Lambda,
we don't evaluate the function body.
We don't evaluate function bodies untill we call them.
So we evaluate y we get parameter + 2,
we evaluate f we create a closure, we evaluate w we parameter + 7.
And now when we call that closure we created,
w has been nicely initialized we'll look it up and there will be no problem, okay?
So that is is letrec.
10:13
Here's an example where letrec is actually needed, is actually properly used.
I'll let you puzzle through this code on your own.
It's actually taking an argument and returning true if it's,
sorry, if it's taken the number mod2.
So if it's an even number it returns 0, if it's an odd number it returns 1.
It only works for non negative numbers and
it does it via these very silly mutually recursive functions.
But my point is to use letrec, so
that even is bound to a Lambda that in its body called odd.
And odd is bound to a Lambda in its body called even, so forward reference and
backward reference.
I am using the Greek letter Lambda here,
just on the slide rather than writing out the word Lambda.
It turns out you can do that in Dr. Racket, but I prefer you write it out.
So that when you submit your homework assignments,
we don't have unicode characters that don't always print well everywhere.
But if you really want to use Lambda, I suppose we will survive.
11:08
And here's another example of not using later bindings,
because you get this undefined issue that we talked about on the previous line.
So that's my discussion of letrec.
I told you there's one other way to define local variables.
So in certain positions in your function body, you can actually use define.
So for example right here,
I'm defining another version of this silly mod function.
My function body is the everything but the first line,
the function body is the second, the third line and the fourth line.
You can actually in here write define, and then have bindings.
And you can have any number of these and
those will be local variables to your function.
And the semantics is the same as letrec.
Exactly everything I told you about letrec is true for local defines as well.
It's just a different syntax as long as we are using all the constructs
we will use in this course and I prefer to use let*r and letrec.
It's the more traditional style.
It emphasizes the different semantics, but I should be honest with you.
The biggest users and designers of the Racket language now say that local
defines as the preferred style that you should use let and
lets* when you want those semantics.
But if the semantics doesn't matter, meaning the result would be the same for
all three.
Or if you actually need letrec, then they'd prefer this local define syntax.
And so on your homework, I will let you choose.
If you want to use local defines, you can.
But I find it easier to think about the traditional expressions let,
let* and letrec, okay?
So that's our introduction to scope.
Remember not only do you need to know the differences between these concerts,
to read Racket code and to write Racket code in good style.
But it's wonderful that we have a language that lets us think about three
different definitions for how let expression should work.
Because each of them is more convenient and most useful in different situations.