In this video, we're going to look at encapsulation, which is the first
important way that we're going to use local.
Encapsulation is one of the most fundamental concepts in software
engineering, and this is what it's about. Imagine a large software system with 10,
20, or 50, or 100 programmers on it. Now, ask yourself, how likely is it in
that situation that two programmers are going to want to use this same function
name for two completely different functions.
It's pretty likely right? It's almost guaranteed to happen, in
fact, and that's the problem that encapsulation solves.
What encapsulation lets us do is take each part of our program and make it into
a capsule, a little package that has a bunch of internal functions and constants
constructors. And only a small number of external
functions in constants and structures. What encapsulation rests on, and I'll
explain why we call it encapsulation as we go, but what encapsulation rests on,
is the observation that with a local, the a and the b in this example don't exist
out here. If we draw the scope contour, that a and
b don't exist out here. Now why would that be useful?
Well, let's go to fs-v4, which is the file system example we did last time.
Now when we designed the functions in this file, because we had mutually
referential types We always ended up with 2 functions, with these kind of silly
names there was some data element and some data LOE.
And really, from the perspective of someone who's holding onto an element and
wants to sum all the data. They don't care that there's two
functions. They really only want one function and
they would like it not to have this silly name.
But the function they want to call some data has to have this helper.
So how can we fix that with local? Well, watch this, I'm going to do it once
quickly, and then I'll come back and do it slowly.
What I'm going to do is I'm going to say you know really the function that the
rest of the world wants to call, is a single function that operates on an
element. So I'm going to give the rest of the
world a single function that operates on an element.
And I'm going to put the two functions I wrote before as locally defined
functions. [SOUND] I'll press Cmd+I to fix my
indentation. So now the two functions I originally
defined are in there. I gotta go change the name of all these
tests. I gotta get rid of this test, because
some data [UNKNOWN] is not available at top level anymore.
[SOUND] I have to switch to Intermediate student language because I'm using local.
And let's try it. That functions now running.
Now several things are different here. I'll redo the process in a minute with
another example but several things are different here, and let me talk about
them. The first and most important is that the
two mutually recursive functions, the ones with the funny names, are now
encapsulated. They're wrapped up inside this local, and
nobody outside the local can see them. As far as the top level program is
concerned, the only function that exists is sum data.
Sum data encapsulates sum data dash element and sum data dash, dash, loe.
So that's the first difference. The, the mutually recursive functions are
encapsulated. We only see a single global function,
because of that I'm only publishing one signature.
The signature of some data, because of that I have fewer tests because some data
is the only top level function. Now in the encapsulation mechanism,
provided by industrial strength languages, you would still be able to
write unit tests for encapsulated functions.
The fact that we can't write unit tests for encapsulated functions here is just
to keep check expect simple. Being able to write unit tests for
encapsulated functions introduces a number of complexities that aren't really
interesting for us for now. So those are some observational
differences. How's this thing working?
When we call some data, for example with F1.
So we call some data. We evaluate the local, all the renaming
and lifting happens, and then the body of the local calls sum-data dash dash
element. And then that calls sum-data dash dash
LOE and back and forth, and back and forth until it gets the result.
That's produced by the body and the function, sometimes we call this line
here a trampoline because what happens is, we come into the top of some data.
Balance off the trampoline, into the mutually recursive functions.
Boing. It's a silly name, a trampoline.
I think you've the idea pretty well by now.
The computer scientists like silly names. The more, the better.
So there we go, that's a first example of encapsulation.
Now, what I'm going to do again is to do the process of converting multiple
functions into a single function with encapsulation, more slowly.
I'll work through the process more slowly.
Telling you what each step is, and then I'll leave you to do the third example on
your own. So let's go down here to this next one.
The first thing is to identify some functions that are a good candidate for
encapsulation. And what makes these functions a good
candidate for encapsulation is that there's 2 or more functions that work
together. Another way of saying is that there is 1
function that has 1 or more helper functions and the rest of the program
really only wants to call 1 function. In this case, all names operating on an
element is really the only function that the rest of the program wants to call.
All names dash, dash LOE is just a helper for all names dash element.
So the first thing is to identify a candidate that has this property.
That there is a function with one or more helpers, and the rest of the world really
only wants to call one function. Next thing to do is to think of a name
for the function that the whole world will call.
Sometimes one of the functions already has that name, sometimes it does.
In this case, I think all names dash dash element is a lousy name for the rest of
the world to use, so I'm going to make up a new name.
So I put the function header like that. Then, I say Local in an opening square
bracket, and I go after the last of the functions, and I close the square
bracket. I'm not going to fix the indentation yet;
I will in just a second. And then, I write a trampoline
[SOUND].
That calls the appropriate one of the multiple functions to get things started.
I close all the parentheses. I type Command I to fix the indentation.
So now I've done the encapsulation, I just have to clean up the paperwork.
First thing I have to do is, if there were multiple signatures, now there's
really only one. Now there's just the signature of the
function that the whole world sees. So I'll get rid of the second signature
and I'm going to clean up the tests. Oftentimes this is going to mean getting
rid of the base level tests and I'll talk about the implications of that in a
minute. I'm going to first get rid of the tests
for the function that we don't have anymore.
Then I will do a renaming of all the other tests.
I want to copy that, I'll say Find, come down here, find that, replace it with
that. I'll say re- Replace, Replace, Replace,
Replace, Replace. These stubs, you know, there's two
choices now, I could do that replace, and get rid of the second stub, get rid of
that s, that might be one thing to do. Or you could just delete the stubs
entirely. And now, of course, whenever you do
something like this, you test right away to make sure you didn't mess up.
And I didn't mess up. Now, the way I did that process so
methodically follows this notion of a refactoring.
The idea of a refactoring is, you're going to make a change to a program that
doesn't change its behavior at all. It just changes its structure.
Sometimes you want to do a refactoring and then change the behavior.
You should avoid the temptation to do both of those at the same time.
First, do the refactoring, check and test that the behavior is still correct then
change the behavior. Alright?
Move in small steps. So there we go, that's a second example.
I'm going to come back to talk about the fact that we deleted the base case test
in a minute. But first, I'm going to leave you to do
an exercise, which is to go ahead and do the encapsulation of the fine functions
in the way that I've just done. So have this started in front of you.
You should have have it in front of you and you should've been doing this all
along. But I'd like you to go ahead and do this
one now for yourself, just to practice the whole step-by-step process I did.
And to help you, I'm going to put the steps of the process back on the screen.
And you can also find them in the design recipes on the Using Local for
Encapsulation link. So there's my solution, I basically just
follow that process. I called the top level function, Find,
and I removed one signature, I removed some tests, I renamed the function in the
test. I did the encapsulation.
We did the whole thing and there we go, all tests passed.
Now, we just did this for all three of the functions operating on this arbitrary
area tree. And we also know that for functions
operating on mutually referential types, there's always going to be multiple
functions. They are always going to go together.
So that might give you an idea about the template.
It gives me an idea about the template, and here's the idea it gives me.
Which is maybe what I should do is pre-encapsulate the templates.
Maybe I should make a function like this. Maybe I should write the temple like
this, define fn for element B, local, and then both of these template functions.
Get rid of this comma in here, and now I have a pre-encapsulated template.
You could do that it would work out pretty nicely actually let's see what the
consequences of having that template are let's copy that template and let's go
back down to for example some data and pretend for a minute that we don't have
the solution. Pretend that we're just at the copy
template stage, I've now just copied the template, well, what am I going to do?
I have to rename the template. Notice, I don't have to bother to rename
the internal functions. The rest of the world isn't going to see
those internal functions so they could be called strawberry and banana for all the
rest of the program cares. It's perfectly fine for them to be called
fun for element and fun for loe. You don't have to rename them at all.
And now I just code to the examples. Let's see, if I'm summing the data, I
don't care about the name. I'm going to add the data of this
element, together with the natural mutual recursion.
The base case result is zero, and in the combination step here, I also put a plus.
And I'll run it and here I very, very quickly got to the encapsulated solution.
It's almost exactly the same as this encapsulated solution.
It's just that here, the inner functions, the encapsulated functions, still have
their template name rather than having a custom name, but as I said, that doesn't
matter. What's the advantage of doing it this way
rather than starting with separated templates?
Well the big disadvantage of doing it this way is that I wasn't able to have a
base case test, during development. Because I'm not able to write a test for
just the LOE function. I have to write a test for the whole
function. So here's what I would say about that.
At this point in the course, it's still important to have unit tests it's always
important to have unit tests. And the basic idea of testing simple
cases first is always important. But, you're getting sophisticated enough
now that you may not need to actually have the absolute base case test first.
It might be enough for you to have a simple test first, a test that's 1 away
from the base case. And if that's true for you, then you can
go ahead and. Design a function like some data using
clue encapsulated templates. If you're not comfortable with that idea,
if that gives you the willies, then don't start with pre-encapsulated templates.
Start with separate templates, start with a full set of unit tests with base case
tests in everything and then once your program works Encapsulated.
That's fine. For that matter, you don't absolutely
have to encapsulate it. But let me tell you why encapsulation
matters. Remember what I said, cars have 8 million
lines of code in them. In 8 million lines of code, how likely do
you think it is that two different programmers once wanted to use the same
function name? Is it 99% likely or 100% likely?
It's one of those two. And that's what encapsulation does.
It lets one programmer have a whole bunch of functions with whatever names they
want and they just publish to the rest of the world a small number of functions
with really good names. All of their internal functions are
totally hidden and that lets them do name, what's called name space
management, it lets them deal with this naming problem, and it also makes sure
that other programmers don't call functions that they're not Supposed to.
You know about the 2htdp image library. And really, what's going on when you say
require 2hdtp/image is, what you're saying is, you're talking to the
encapsulation mechanism built into Racket, and you're saying Hey, grab the
whole rack of stuff, that's the two HTTP image library.
And let me see only the published names. There's also going to be a lot of
unpublished names. As a general rule, when you grab a module
of code, there's far more unpublished or hidden names than published names.
And that's what a require mechanism is letting you do.
Is it's letting you say, just let me see the public names, leave all the other
ones encapsulated. So there you go.
That's the first use of local, which is to support encapsulation in the teaching
languages. Other languages have more sophisticated
encapsulation mechanisms, but the basic idea is always the same.
Take two or more functions, hide all the helpers inside a capsule so that the rest
of the world sees only the functions that are really appropriate for the rest of
the world to call.