Skip to Content
Skip to Table of Contents

← Previous Article Next Article →

ATPM 9.01
January 2003

Columns

Segments

How To

Extras

Reviews

Download ATPM 9.01

Choose a format:

Roll Your Own

by Charles Ross, cross@atpm.com

How to Handle Anything

Hello again and welcome to another installment in our series on how to program your Macintosh using the AppleScript language. Last time we covered the try block and the on error handler to demonstrate how to gracefully handle errors that may occur while a user runs your software. The on error handler is a special kind of construct in AppleScript that is one of a more general set of tools called the handler. This month we’re going to take a close look at what handlers are and how you can use them to help you write programs more efficiently.

If you have any experience with other programming languages such as C or Pascal, you’ll be familiar with the procedure and the function. AppleScript handlers serve the same purpose as these constructs. A handler is a named grouping of programming code that can be called from elsewhere in the program, can be passed information, and can optionally return a value. When it returns a value it works like a Pascal function. When it doesn’t return a value, the handler is operating like a Pascal procedure.

In any given program, there are probably going to be times when the same code needs to be run from different parts of the program. Programmers will take this repeating code and break it out into a separate handler that can be called at any time. This has a number of advantages. If you need to change the way that part of the program works, you only need to do so once rather than every time the code needs to be executed. Also, working with small and easily defined handlers makes debugging much easier, since you can test everything that the handler needs to do separately from the entire program. Once you have the handler working exactly how it should, you can place it in your program and forget about it, dedicating your efforts to other parts of your software.

Let’s take a look at a simple example that will present the user with a dialog box that displays the current time.

on DisplayCurrentTime()
    display dialog "The current time is " & ¬
        (time string of (current date)) buttons {"OK"} default button 1
end DisplayCurrentTime

If you enter the above handler into Script Editor and run it, nothing will happen. This is because a handler doesn’t do anything until it is called elsewhere in the program.

There is an exception to this. Any code in an AppleScript program that doesn’t explicitly appear in a handler is actually part of the on run handler. In other words, you’ve been using handlers all along and didn’t know it! When you tell Script Editor to run a program, it actually sends a run call to the on run handler, which includes all the code not contained in another handler. Take a look at the following very simple program.

display dialog "Hello!"

This program is actually equivalent to, and interpreted by AppleScript as

on run
    display dialog "Hello!"
end run

However, you can’t have statements outside of a handler (like the first example above) and also have statements within a run handler. This is because handlers in a program must have a unique name, and when you have statements outside of a handler (which are actually in an implicit run handler) and have statements in an explicit run handler, you are trying to create two handlers with the same name. Try to compile the following script to see what I mean.

display dialog "Implicit run handler"

on run
    display dialog "Explicit run handler"
end run

Given this new piece of information (and for reasons that will become clear as we cover more of the special handlers provided by AppleScript), we won’t use the implicit on run handler any longer, but use the explicit version. So, let’s edit our handler demonstration program so that it actually does something.

on run
    DisplayCurrentTime()
    display dialog "I just displayed the time." ¬
        buttons {"OK"} default button 1
end run

on DisplayCurrentTime()
    display dialog "The current time is " & ¬
        (time string of (current date)) buttons {"OK"} default button 1
end DisplayCurrentTime

Let’s take a closer look at what this program is doing. When we tell AppleScript to run the above program, it sends a run message to the program. The program sees that it has an on run handler, so it begins executing the code within that handler. The first line in the on run handler is DisplayCurrentTime(), which is a call to another handler, so execution of the program jumps out of the on run handler and begins with the first line of on DisplayCurrentTime(). That handler simply displays a dialog box that lets the user know what the current time is. Since there aren’t any more instructions to perform in on DisplayCurrentTime(), execution returns to the calling portion of the program and executes the next line, which lets the user know that the program had just displayed the time.

Our sample program operates exactly like the following program.

display dialog "The current time is " & ¬
    (time string of (current date)) buttons {"OK"} default button 1
display dialog "I just displayed the time." ¬
    buttons {"OK"} default button 1

A few things to note about the above example. First, note how the on DisplayCurrentTime() handler is named. There are no spaces or special characters, and it describes what it does. The rules for naming variables that we covered in a previous column apply to naming handlers also. And, just like you would want to name a variable something that describes what it holds, you want to name a handler something that either describes what it does or what it returns.

Second, note the order of execution of lines in the program. It isn’t linear. The program jumps around.

Third, we could have written the program like this, reversing the order of the handlers.

on DisplayCurrentTime()
    display dialog "The current time is " & ¬
        (time string of (current date)) buttons {"OK"} default button 1
end DisplayCurrentTime

on run
    DisplayCurrentTime()
    display dialog "I just displayed the time." buttons {"OK"} default button 1
end run

In fact, some programming languages would require that the handlers be in this order because they won’t let you call a handler before the handler appears. AppleScript doesn’t care about the order of handlers, however. When a handler call is made, AppleScript will look through the entire code to find where it should go to execute the handler.

Each of the examples I’ve given begins the handler definition with the word on. Handlers can also begin with the word to. AppleScript doesn’t care which one you use, so you can use the one that makes the most sense for an English interpretation of your program.

Note that each handler is separate. In general, you can’t have one handler appear inside of another handler. As with most rules in programming, there are exceptions. The first exception we’ve already covered: The on error handler can appear in other handlers. We won’t cover the other exception until we get to script objects in a future column.

Lastly, you may be wondering why the on DisplayCurrentTime() handler ends with a pair of parenthesis while the on run handler doesn’t. The answer is that the on run handler is defined by AppleScript and doesn’t take any parameters. Parameters are pieces of data that can be passed to a handler by the calling code. When we define our own handlers, such as on DisplayCurrentTime(), AppleScript doesn’t know in advance whether it takes any parameters or not, so we have to let it know that it doesn’t by adding a blank pair of parentheses to the handler. When we call the handler, the parentheses tell AppleScript that we are calling a handler that doesn’t take any parameters.

If a handler does receive parameters, the parameters are passed to the handler from the calling code by placing the data between the parentheses. Here’s an updated version of the DisplayCurrentTime() handler that takes a single parameter, a name.

on DisplayCurrentTime( theName )
    display dialog theName & ", the current time is " & ¬
        (time string of (current date)) buttons {"OK"} default button 1
end DisplayCurrentTime

on run
    DisplayCurrentTime( "Chuck" )
end run

Notice that within the DisplayCurrentTime() handler, the theName parameter acts just like a variable, and we can reference it just like any other variable, as well as change its value.

When passing data to a handler via parameters, you can use a constant (as we did with the constant string "Chuck") or a variable of the type that the function is going to expect. The following script performs exactly the same way as the one above.

on DisplayCurrentTime( theName )
    display dialog theName & ", the current time is " & ¬
        (time string of (current date)) buttons {"OK"} default button 1
end DisplayCurrentTime

on run
    set theName to "Chuck"
    DisplayCurrentTime( theName )
end run

Handlers can have as many parameters as you like. When a handler accepts multiple parameters, each one is separated by a comma. Parameter names must conform to the same rules as variable names. Take a look at the next example.

on DisplayCurrentTime( firstName, lastName )
    display dialog firstName & " " & lastName & ", the current time is " & ¬
        (time string of (current date)) buttons {"OK"} default button 1
end DisplayCurrentTime

on run
    DisplayCurrentTime( "Chuck", "Ross" )
end run

Some of you out there who have previous experience with AppleScript may be thinking right about now that this isn’t the only way to send parameters to a handler. The method I’ve been describing here is called positional parameters. There’s another method of passing parameters to handlers called labeled parameters. The rules for labeled parameters are much more complex, but will usually result in code that is more like English. For instance, using named parameters, the call to DisplayCurrentTime( "Chuck" ) could be written DisplayCurrentTime for "Chuck".

Since we’re working with beginning concepts here, I’m going to bypass labeled parameters for the time being. However, if you’re interested in finding out how to use them, check out the chapter on handlers from the AppleScript Language Guide, or chapter 8 from O’Reilly’s AppleScript in a Nutshell.

Handlers also have the ability to return data back to the calling section of the program. Handlers that don’t return any data are often called procedures or subroutines. Handlers that do return data are often called functions. Below is a program that includes a handler to calculate the area of a triangle. When the handler is called, it gets passed the height and width of the triangle, performs the calculation needed, and returns the result. Since the handler returns a result, it can be used anywhere the type of result returned is expected. So if the handler returns a number, anywhere in your code where a number is expected you could place the handler instead, just like a variable.

on TriangleArea(height, width)
    return 0.5 * height * width
end TriangleArea

on run
    set height to text returned of (display dialog ¬
        "Enter the triangle height:" default answer ¬
        "" buttons {"OK"} default button 1)
    set width to text returned of (display dialog ¬
        "Enter the triangle width:" default answer ¬
        "" buttons {"OK"} default button 1)
    display dialog "The area of a triangle with height " & ¬
        height & " and width " & width ¬
        & " is " & TriangleArea(height, width) ¬
        buttons {"OK"} default button 1
end run

As you can see, the handler passes the results back to the calling code by use of the return command. The return command can also be used to simply exit the handler without returning a value by placing it on a line by itself with no value to return. As soon as a return command is executed, control of the program is returned to the calling portion, even if there are more statements after the return command.

One last point about handlers before we integrate the concept into our Sum Numbers program. Handlers can have variables within themselves that are independent of variables in any other handler, even if the variables have the same name. This includes the parameters that are sent to handlers. In our TriangleArea() handler, the parameters within it are named height and width. These are the most obvious names for the parameters. We also use these names in the run handler, but changing the values within the handler would have no effect on the values outside the handler. Here’s an example to demonstrate this concept.

on ChangeNumber()
    set theNumber to 12
    display dialog "Inside ChangeNumber, theNumber is equal to " & theNumber ¬
        buttons {"OK"} default button 1
end ChangeNumber

on run
    set theNumber to 6
    ChangeNumber()
    display dialog "Inside run, theNumber is equal to " & theNumber ¬
        buttons {"OK"} default button 1
end run

If you run this program, you’ll get two dialog boxes. The first tells you that theNumber is equal to 12. The second dialog box says that theNumber is equal to 6. This is possible because the variable theNumber within the ChangeNumber() handler has absolutely nothing to do with the variable theNumber anywhere else in the program.

This is a good thing. You’re able to work with a handler on its own terms without having to worry about what it will affect in the outside world.

As with most things in AppleScript, this rule has an exception. You can create global variables that are accessible from anywhere in a script.

global theNumber
on ChangeNumber()
    set theNumber to 12
    display dialog "Inside ChangeNumber, theNumber is equal to " & theNumber ¬
        buttons {"OK"} default button 1
end ChangeNumber

on run
    set theNumber to 6
    ChangeNumber()
    display dialog "Inside run, theNumber is equal to " & theNumber ¬
        buttons {"OK"} default button 1
end run

In this case the global variable theNumber isn’t part of an implicitly run handler because global theNumber isn’t a command but a statement that declares a global variable called theNumber. With this program you’ll get two dialog boxes, both of which report that theNumber is equal to 12. In this case, we’ve declared that theNumber is a global variable, which means that it is the same in every handler there is. So when the ChangeNumber() handler sets theNumber to a new value, the new value is stored in the same theNumber variable found in the run handler.

If you have a global variable and you want a handler to use a local version instead, use the local keyword to declare the variable as local to the handler.

global theNumber
on ChangeNumber()
    local theNumber
    set theNumber to 12
    display dialog "Inside ChangeNumber, theNumber is equal to " & 
        theNumber buttons {"OK"} default button 1
end ChangeNumber

on run
    set theNumber to 6
    ChangeNumber()
    display dialog "Inside run, theNumber is equal to " & theNumber ¬
        buttons {"OK"} default button 1
end run

This version of the program operates the same way as the first version, with the assignment of a value to theNumber within the ChangeNumber() handler having no effect on the variable theNumber within the run handler.

In general, programmers avoid global variables. You might think that globals will make programming easier for you. After all, using global variables would allow you to do away with parameters as a way to get information to a handler. While it may seem easier to you to have a variable be accessible in all parts of a program, the truth is that it becomes quite a mess when you decide that a handler would be useful in another program. Not only do you need to get the handler over to the other program, but you need to keep track of any global variables that the handler makes reference to. Using global variables will make your code much harder to reuse and maintain.

Well, now that we’ve covered how to use handlers, let’s take a look at integrating them with our existing Sum Numbers program.

on run
    -- Get the number to sum up to from the user
    -- and store it in the variable theNumber
    set theNumber to (display dialog ¬
        "Please enter a positive number:" default answer ¬
        "" buttons {"OK"} default button 1)
    -- Assume that invalid data was entered
    set isValidEntry to false
    -- Repeat the following block of statements until we've made
    -- sure that the user has entered valid data.
    repeat until isValidEntry
        -- If there is a problem with the data entered by the user…
        if NumberHasProblem(theNumber) then
            -- Set the variable dialogMessage to show appropriate
            -- feedback to the user.
            set dialogMessage to DetermineDialogMessage(theNumber)
            -- Prompt the user to enter valid data, storing ¬
            -- the results in theNumber
            set theNumber to text returned of ¬
                (display dialog dialogMessage & ¬
                    "  Please enter a positive integer:" default answer ¬
                    "" buttons {"OK"} default button 1)
        else            -- Everything is fine.  ¬
            -- Set isValidEntry to true so we can exit the loop.
            set isValidEntry to true
        end if --(not IsNumber(theNumber)) or (not IsInteger(theNumber))
    end repeat -- until isValidEntry
    -- Report the sum to the user.
    display dialog "The sum of the first " & theNumber & ¬
        " numbers is " & SumNumbers(theNumber) & ¬
        "." buttons {"OK"} default button 1
end run
-- Returns true if the parameter passed either 
-- isn't a number or isn't an integer.
on NumberHasProblem(theNumber)
    return IsNumber(theNumber) and IsInteger(theNumber)
end NumberHasProblem

-- Accepts a piece of data and returns true if the data is a number.
on IsNumber(theVariable)
    -- Enclose an attempt to coerse the data to a number within a try block..
    try
        -- Try to coerse the data to a number.
        set theVariable to theVariable as number
    on error
        -- If there was a problem with coercing the data, return false.
        return false
    end try
    -- If no problem occured, return true.
    return true
end IsNumber
-- Accepts a piece of data and returns true if after ¬
-- coersing the data to a number it is an integer.
on IsInteger(theNumber)
    return (class of (theNumber as number) is integer)
end IsInteger
-- Accepts a piece of data and returns an appropriate dialog message
-- that depends on the problem the data has.on DetermineDialogMessage(theVariable)
    -- If the problem is that the data isn't a number…
    if not IsNumber(theVariable) then
        -- Set the dialogMessage variable to report the appropriate problem..
        return "You have entered text intstead of a number."
        -- If the problem is that the number isn't an integer…
    else if not IsInteger(theVariable) then
        -- Set the dialogMessage variable to report the appropriate problem..
        return "You have entered a number with a fractional part."
    end if -- not IsNumber(theVariable)
end DetermineDialogMessage
-- Accepts a number as a parameter and returns the ¬
-- sum of the positive integers up to that number.on SumNumbers(theNumber)
    -- Initialize sum to 0.
    set sum to 0
    -- Sum up the numbers.
    repeat with i from 1 to theNumber
        set sum to sum + i
    end repeat -- with i from 1 to theNumber
    -- Return the results of the process to the user.
    return sum
end SumNumbers

Quite a lot has changed with this version of the program, but the functionality of it is the same as it was when we left it last month. All that has changed is that we’re now using handlers to get some of the work done. Let’s go through each of the 6 handlers.

The first one, the run handler, is not actually new, but we’re declaring is explicitly this time. We’ve changed the structure of this handler somewhat since we can now use handlers to do much of the real work. For instance, the try block is nowhere to be seen in the run handler since we have a separate handler that checks if the data entered by the user is valid.

The handler which does that checking is the NumberHasProblem handler. All this handler does is return true if the data entered by the user is both a number and an integer and false if either of those requirements isn’t met.

NumberHasProblem actually doesn’t do much except call two other handlers. The first is IsNumber, which gets passed a variable and returns true if the variable is a number and false if it doesn’t. This handler is where you’ll now find our try block. Notice that the IsNumber handler has two return statements, but only one of them will ever get executed. If an error occurs when we attempt to coerce the variable, we immediately enter the on error handler and return false. Execution within the handler at that point stops and control of the program is returned to the calling portion. If no problem occurred, then IsNumber simply returns true.

NumberHasProblem also calls the IsInteger handler, which returns true if the parameter passed to it is an integer and false otherwise.

I’ve also broken the portion of the program that determines the custom dialog message into its own handler. The DetermineDialogMessage handler accepts a variable and returns an appropriate text string depending on what the trouble with the variable is.

Lastly, there’s the SumNumbers handler, which after we’ve made sure that we’ve got an integer to work with, sums up all the numbers up to that integer and returns the result.

Some of the handlers I’ve created in this new version were broken into their own handler because they might be useful in other circumstances. For instance, I might write another program some day for which the IsNumber, IsInteger, and SumNumbers handlers might be needed. If that’s the case, I’ve already tested these, and can simply copy and paste them into the new program without worrying about the code.

The other two handlers were created for a different reason. The run handler was beginning to get very long and complicated, making it difficult to read and be able to see at a glance what was going on. When a single handler begins to grow lengthy (for me, that usually means more than about 20 lines), I’ll often break some of it’s code into a separate handler, replacing the code with the handler name, which I make as descriptive as possible. When reading the run handler for the first time, the reader may not really care how DetermineDialogMessage gets its job done, but the reader can see in general what it’s supposed to do. This makes the run handler much more manageable.

The NumberHasProblem handler serves much the same purpose. The line of the program that reads:

if NumberHasProblem(theNumber) then

would have worked just as well as:

if (not IsNumber(theNumber)) or (not IsInteger(theNumber)) then

But by breaking this out into a separate handler, the code within the run handler reads more cleanly.

• • •

That’s it for this month. Before I let you go, let me suggest some improvements you might try making on this program. One other problem that data entered by the user might have is that it could be zero or negative. Add another handler called IsPositive, and call that handler from NumberHasProblem and DetermineDialogMessage to appropriately check for that trouble and appropriately inform the user of that possible problem.

If this column were being written about 20 years ago (and if you’ve been with us from the beginning), you would know just about everything about programming to really dive in and begin creating your own programs. You now have the tools to create what are called procedural programs with very simple user interfaces. In recent decades, however, there have been some major advances in programming. Two of those advances are in user interfaces and object oriented programming. In the next column we’re going to begin getting into what object oriented programming is and how is can be implemented with AppleScript. Until then, keep the e-mail coming with questions, comments, and gushing praise.

Also in This Series

Reader Comments (2)

anonymous · February 5, 2003 - 22:45 EST #1
Good column. Keep it going. Very helpful. Thanks.
anonymous · February 18, 2003 - 22:05 EST #2
There is an "execution error" cropping up when I try to run the main script in this "How To Handle Anything" article of January 2003. I wonder if you've noticed that! I don't understand what the problem is. It doesn't seem to be due to possible errors in how I may have entered the script from the lesson because just copying the script in as you've written it (with some editing) reproduces the problem as well.

In any case, thanks very much for the instruction!

Regards,

Duane

Add A Comment





 E-mail me new comments on this article