BY MATT NEUBURG
Originally published in: MAC DEVELOPER JOURNAL Summer 2004
The AppleScript language should not be underestimated; for all its faults, it has some powerful abilities that many users may not be aware of. This article, reproducing two passages from my book AppleScript: The Definitive Guide, describes some surprising things one can do with handlers (subroutines).
A handler is a data type in AppleScript. This means that a variable's value can be a handler. In fact, a handler definition is in effect the declaration (and definition) of such a variable. (That variable's status is essentially the same as that of a property.) The variable's name is the name of the handler, and its value is the handler's byte code, its functionality.
A handler may thus be referred to like any other variable, and you can get and set its value, like this:
on sayHowdy() display dialog "Howdy" end sayHowdy set sayHello to sayHowdy sayHello() -- Howdy
In that example, we stored a handler as the value of a variable, and then called the variable as if it were a handler! This works because the variable is a handler.
The value of a handler can also be set. No law says you have to set it to another handler. For example, you could do this:
on sayHowdy() display dialog "Howdy" end sayHowdy set sayHowdy to 9 display dialog sayHowdy -- 9
You can set one handler to the value of another, in effect substituting one entire functionality for another. Of course, the functionality has to be defined somewhere to begin with -- for example:
on sayHowdy() display dialog "Howdy" end sayHowdy on sayHello() display dialog "Hello" end sayHello set sayHello to sayHowdy sayHello() -- Howdy
At this point, you're probably thinking: "Wow! If I can store a handler as a variable, I can pass it as a parameter to another handler!" However, this code fails with a run-time error:
on sayHowdy() display dialog "Howdy" end sayHowdy on doThis(what) what() end doThis doThis(sayHowdy) -- error
We did succeed in passing the handler sayHowdy
as a parameter to doThis()
, but now we can't seem to call it; AppleScript refuses to identify the what()
in the handler call with the what
that arrived as a parameter. This is actually another case of the rule that an unqualified handler call is a message directed to the current script object; AppleScript looks at the current script object, which is the script as a whole, for a global named what
(see page 151 of my book for further explanation of this rule).
One obvious workaround is to use such a global:
on sayHowdy() display dialog "Howdy" end sayHowdy on doThis() global what what() end doThis set what to sayHowdy doThis() -- Howdy
But globals are messy; we want a real solution. Here's one: we can pass a script object instead of a handler. This is actually very efficient because script objects are passed by reference, and it works, because now we can use the run
command instead of a handler call:
script sayHowdy display dialog "Howdy" end script on doThis(what) run what end doThis doThis(sayHowdy) -- Howdy
Alternatively, we can define a handler in a script object dynamically and then pass it. This is more involved, but it permits both the script and the handler that receives it as a parameter to be completely general:
script myScript on doAnything() end doAnything doAnything() end script on doThis(what) run what end doThis on sayHowdy() display dialog "Howdy" end sayHowdy set myScript's doAnything to sayHowdy doThis(myScript) -- Howdy
Observe that we can actually redefine a handler within a script object, from the outside!
My favorite solution is to pass a handler as parameter to doThis
, just as in our first attempt, and to have a script object inside doThis
waiting to receive it, like so:
on sayHowdy() display dialog "Howdy" end sayHowdy on doThis(what) script whatToDo property theHandler : what theHandler() end script run whatToDo end doThis doThis(sayHowdy) -- Howdy
This depends upon the remarkable rule that a script object within a handler can see that handler's local variables. Thanks to this rule, our property initialization for theHandler
can see the incoming what
parameter and store its value -- the handler. Now we are able to call the handler using the name theHandler
, because this is the name of a global (a property) within this same script object.
For a useful application of this technique, let's return to the earlier example (page 153) where we filtered a list to get only those members of the list that were numbers. The trouble with that routine is that it is not general; we'd like a routine to filter a list on any Boolean criterion we care to provide. There are various ways to structure such a routine, but the approach that I consider the most elegant is to have filter()
be a handler containing a script object, as in the example above. This handler accepts a list and a criterion handler and filters the list according to the criterion.
on filter(L, crit) script filterer property criterion : crit on filter(L) if L = {} then return L if criterion(item 1 of L) then return {item 1 of L} & filter(rest of L) else return filter(rest of L) end if end filter end script return filterer's filter(L) end filter on isNumber(x) return ({class of x} is in {real, integer, number}) end isNumber filter({"hey", 1, "ho", 2, 3}, isNumber)
I consider that example to be the height of the AppleScript programmer's art, so perhaps you'd like to pause a moment to admire it.
The result of a handler can be a script object. Normally, this script object is a copy, passed by value; it could not be passed by reference, since after the handler finishes executing there is no script object back in the handler for a reference to refer to. (Actually, if the returned script object is the same script object that was passed in as a parameter by reference, then it is returned by reference as well; still, that fact isn't terribly interesting, since at the time the script object was passed in, you must have had a reference to it to begin with.) For example:
on scriptMaker() script myScript property x : "Howdy" display dialog x end script return myScript end scriptMaker set myScript to scriptMaker() run myScript -- Howdy
In the last two lines, we acquire the script object returned by the handler scriptMaker
, and run it. Of course, if we didn't want to retain the script object, these two lines could be combined into one:
run scriptMaker() -- Howdy
A handler can customize a script object before returning it:
on scriptMaker() script myScript property x : "Howdy" display dialog x end script set myScript's x to "Hello" return myScript end scriptMaker set myScript to scriptMaker() run myScript -- Hello
In that example, the handler scriptMaker
not only created a script object, it also modified it, altering the value of a property, before returning it.
Obviously, instead of hard-coding the modification into the handler, we can pass the modification to the handler as a parameter:
on scriptMaker(s) script myScript property x : "Howdy" display dialog x end script set myScript's x to s return myScript end scriptMaker set myScript to scriptMaker("Hello") run myScript -- Hello
As already mentioned, contrary to the general rules of scoping, a script object defined inside a handler can see the handler's local variables. This means that in the previous example we can save a step and initialize the property x
directly to the incoming parameter s
:
on scriptMaker(s) script myScript property x : s display dialog x end script return myScript end scriptMaker set myScript to scriptMaker("Hello") run myScript -- Hello
The real power of this technique emerges when we retain and reuse the resulting script object. For example, here's a new version of the general list-filtering routine we wrote earlier. In that earlier version, we passed a handler both a criterion handler and a list, and got back a filtered list. In this version, we pass just a criterion handler, and get back a script object:
on makeFilterer(crit) script filterer property criterion : crit on filter(L) if L = {} then return L if criterion(item 1 of L) then return {item 1 of L} & filter(rest of L) else return filter(rest of L) end if end filter end script return filterer end makeFilterer
The script object that we get back from makeFilterer
contains a filter
handler that has been customized to filter any list according to the criterion we passed in at the start. This architecture is both elegant and efficient. Suppose you know you'll be filtering many lists on the same criterion. You can use makeFilterer
to produce a single script object whose filter handler filters on this criterion, store the script object, and call its filter
handler repeatedly with different lists:
on makeFilterer(crit) -- ... as before ... end makeFilterer on isNumber(x) return ({class of x} is in {real, integer, number}) end isNumber set numbersOnly to makeFilterer(isNumber) tell numbersOnly filter ({"hey", 1, "ho", 2, "ha", 3}) -- {1, 2, 3} filter ({"Mannie", 7, "Moe", 8, "Jack", 9}) -- {7, 8, 9} end tell
A closure is one of those delightfully LISPy things that have found their way into AppleScript. It turns out that a script object carries with it a memory of certain aspects of its context at the time it was defined, and maintains this memory even though the script object may run at a different time and in a different place. In particular, a script object returned from a handler maintains a memory of the values of its own free variables.
For example, a script object inside a handler can see the handler's local variables. So a handler's result can be a script object that incorporates the value of the handler's local variables as its own free variables. This means we can modify an earlier example one more time to save yet another step:
on scriptMaker(s) script myScript display dialog s end script return myScript end scriptMaker set myScript to scriptMaker("Hello") run myScript -- Hello
This is somewhat miraculous; in theory it shouldn't even be possible. The parameter s
is local to the handler scriptMaker
, and goes out of scope -- ceases to exist -- when scriptMaker
finishes executing. Nothing in myScript
explicitly copies or stores the value of this s
; we do not, as previously, initialize a property to it. Rather, there is simply the name of a free variable s
:
display dialog s
This s
is never assigned a value; it simply appears, in a context where it can be identified with a more global s
(the parameter s
), and so it gets its value that way. Yet in the last line, myScript
is successfully executed in a completely different context, a context where there is no name s
in scope. In essence, myScript
"remembers" the value of its free variable s
even after it is returned from scriptMaker
. myScript
is not just a script object; it's a closure -- a script object along with a surrounding global context that defines the values of that script object's free variables.
Here's an example where the value of the free variable comes from a property of a surrounding script:
on makeGreeting(s) script outerScript property greeting : s script greet display dialog greeting end script end script return outerScript's greet end makeGreeting set greet to makeGreeting("Howdy") run greet -- Howdy
In that example, makeGreeting
doesn't return outerScript
; it returns just the inner script object greet
. That script object uses a free variable greeting
whose value is remembered from its original context as the value of outerScript
's property greeting
. In the last line, the script object greet
runs even though there is no name greeting
in scope at that point.
Another use for a script object as a result of a handler is as a constructor. Here we take advantage of the fact that when a handler is called, it initializes any script objects defined within it. So a handler is a way to produce a copy of a script object whose properties are at their initial value.
As an example, consider a script object whose job is to count something. It contains a property, which maintains the count, and a handler, which increments the count. (This is using a sledgehammer to kill a fly, but it's a great example, so bear with me.) A handler is used as a constructor to produce an instance of this script object with its property set to zero. Each time we need to count something new, we call the handler to get a new script object, like so:
on newCounter() script aCounter property c : 0 on increment() set c to c + 1 end increment end script return aCounter end newCounter -- and here's how to use it set counter1 to newCounter() counter1's increment() counter1's increment() counter1's increment() set counter2 to newCounter() counter2's increment() counter1's increment() display dialog counter1's c -- 4 display dialog counter2's c -- 1
AppleScript is a small language, and is usually thought of as directed at novice programmers, but it is capable of some remarkably sophisticated constructs. This article has presented examples of powerful and elegant things one can do with handlers in AppleScript. I hope you've found them as surprising as I did when I was writing my book!