Kyle Simpson: Let's get into a discussion of scope enclosures. Here first, what you need to know defini Scope, where to look for things. Now, you should rightly ask yourself a question on that first slide. What are we looking for? Well, what we're looking for are going to be the variables that we reference.
Anytime you reference a variable, like my object, and use an identifier name, somebody needs to go, and do a lookup to find out where is that variable? Where does he exist? Where was he declared? And as we'll get to, there's lots of different places, lots of different scopes that, that can exist.
So that's the first question is, what are we looking for? What are those things? Well, we're looking for variables. We're looking for lexical identifiers. The other question is, who's doing the looking? And that's an even more important question. Who's doing the looking? And this is where we're going to start to maybe push your comfort zone a little bit, right off the bat, is I'm going to start to introduce some compiler terminology to you.
Now, it's not quite the same as how we compile our C++, or we compile our Java, or things like that. There are certainly some differences in the way we deal with it, the primary difference is that we don't distribute, that is we don't sent out the binary compiled form of our programs.
But that doesn't mean that it's not compiled. Now, what do I mean by an interpreter? So the difference between a compiled language and an interpreted language is, let me give you a comparison. If any of you have ever done Bash scripting. Bash is an interpreted language, which means that when it is running in line three, it has absolutely, and no idea what to expect on line four.
It hasn't even looked at line four yet, it's not there yet. It's dealing with line three. So an interpreter literally goes from top to bottom. Now there's obviously some looping mechanisms and other things that interpreters can do, through Go To and things like that. But, interpretive languages go top to bottom, just line at a time.
But at the moment, in the current standard version of the language you have, the smallest atomic unit of scope that we have in the language is the function. So here's one of those cases where I'm going to put up a slide, and I'm going to talk about this slide for quite a while.
We might be on this slide for 20 or 30 minutes. And you're looking at that like, that's crazy, this is a small amount of code. How could we possibly do that? I need to start introducing some of that compiler terminology to you, because you need to understand that the compiler is going to look at this code a bit differently than you have probably trained yourself to look at this code.
Compilation is all kinds of things like tokenizing, and all that stuff. We're going to completely hand wave and gloss over all of that stuff. But there's one important part of the compiler phase, that's really important to our discussion. And that is finding declarations of variables and functions and putting them into their appropriate scope slots.
That's the pass that we're going to think about. So what we need to do is think about this code will get passed through, once by the compiler first. And then a few microseconds later, it's going to get a second pass through. After it's been compiled, it's going to get a second pass through where it's been executed.
So that's conceptually, how we're going to need to think about this code. The first thing that you'll see there right on line one, you'll see what everybody will probably commonly recognizes a variable declaration. Var foo, we see the var keyword it means a variable declaration. We see an identifier named foo.
There's a declaration operation which we could map to saying is the var foo part, and there is an initialization operation, which is the foo equals var part. And those two operations actually happen at totally separate times. They might only be a couple microseconds apart, but they do happen at the same time.
And in fact, it's not even the same mechanism within the engine that's dealing with them. And that's the key thing we need to understand, so we need to start to look at our code with slightly different eyes than we might be used to. We might be used to thinking of that as a single statement, in fact, we need to think about it as two statements.
So the compiler is going to do a single pass, or the pass that we care about, is going to pass through looking for all of the variable declarations in all the places that it can find them. And it's not just the var declarations, but it's also these function declarations. Those are also declarations that we're going to look through.
So the first line, the compiler is going to come through and say, I see a variable declaration for an identifier called foo. Which current scope am I in? And the answer to that question is, you're in the global scope right now, as we see it in this code. Okay, I want to register the foo identifier into my current scope which happens to be the global scope.
Now, I move on. I completely ignored this thing, because that's not something I need to deal with. I move on, where's my next declaration? Okay, my next declaration is on line three, I see a function declaration with an identifier named bar. Now, as differentiated from line one where we didn't care about the assignment of the value, here we are actually going to care about the value.
We're going to care that it is actually a function that goes along with this declaration. Those really aren't kind of separated, they're treated as one sort of operation, at least in our concept here. So we're going to register the function bar into the same scope, we're currently in that scope of bar.
I took a compilers' class, a compiler theory class, I got to do something like that. If you were told to write a compiler, you would just sort of do this top down approach. You just sort of skim through the code, and when you recognize the declaration you register to whichever particular scope you're in.
So rather than compiling the contents of the function bar, we'll just skip over it, and we'll come back to it later. We'll compile the function bar whenever we are forced to, because it's been asked to be executed. So we'll defer the compilation, and we'll compile it just in time.
In a sense the compiler has to kind of make a best guess, as to how you're going to use the function. It can kind of do some inferences about types and other fancy things like that, but it has to make essentially a guess, as to how you're doing it. As opposed to the C++ compiler, it knows exactly how that function's going to work.
And so, many of the engines will make a guess, but they'll instrument that initial guess, and they'll let the function run maybe say a couple dozen times, or something like that. Then monitor how well the guess was. Did we guess correctly? Is it being used, in the way that we think it is?
If the guess was incorrect, if it turns out they didn't do a very good job of guessing, they'll throw away the compilation, and then recompile the function and the hot swap and those bits directly. So there's all kinds of, and there's way more even I am totally glossing over a bunch of There is way more to it than that.
We are going to be the naive top down compiler. So even though the real ones might not compile bar now, we're going to compile bar right now. And just as if we're being in a naive top down compiler.