Previous Page
Next Page

10.10. Closures

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.

Actually, AppleScript makes no attempt to decide at this point what's a free variable and what's not. It just stores the script object's entire context in the file. Thus, in this case, f ends up in the compiled script file as well. This makes no difference to how the stored script object will run, because the script object makes no reference to f. It might make a difference to you if you are strongly security-minded, though, as prying eyes with the proper tools can read the value of f. Also it can make the resulting compiled script file unnecessarily large. A trick to prevent store script from storing unwanted contextual material is to set the script object's parent to AppleScriptbut then of course no local context is stored at all (and the rest of the examples in this section will break). Storage of context is an all-or-nothing proposition.


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


Previous Page
Next Page