A closure is one of those delightfully LISPy things that turns up in AppleScript. The subject is rather advanced, though, so don't feel you have to understand everything in this section at once.
It turns out that a script object may capture certain aspects of its context, maintaining this context even though the script object may run later in a different context. For example:
property x : 5 script myScript display dialog x end script set x to 20 set dummy to myScript set x to 10 run myScript -- 20
That is extremely odd. It violates the rule stated earlier ("Free Variables") about the value of free variables being determined at runtime. By the time myScript runs, its free variable x has been set to 10, yet the dialog displays 20. The proximate cause turns out to be the mysterious line "set dummy to myScript." If that line is removed, the dialog says 10, just as we expect. Yet it is hard to see what difference that one line can make. It's not as if we ever do anything with the variable dummy, after all. We simply assign to it and forget about it. So what's going on?
The rule appears to be that the mere act of assigning a script object variable to another variableit can equally be a copy as a setcauses the script object to become a closure . A closure is a scope block plus the values of its free variables at that moment.
The example is structured in three parts so as to demonstrate the phenomenon fully. First, a property declaration precedes the script definition; this is how x inside the script definition becomes a free variable at compile time. Then, the script object is assigned to another variable; at that moment the closure is formed. The free variable has a different value by this time, so this is the value that gets frozen into the closure. Finally, the free variable's value is changed again and the script object is executed; but the script object was already turned into a closure, so the change in the value of top-level x has no effect on it.
If, at the start of that example, we substitute a global declaration for the property declaration, the example doesn't work: we don't get a closure. Rather, the dialog displays 10, the value of x at the moment the handler is executedthe normal, nonclosure behavior. So only free variables whose value is supplied by a top-level entity can form a closure.
10.10.1. Closures and Handlers
So far, we've generated a closure in accidental circumstances. In fact, this feels like a bug; the mere act of assigning a script object to a variable hardly seems to warrant freezing that same script object into a closure, and seems to be more a trap we might fall into, causing our script to misbehave mysteriously, than a feature we would use deliberately. Now, however, let's turn our attention to a situation where we might actually like to generate a closure: when returning a script object from a handler.
Sure enough, it works. A closure is generated at the time we run the handler and generate the script object. Whatever the value of the script object's free variables are at that moment, if that value comes from a top-level entity, that's the value they retain:
property x : 5 on h( ) script myScript display dialog x end script return myScript end h set x to 10 set s to h( ) run s -- 10 set x to 20 run s -- 10 set x to 30 run s -- 10
As before, if we replace the property declaration in the first line with a global declaration, there's no closure, and the dialogs say 10, then 20, then 30. Similarly, if we put a global x declaration at the start of h, there's no closure. To get a closure, we need a top-level entity to come shining down from above into our script object's scope.
Because we're inside a handler, there's one more type of variable we need to consider. Remember, a script object inside a handler can see the handler's locals. So a handler's locals can be free variables in the script object. Can they generate a closure? Yes, they can:
on h( ) local x set x to "howdy" script myScript display dialog x end script return myScript end h set s to h( ) run s -- howdy
A handler's parameter variables are locals. This means we can feed a parameter into a handler and capture it in a closure produced by the handler:
on scriptMaker(what) script myScript display dialog what end script return myScript end scriptMaker set s to scriptMaker("Hello") run s -- Hello
Waitdo you recognize that example? It comes from "Handler and Script Object as Result" in Chapter 9, except that there's a line missing. Previously, we captured the handler's parameter in the script object by initializing a property to it:
property x : what
Now it turns out that, thanks to closures, there was no need for that line. Similarly, we could rewrite the makeFilterer example (from the same section) to use a closure, changing the name criterion to crit everywhere and eliminating this line:
property criterion : crit
I hope I'm communicating a sense of how marvelous closures are. A script like this shouldn't even be possible. The parameter what is local to the handler scriptMaker, and goes out of scopeceases to existwhen scriptMaker finishes executing. Nothing in myScript explicitly copies or stores the value of this what. Yet in the last line, a copy of myScript is executed successfully in a context where there isn't even a name what in scope. That's because, mysteriously, invisibly, it brings along its own context where there is a name what in scope. That's a closure.
10.10.2. Closures and Stored Script Objects
Closures also come into play when a compiled script file is to be executed with load script or run script (see "Persistence of Top-Level Entities" in Chapter 8). There's more to a compiled script file than meets the eye. The store script command saves a script object into a compiled script file, but it also saves that script object's context , so that if the script object has free variables, it will still work. (You can't see this context unless you use Script Debugger, but it's there.) The load script command loads this context, and the run script command runs within it.
Consider what happens when we create a compiled script file like this:
set f to (path to desktop as string) & "myScript.scpt" global greeting set greeting to "Howdy" property farewell : "Byebye" script s display dialog greeting display dialog farewell end script store script s in file f replacing yes
Both greeting and farewell are free variables in the script object s. Only the two display dialog lines are being stored in a compiled script file, but that is not enough information to make sense of these variables. Therefore their context is stored in the file as well.
With run script, the situation is simple. No script object is generated. The compiled script file runs within its stored context and that's that:
set f to (path to desktop as string) & "myScript.scpt" run script alias f -- Howdy, then Byebye
If we use load script to load the compiled script file into a script, things are more complicated. Now we have a script object, and that script object has a context within the current script as well as the context with which it was saved. How will those two contexts interact?
set f to (path to desktop as string) & "myScript.scpt" set s to load script alias f global greeting set greeting to "Bonjour" property farewell : "Au revoir" run s -- Bonjour, then Byebye
The first dialog displays the value of greeting from the new, current context. But the second dialog displays the value of farewell from the old, stored context. This result actually makes sense in light of what we already learned about closures. Recall that a free variable whose value is a top-level entity, such as a property, makes a closure; a free variable whose value is a global does not. So the value of farewell, which was stored in the compiled script file as a free variable whose value was a property, is unaffected when the compiled script is loaded into a new context. But in the case of greeting, things are different. This was a global originally, so it does not form a closure. The fact that this is a free variable, however, is remembered; and the variable remains free. When the compiled script file is loaded into our script, it looks to our script to supply a value for this free variable. The supplied value can be a top-level entity (usually a property); it can be a declared global, as in our example; it can even be an implicit global. This last fact is surprising, so I'll demonstrate:
set f to (path to desktop as string) & "myScript.scpt" set s to load script alias f set greeting to "Bonjour" property farewell : "Au revoir" run s -- Bonjour, then Byebye
The point is that there must be something named greeting at a higher level to which the script object's free variable greeting can look to obtain a definition, or there will be a runtime error saying that greeting is not defined.
The same thing happens if there is a global declaration in the stored script object. Let's say we store our script object like this:
set f to (path to desktop as string) & "myScript.scpt" script s global greeting display dialog greeting end script store script s in file f replacing yes
When we load the stored script into a new context, there's no closure for greeting. But greeting is declared global, so we can supply a value:
set f to (path to desktop as string) & "myScript.scpt" set greeting to "Howdy" run (load script alias f) -- Howdy