Roll Your Own
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
- Getting the List of It—Part 2 · July 2003
- Getting the List of It · June 2003
- The Object of Programming—Part 2 · March 2003
- The Object of Programming · February 2003
- How to Handle Anything · January 2003
- Try to Handle This · December 2002
- Charting Your Success · September 2002
- Go with the Flow · August 2002
- Variables and Data · June 2002
- The Ultimate Customization · May 2002
- Complete Archive
Reader Comments (2)
In any case, thanks very much for the instruction!
Regards,
Duane
Add A Comment