Previous Page
Next Page

9.10. Power Handler Tricks

A handler takes values as parameters and returns a value. A handler is a value. A script object is a value and can contain a handler. If these facts suggest to your mind an intimation of amazing possibilities, read on.

9.10.1. Handler and Script Object as Parameter

You can pass a handler as a parameter to handler. The difficulty is in calling it. This code fails with a runtime error:

on sayHowdy( )
    display dialog "Howdy"
end sayHowdy
on doThis(what)
    what( )
end doThis
doThis(sayHowdy) -- error: «script» doesn't understand the what message

The trouble is that 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 (see "Handler Calls, Commands, and Script Objects" in Chapter 8) that an unqualified handler call is a message directed to the current script object, which in this case is the script as a whole.

One possible workaround is to use a global. This approach works because by copying the handler to a global we're putting it where a message directed to the script as a whole can find it (see "Scope of Globals" in Chapter 10):

on sayHowdy( )
    display dialog "Howdy"
end sayHowdy
on doThis(what)
    global what2
    set what2 to what
    what2( )
end doThis
doThis(sayHowdy) -- Howdy

This solution is clever, but now we've broken encapsulation. Global variables pose risks (other code might access this same global, or we might be tromping accidentally on some other code's global), and besides, if we're going to use a global there's little point to passing a parameter in the first place.

Another possible workaround is to pass a script object instead of a handler:

script sayHowdy
    display dialog "Howdy"
end script
on doThis(what)
    run what
end doThis
doThis(sayHowdy) -- Howdy

This is very efficient because script objects are passed by reference. But we ended up having to use the run command instead of a handler call. That's not going to be very pretty if our handler takes any parameters, because it's hard to pass parameters to a run handler (as shown earlier in this chapter). But waita script object can contain a handler! So we can use it as a kind of envelope. We can define a handler in a script object and pass the script object. In fact, this device 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

However, there's another way entirely. This happens to be my favorite solution. We don't pass a script object; we pass a handler, just as in our first attempt. But inside the handler, we have a script object waiting to receive it:

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

The fact that this code works is astonishing (at least, when I stumbled upon it I was astonished). It depends upon an obscure but powerful rule of scope (see "Scope of Locals" in Chapter 10) which says that a script object within a handler can see that handler's local variables. An incoming parameter is such a variable. Thanks to this rule, our property declaration for theHandler can see the incoming what parameter and store its value. So now the property theHandler is a handler! But it is also a top-level entity of the script object, which means that we can call it from within this same script object. Tricky, eh?

For a useful application of this technique, let's return to the example earlier in this chapter where a handler called filter filtered a list to get only those members of the list that were numbers. That handler is not general; we'd like a way to filter a list on any criterion we care to provide. So we want to pass it both a list and a handler (a handler that takes a single argument and returns a boolean saying whether it fits the criterion). We can do so by writing filter( ) as a handler containing a script object:

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.

9.10.2. Handler and Script Object as Result

A handler may be returned as the result of a handler. Because you can't define a handler directly within a handler, you might have to define it as a handler within a script object within the handler; but this is no bother. So, for example:

on makeHandler( )
    script x
        on greet( )
            display dialog "Howdy"
        end greet
    end script
    return x's greet
end makeHandler
set y to makeHandler( )
y( ) -- Howdy

Of itself, however, this device is not terribly useful; in real life, you're more likely to return a script object rather a handler:

on scriptMaker( )
    script myScript
        property x : "Howdy"
        display dialog x
    end script
    return myScript
end scriptMaker
set s to scriptMaker( )
run s -- 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

Why might it be useful to return a script object as the result of a handler? Mostly because the handler can customize the script object before returning it. For example, instead of hard-coding the property x that will be displayed, we can pass that value into the handler as a parameter:

on scriptMaker(what)
    script myScript
        property x : what
        display dialog x
    end script
    return myScript
end scriptMaker
set s to scriptMaker("Hello")
run s -- Hello

The real virtue of this technique emerges when we retain and reuse the resulting script object. To illustrate, here's one more version of the general list-filtering routine we wrote earlier in this chapter. Previously, we passed a handler both a criterion handler and a list, and got back a filtered list. Now we're going to pass just a criterion handler, and get back a script object, which we will retain. Now we've got a script object containing a filter handler that is already customized to filter any list according the criterion we passed in at the outset. In effect, we've built a custom handler! Now we can reuse that handler repeatedly with different lists. We can even create more than one such handler, each customized to be a certain kind of filter. This architecture is elegant and efficient (and LISPy):

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
on isNumber(x)
    return ({class of x} is in {real, integer, number})
end isNumber
on isText(x)
    return ({class of x} is in {string, text, Unicode text})
end isText
set numbersOnly to makeFilterer(isNumber)
set textOnly to makeFilterer(isText)
tell numbersOnly
    filter ({"hey", 1, "ho", 2, "ha", 3}) -- {1, 2, 3}
    filter ({"Mannie", 7, "Moe", 8, "Jack", 9}) -- {7, 8, 9}
end tell
tell textOnly
    filter ({"hey", 1, "ho", 2, "ha", 3}) -- {"hey", "ho", "ha"}
    filter ({"Mannie", 7, "Moe", 8, "Jack", 9}) -- {"Mannie", "Moe", "Jack"}
end tell

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 c, which maintains the count, and a handler that increments the count. (This approach 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 c set to zero. Each time we need to count something new, we call the handler to get a new script object. Then we can repeatedly count that thing by calling the increment handler of the corresponding script object.

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


Previous Page
Next Page