Magik supports Closures. Most Magik developers don’t understand or use Closures. Closures are useful.
At this point you might be asking, “What’s a Closure?”
If you don’t know, that’s not surprising because practically all Magik developers are taught to use the Object Oriented Programming (OOP) paradigm when they first learn Magik. Then as they start working on existing code, it’s OOP pretty much all the time. And closures don’t often appear in OOP.
Using procedures in a functional programming manner is not something we tend to do in Magik. In fact, we use procedures sparingly and, generally, imperatively.
However closures can help in a number of areas (such as with data privacy, currying and partial applications), so it’s beneficial to understand what they are and how they work.
Closures define what’s in a procedure’s lexical scope (in other words, it’s the procedure bundled with references to its lexical environment), but there are some surprising results surrounding that — which we’ll look at a bit later.
But first, let’s discuss lexical scope.
In Magik, if you define a local variable at the top of a method, a block in that method will have access to that variable. That’s lexical scope (blocks that are textually enclosed within other blocks have access to their outer blocks’ variables).
Look at the following code.
_global lexical_scope << _proc @lexical_scope(p_first_name) _block _local l_last_name << "Beeblebrox" _for i _over 1.upto(3) _loop write(p_first_name, " ", l_last_name) _endloop _endblock _endproc
There are two blocks in the lexical_scope procedure (one beginning on line 3 and the other starting at the loop statement in line 6). The write statement (line 8) references the parameter p_first_name and the local variable l_last_name from outside its block’s scope.
Executing the code writes the values stored in p_first_name and l_last_name to the terminal, thereby demonstrating the inner-most block can access variables in its enclosing blocks.
Magik> lexical_scope("Zaphod") Zaphod Beeblebrox Zaphod Beeblebrox Zaphod Beeblebrox Magik>
This is an example of lexical scope. It’s called, “lexical scope,” because during the lexing phase, the lexer uses the location of the variable declaration, in the source, to determine where that variable is in scope.
However this example is not a useful closure because no inner procedure is being returned or passed to something else.
An Actual Closure
Now look at this code…
_global closure << _proc @closure(p_first_name) _return _proc @innerProc(p_last_name) _import p_first_name _for i _over 1.upto(3) _loop write(p_first_name, " ", p_last_name) _endloop _endproc _endproc
We’ve modified the code slightly to return a procedure (line 4). In line 6 we have to import the p_first_name parameter because Magik doesn’t do automatic lexical scoping for procedures like other languages do for functions.
At this point you might be thinking there’s no way this code will work, because the p_first_name parameter variable will only exist during the closure procedure’s life. So when closure finishes executing, p_first_name won’t exist any longer when innerProc is eventually invoked.
That’s a reasonable assumption, but look at what happens when we execute the procedure.
Magik> z << closure("Zaphod") proc innerproc(p_last_name) Magik> z("Beeblebrox") Zaphod Beeblebrox Zaphod Beeblebrox Zaphod Beeblebrox Magik> a << closure("Arthur") proc innerproc(p_last_name) Magik> a("Dent") Arthur Dent Arthur Dent Arthur Dent Magik>
In line 1, we invoked the closure procedure, setting the first name to Zaphod, and stored a reference to the procedure it returned (innerProc) in the variable z.
In line 4 we then invoked the procedure (referenced by z) and provided the last name, Beeblebrox, to invoke the inner procedure. This wrote, “Zaphod Beeblebrox” to the terminal.
Wait a minute! Why is, “Zaphod,” still being written out when we invoke z()? How in the world does the inner procedure still have access to the outer procedure’s p_first_name variable after the outer procedure has finished executing and gone out of scope?
We did a similar thing for the variable a, in lines 9 and 12, and the same thing happened. a() remembers the value, “Arthur.”
What the heck?!?
I’ll get to the reason in a moment, but for now, look at this…
Magik> z("Dent") Zaphod Dent Zaphod Dent Zaphod Dent Magik> a("Beeblebrox") Arthur Beeblebrox Arthur Beeblebrox Arthur Beeblebrox Magik>
Whoa! It happened again! z() is remembering, “Zaphod,” and a() is remembering, “Arthur.” What’s happening here?
A closure is what’s happening.
The procedure referenced by z still remembers the variable p_first_name as being set to Zaphod although the outer scope procedure has finished executing and returned the inner procedure. So when we invoke the inner procedure, it still has access to the outer procedure’s parameter and remembers it as it was at the time the inner procedure was returned.
And that’s why the procedures referenced by z and a can write, “Zaphod” and “Arthur” respectively.
This is usually surprising to most Magik developers.
The reason for this behaviour is that procedures in Magik form closures — which is a combination of the procedure and the lexical environment (such as outer procedures’ variables and resources) attached to the procedure when it was returned.
And the reason the inner procedure can still access the p_first_name variable is because it maintains a reference to its lexical environment even after the outer procedure has gone out of scope.
An easy way to remember this is to think of a closure as a procedure wearing a backpack. The backpack contains the values of all the variables that were in its scope when the closure was created. If those variables belonged to outer blocks that went out of scope, the backpack stores their values.
Alright then… that’s pretty cool, but why are they useful?
To answer that, let’s look at one more example.
In this example we’ll see how closures can help us implement truly private data and behaviour.
We’ve all been told that encapsulating data (and behaviour) is necessary to create easily maintainable software… and that’s true, because modifying shared data results in a much higher probability of causing unintended side-effects that can lead to problems in other parts of the application.
Unfortunately when we encapsulate data inside a class, it’s not truly private. Subclasses and all sorts of methods have access to these data (even if they don’t require such access).
Closures come to the rescue. See if you can figure out what this code is doing…
_global counter << _proc @counter() _local l_count << 0 _local l_add << _proc @add(p_value) _import l_count l_count +<< p_value write("New Value: ", l_count) _endproc _return _proc @inc() _import l_add l_add(1) _endproc _endproc
Got it? Great. Let’s analyze it anyways.
In line 4 we’re setting a local variable, l_count, to 0. This will be our private variable.
In line 6, we’re defining a procedure. This will be our private procedure.
Finally, in line 15, we’re returning a procedure, inc(), that imports the l_add variable pointing to our add procedure. In line 18 we call add with an argument of 1 (that is, increment, by 1, whatever the value in l_count happens to be).
Now when we execute counter(), inc() will be returned and counter() will go out of scope. However inc() will still have access to its lexical environment (that includes add()).
Notice we didn’t import l_count, so inc() doesn’t have direct access to it, but because inc() has access to add() and add() has access to l_count, inc() has indirect access to l_count. Had we wanted direct access, we would have simply imported l_count in inc().
Now let’s invoke counter() and see what happens.
Magik> c << counter() proc inc Magik> c() New Value: 1 Magik> c() New Value: 2 Magik> c() New Value: 3 Magik> c() New Value: 4 Magik> c1 << counter() proc inc Magik> c1() New Value: 1 Magik> c() New Value: 5 Magik>
See how we create multiple counters (lines 1 and 16) and they independently maintain their correct counts. Changes to the private variable l_count in one closure does not affect the variable in the other closure. Each closure references its own instance of l_count.
Also note that since counter() has gone out of scope, l_count and add() cannot be referenced except through inc(). The data and procedure are truly private to inc().
What we’ve created is a factory procedure, counter(), that returns a public inc() procedure while hiding the private add() procedure and l_count variable. This pattern is very useful in many situations as you’ll see in other articles on this site.
There are also other uses for closures, especially in functional programming — where they are used for currying and partial application.
Closures can take some time to fully comprehend, so it’s a good idea to go through the examples carefully and play around with the code at the Magik prompt. Once you have a basic understanding, the next step is to explore more advanced ideas (such as the aforementioned currying and partial application). But hopefully this article gives you a good start.