Imagine you own a calculator with buttons that each perform a single complete operation—one button adds 5 plus 7, and another button adds 5 plus 8, and so on. It might be convenient for those limited operations, but the calculator could rapidly become unwieldy. (Similarly, Chinese pictographs are inconvenient for computer usage compared to an alphabet from which you can construct any word.) In reality, each button on a calculator actually represents either an operand, such as the number 5, or an operation, such as addition, allowing for many possible combinations with relatively few keys.
At the beginning of this chapter we used the alert
command to display the string “Hello World” in a dialog box. The alert
command accepts any text string as an argument. An argument is analogous to the operands used in the example of a calculator above, and the alert
command is analogous to the addition button that performs some operation using the argument(s). The words parameters and arguments are often used interchangeably to indicate inputs that are passed to a function on which it can operate. Strictly speaking, an argument is the item specified as part of the function call, and a parameter is the same item once it is received inside the function.
Just as the alert
command can accept any string for display, we should strive to make our custom handlers flexible by using variables to represent values that can change. In contrast, using a fixed number, such as 5, or a fixed string, such as “Bruce,” in your program is called hardcoding a value.
Tip
Instead of hardcoding specific values into a handler, generalize the function so that it can operate on whatever arguments are passed into it.
For example, a handler that finds a file should be flexible enough to find any file we ask it to find, rather than always looking for a particular hardcoded filename. Beginning programmers often create two or more copies of the same handler with only minor variations to accomplish nearly identical tasks. Instead, you should create a generalized (that is, flexible) version of the handler that accepts arguments (or parameters) to accommodate the differences. This is shown in detail in Example 1-31 and Example 1-32. Let’s start with a discussion of how arguments are passed to a function.
A simple function performs an operation on the argument(s) passed into it. For example, let’s define an avg()
function that averages two numbers.
The names avg
, a
, and b
are chosen arbitrarily. Note that there is a space between the handler name (avg
) and the first parameter (a
) but that subsequent parameters (such as b
) are separated by a comma from the previous parameter. The return
statement sends the answer back to whoever called the function. Without the return
statement, the answer
avg
calculates would never be known! (Forgetting to return a result is a very common error. If the result from a custom function returns VOID
, you probably forgot the return
statement).
Type the handler in Example 1-29 into a movie script, and test it from the Message window. The put
command prints the result returned by avg()
.
put avg (5,8) -- 6.5000
The integers 5 and 8 are arguments that are operated upon by avg()
. The arguments are separated by commas. The parentheses are required to obtain the value returned by avg()
, but parentheses are optional when calling a command that does not return a value, such as alert
.
The first argument (5) is automatically assigned to the first parameter (a
), and the second argument (8) is assigned to the second parameter (b
). In this case, the order of the parameters does not affect the result, but in most cases the order of the parameters is crucial. For example, division is not reflexive: 5 divided by 8 would not be the same as 8 divided by 5.
Tip
If the number of arguments does not match the number of parameters, Director won’t complain. It is up to you to ensure that the correct number of arguments is specified.
Modify the avg()
handler to create newAvg
as follows:
on newAvg a, b put "The first parameter, a, equals" && a put "The second parameter, b, equals" && b set answer = (a+b)/ 2.0 put "The answer is" && answer end
We’ve added a local variable, arbitrarily named answer
, which is convenient for holding the value that is printed and then returned to the calling program. Whereas a
and b
are implicitly assigned to the arguments in the function call, answer
is explicitly assigned a value using set...=
.
There is no difference (as far as the newAvg
handler can tell) if we pass integer variables as arguments to newAvg
instead of the integer literals 5 and 8. Type each of these lines in the Message window, pressing RETURN
after each:
set x = 5 set y = 8 put x newAvg(x, y) -- "The first parameter, a, equals 5" -- "The second parameter, b, equals 8" -- "The answer is 6.500"
We don’t need to use the put
command because newAvg
displays the result itself using put
, rather than returning an answer.
Tip
The parameters within newAvg
, namely a
and b
, are still equated to 5 and 8, respectively. The values of the arguments x
and y
, not x
and y
themselves, are passed to newAvg
. This is called passing arguments by value, rather than by reference.
Refer to "Parameter Passing,” in Chapter 4 for more details on parameters passed by reference and to Chapter 6 for how this affects Lingo lists.
NewAvg
is a generalized handler that can average any two numbers passed into it (we’ll see later how to make it accept any number of arguments to average).
To perform an operation on a sprite you must refer to the sprite by its channel number (or an expression that evaluates to a channel number). For a sprite in channel 1, you might use:
on mouseUp set the foreColor of sprite 1 = random (255) end
A beginner might create another script to attach to a different sprite in channel 2 as follows:
on mouseUp set the foreColor of sprite 2 = random (255) end
Not only is this wasteful, but these scripts will fail miserably if you move the sprites to a new channel. Thankfully, Lingo provides several system properties that can be used to generalize a handler. When a script is attached to a sprite, the currentSpriteNum
property always indicates the sprite’s channel number. Therefore, we can replace the two separate scripts with a single sprite script that can be attached to any sprite in any channel and that will always work.
Example 1-30. A Simple Generalized Behavior
on mouseUp set the foreColor of sprite (the currentSpriteNum) = random (255) end
Refer to Chapter 9 for details on the currentSpriteNum
, the spriteNum of me
, and the clickOn
properties and how they can be used to generalize handlers for use with any sprite.
The following is a more sophisticated example of a generalized function, but the principle is the same (feel free to skip this section if it is confusing). Let’s suppose you want to check if the file "FOO.TXT
" exists.
You might write the code shown in Example 1-31 (see Chapter 14 for an explanation of this Lingo and a more robust example).
Example 1-31. A HardCoded Function
on doesFooExist -- Use the FileIO Xtra to try to open the file FOO.TXT set fileObj = new (xtra "FileIO") if objectP(fileObj) then openFile (fileObj, "FOO.TXT", 1) set result = status (fileObj) -- A result of 0 indicates success if result = 0 then alert "FOO.TXT exists!" else -- Print the error message in the Message window put error (fileObj, result) alert "FOO.TXT can't be found" end if -- Clean up after ourselves closeFile (fileObj) set fileObj = 0 end if end doesFooExist
Now suppose you want to check if a different file exists. Most beginners would duplicate this long block of code, then change the filename in the openFile()
function call from “FOO.TXT” to their new filename.
Tip
Never duplicate near-identical long blocks of code. Your code becomes harder to debug—and much harder to change if you do find a bug. Always generalize the code into a utility function that you can add to your “tool belt” for future use.
Below we’ve created a generalized function. Note that it accepts a file name as a parameter. The name that you specify gets substituted automatically for the fileName
parameter and is used in the openFile
command. Note also that it returns either TRUE
(1) or FALSE
(0) to indicate whether the file was found. If it couldn’t be found, you may want to return the error code that was obtained from the FileIO Xtra’s status()
call. (It is good practice to simply return some result or status and let the caller decide whether to post an alert message or do something else.) Beyond that, it is essentially the same handler as shown in Example 1-31. Try to make your utility code as non-intrusive as possible, and clean up after yourself. Note that we opened the file in read-only mode to avoid failing if the file was already open. We also closed the file when done to clean up after ourselves. Example 1-32 is primarily for illustration. The FileIO Xtra will search in the current folder if the searchCurrentFolder
is TRUE
(the default). It will also search the list of paths, if any, in the searchpaths
. It may not work correctly with long filenames under Windows. Thus Example 1-32 is not completely robust.
Example 1-32. A Generalized FileExists Function
on fileExists fileName -- Use the FileIO Xtra to open the specified file set fileObj = new (xtra "FileIO") if objectP(fileObj) then -- Open file with mode = 1 "read-only" openFile (fileObj, fileName, 1) set result = status (fileObj) -- A status of 0 indicates success if result = 0 then set found = TRUE else -- Display the error for debugging put error (fileObj, result) set found = FALSE end if closeFile (fileObj) set fileObj = 0 -- Return TRUE or FALSE to indicate if the file exists return found end if end fileExists
Now we can easily determine if any file exists, such as:
put fileExists ("FOO.TXT") -- 1 put fileExists ("FOOPLE.TXT") -- 0
Or use it as follows:
if fileExists ("FOO.TXT") then
-- Do whatever I want to with the file...
-- such as open and read it
else
alert "The file can't be found"
end if
Let’s revisit the simple newAvg
handler. Add the following handler to your movie script that already contains the newAvg
handler shown earlier.
Example 1-33. Using Generalized Functions
on testAvg newAvg (5, 3) newAvg (8, 2) newAvg (4, 4) newAvg (7, 1) end
Choose Control
➤Recompile Script
, and then test testAvg
from the Message window.
testAvg
You should see the output of the newAvg
function repeated four times.
Now, type this in the Message window:
newAvg (5)
What happens and why? What is the value of the second parameter within the newAvg
handler? It defaults to VOID
, as do all unspecified parameters, because only one argument was specified in the function call. What happens if we forget to specify the fileName
when calling our fileExists()
function created earlier?
Let’s create a simple divide
function (in reality you’d just use “/” to divide):
on divide a, b return float(a)/ b end put divide (5,5) -- 1.0000
What happens if we forget to specify the parameter used as the divisor?
put divide (7) -- This causes an error
We saw earlier that an error occurs when a local variable is used before it is assigned a value. Enter the code shown in Example 1-34 in a movie script, and recompile the script.
Example 1-34. Special Treatment of First Argument to a Function Call
on testFirstArg dummyHandler(x) end testFirstArg
Isn’t x
an unassigned local variable? Shouldn’t it generate a "Variable used before assigned a value" error message? Before answering that, let’s alter testFirstArg
and recompile the script:
on testFirstArg dummyHandler(x, y) end testFirstArg
The variable y
is also an unassigned local variable. Why does it generate an error message, if x
did not? The answer lies in the unique way that Lingo treats the first argument to any function call.
Warning
Even though it is an error, Lingo does not complain if the first argument to a function call is an undeclared local variable (in this case x
).
Multiple scripts may contain handlers of the same name. When you call a function, Lingo must decide which script to look in first. If the first argument to the function call is a special entity called a script instance (see Chapter 2), Director runs the handler in that particular script rather than performing its usual search to find the right script automatically. Lingo allows anything as the first argument to a function call because it does not verify script instances or handler names during compilation. (At runtime it will most likely cause an error, though).
Tip
If the first argument passed to a custom function call is a script instance, Lingo searches only that script instance for the specified handler. Therefore, you can not pass a script instance as the first parameter to a handler in a movie script because Lingo won’t look in the movie script!
Suppose you want to call a movie script’s handler from a Behavior, and suppose you want to pass in the Behavior’s script instance as an argument. Assume that this is the Behavior script.
This is the movie script:
on displayInfosomeScriptIntanceOrObject
put the spriteNum ofsomeScriptIntanceOrObject
end
What will happen? Director will issue a "Handler not defined" error because it will look for the displayInfo
handler only in the Behavior script (but it won’t find it). You can move the displayInfo
handler into the Behavior script, in which case it will be available only to instances of that Behavior, or you can rewrite the example as shown in the code that follows. Note that we add a dummy VOID
argument as a placeholder for the first argument to the function call, which allows our script instance to become the second argument. Because the second argument is not afforded any special treatment, Lingo searches the usual hierarchy and finds the displayInfo
handler in the movie script!
Rewrite the displayInfo
function call as:
on mouseUpme
displayInfo (VOID,me
) end
In the displayInfo
handler declaration in the movie script we must add a dummy parameter to “catch” the first dummy argument:
on displayInfodummyParam
,someScriptIntanceOrObject
-- Use a dummy argument as the first parameter -- to allow an object to be passed as second argument. put the spriteNum ofsomeScriptIntanceOrObject
end
A script instance or child object (which can be thought of as the same thing) is often intentionally passed as the first argument to force Lingo to look in the correct script for the correct handler or property variables. See the example of property variables in the earlier "Variable Types" section and Chapter 12 and Chapter 13.
Lingo’s built-in commands typically complain if you pass the wrong number or unexpected type of arguments, but some accept arguments of different data types and/or a variable number of arguments (”variable" is used here to mean "varying,” not a Lingo variable). For example, the second argument to setaProp()
is either a property name or a property value, depending on whether the first argument is a property list or linear list. Likewise, the puppetSound
command accepts either one or two arguments, and some arguments to the puppetTransition
command are optional.
You can also design your custom handlers to allow a variable number of parameters or arguments of varying data types. This makes it easier to create generalized functions rather than multiple, highly similar versions. If you are creating a library of functions for yourself or others, it also makes those functions more flexible and easier to use by the calling routine.
Your function will typically require one or more mandatory arguments that should be placed at the beginning of the parameter list. Optional arguments should be placed after the required parameters. If the caller specifies fewer arguments than the number of parameters you are expecting, later parameters will be VOID
.
The playSound
example that follows accepts the name or number of a sound to play and an optional sound channel number. If no channel is specified, it plays the sound in channel 1. Note the use of voidP()
to check whether the caller has specified the requested parameters.
Example 1-36. Function Accepting Varying Arguments
on playSound soundID, chan if voidP(soundID) then alert "A sound must be specified" exit end if -- Use channel 1 if the caller does not specify a channel -- Or specifies an invalid channel if voidP(chan) then set chan = 1 else if not integerP(chan) or ¬ (integer (chan) < 1 or integer (chan) > 8) then put "Channel should be an integer from 1 to 8" set chan = 1 end if -- Play the sound puppetSound chan, the number of member soundID updateStage end -- This plays "woof" in channel 1 (the default) playSound ("woof") -- This plays "bark" in channel 3 playSound ("bark", 3)
Note that we also check whether the channel number passed in is an integer and whether it is a valid sound channel number. Our example also accepts either a sound cast member’s number or its name, just like as built-in puppetSound
command. We could enhance the error checking to make sure the specified cast member is a sound.
Refer to Example 5-3 in Chapter 5, Coordinates, Alignment and Registration Points, in Director in a Nutshell. It accepts parameters in numerous formats and includes substantial error checking.
You can extend the playSound
example to create a handler that accepts additional optional arguments, but note that the caller cannot pass a value for an optional argument unless all preceding arguments have been specified. For example, if we wanted to add a flag to playSound
indicating whether to wait for the sound to play, we could add another optional parameter called waitFlag
.
Example 1-37. Placeholder Arguments
on playSound soundID, chan, waitFlag -- Beginning of handler is the same code as example above -- but is omitted here for brevity puppetSound chan, the number of member soundID updateStage -- This will wait if waitFlag is non-zero. It will -- not wait if waitFlag is omitted, and therefore VOID if waitFlag then repeat while soundBusy (chan) nothing end repeat end if end
Tip
Arguments and parameters are always matched up by position. The first argument in the function is assigned to the first parameter in the handler definition, and so on.
In this example, if the caller specifies only two arguments, the second argument will be used as the second parameter (chan
). If the caller wants to specify waitFlag
(the third parameter), he or she must specify three arguments, including a placeholder for chan
.
-- The second argument is assumed to be the second-- parameter and is mistakenly -- interpreted as a channel number playSound ("bark", TRUE) -- Instead, use 1 as a placeholder for the chan parameter playSound ("bark", 1, TRUE)
In the previous example, some arguments are optional, but the maximum number of arguments is known. You can also create handlers that accept an unknown (ostensibly unlimited) number of arguments. The paramCount
property and param()
function decipher an unknown number of arguments passed into a handler. The paramCount
indicates the total number of parameters received and param(
n
)
returns the n
th parameter.
Example 1-38. Variable Number of Parameters
on countParams put "Total Params:" && the paramCount repeat with n = 1 to the paramCount -- This statement prints out each parameter's -- number and its value by building a fancy string put "Param" && n & ":" && param(n) end repeat end countParams countParams ("Hello", "there", 5) -- "Total Params: 3" -- "Param 1: Hello" -- "Param 2: there" -- "Param 3: 5"
Note that no parameters are declared in the handler definition of on countParams
. It will accept any number of parameters, as would be appropriate if we wanted to, say, average any number of values. If we expected a fixed number of parameters, we could instead declare some parameters (in this case a
, b
, and c
) when we define our handler, such as:
on newCountParams a, b, c put "Total Params:" && the paramCount put "Param a:" && a put "Param b:" && b put "Param c:" && c put "Param 1:" && param(1) put "Param 2:" && param(2) put "Param 3:" && param(3) end newCountParams newCountParams ("Hello", "there", 5) -- "Total Params: 3" -- "Param a: Hello" -- "Param b: there" -- "Param c: 5" -- "Param 1: Hello" -- "Param 2: there" -- "Param 3: 5"
We can access the first parameter as either a
or param(1)
. Likewise, we can access the second parameter as either b
or param(2)
, and so on. That is, param(1)
is always the first parameter, not merely the first unnamed parameter. Note that named parameters are easier to work with when you know how many to expect, but param()
and the paramCount
are more flexible. Use any combination of the two. Refer to Example 8-6 and Example 8-7 which use the paramCount
and param()
to take the sum or average of an indeterminate number of arguments.
The playSound
example discussed previously ignores extraneous arguments (that is, if more arguments are specified than the number of parameters expected), as do many Lingo commands. You can always check the paramCount
to warn the caller if too many or too few arguments are specified, such as:
if the paramCount > 3 then alert "No more than 3 please"
or
if the paramCount <> 4 then alert "Expected 4 params"
You can also check the type of each argument, as described in detail in Chapter 5:
if not integerP(param(1)) then alert "First parameter must be an integer" exit end if
The verifyParams()
function shown in Example 1-39 checks whether the parameter(s) passed into a handler are of the expected data type(s). The details are fairly complex, but you don’t need to understand them at this point.
Tip
You can often use a handler as a "black box.” You don’t need to know what happens inside the box; you need to know only what inputs it requires and what outputs it provides.
Likewise, you may provide handlers to others without supplying details on how they work. You need not understand all the magic as long as you trust the wizard behind the curtain. This book and its companion, Director in a Nutshell, try to dispel some of the mystery about how Director and Lingo work.
The verifyParams()
function shown in Example 1-39 accepts a property list containing parameters and their expected data types (it checks only for integers, floats, and strings). See Chapter 6 if you don’t understand lists, or just skip the details for now. VerifyParams()
returns TRUE
if the number and type of parameters are correct and FALSE
otherwise. You can extend verifyParams()
to handle more data types or to post an alert dialog instead of printing errors to the Message window.
Example 1-39. Verifying Parameters
on verifyParams verifyList, numInput -- Check the number of parameters vs. the number expected set numExpected = count (verifyList) if numInput < numExpected then put "Too few parameters. Expected" && numExpected return FALSE else if numInput > numExpected then put "Too many parameters. Expected" && numExpected return FALSE end if -- Check each item in the list and its data type repeat with x = 1 to count (verifyList) set nextItem = getAt (verifyList, x) case (getPropAt(verifyList, x)) of #integer: if not integerP (nextItem) then put "Expected integer for parameter" && x return FALSE end if #float: if not floatP (nextItem) then put "Expected float for parameter" && x return FALSE end if #string: if not stringP (nextItem) then put "Expected string for parameter" && x return FALSE end if otherwise: put "Unsupported type for parameter" && x return FALSE end case end repeat return TRUE end verifyParams
You can use verifyParams()
to check if your routine is called with the correct number and type of arguments. This is useful for debugging your own code or for trapping errors if you distribute your code for others to use. VerifyParams()
expects a property list containing each parameter and its expected data type. The following verifies whether a
is an integer, b
is a string, and c
is a float. It also checks whether exactly three parameters have been received.
on myHandler a, b, c -- Make sure that we received the expect parameters if not (verifyParams([#integer:a, #string:b, #float:c],[LC] the paramCount)) then alert "Something was wrong" exit end if -- Otherwise everything is okay and we can proceed. statements end myHandler
Test it from the Message window:
myHandler (12.5, "a", 5.7) -- "Expected integer for parameter 1" myHandler (12, 6, 5.7) -- "Expected string for parameter 2" myHandler (5, "a") -- "Too few parameters. Expected 3"
Reader Exercise: Modify Verify Params()
to return an error string.
See also Example 8-4 in Chapter 8.
Whew! You now have a foundation on which to build a greater understanding of Lingo. Even the most complex programs are built with simple components—variables, handlers, keywords, repeat
loops, and if
statements—so don’t be intimidated. With patience, you can (de)construct very complicated programs. Refer to Table 18-1, for a list of all Lingo keywords so that you can distinguish them from variables and custom handler names. Look at examples of other people’s Lingo code. Try to recognize the various pieces of the Lingo puzzle. (Remember diagramming sentences in English class, where you picked out the verbs, subjects, adjectives, and prepositional phrases?) Which items are variables? Which are keywords? Which are parameters? Which items are arbitrarily chosen by the programmer, and which are dictated by Lingo’s grammar or syntax?
This single chapter has covered material from both beginner and intermediate programming courses that might be spread out over many months. We also touched on some very advanced concepts that will serve you well as you read the rest of this book. Don’t be discouraged if you didn’t understand a lot of it, or if you skipped the more intimidating parts. Re-visit this chapter frequently, and you’ll find new treasures each time. It may seem hard to believe now, but when you look back on this chapter a year from now, most of the things that confused you will seem quite simple.
Most of this chapter applies to other programming languages you may encounter. If Lingo is your first programming language, rest assured that picking up additional languages becomes much easier. In Chapter 4 we compare Lingo to C/C++ so that you can see both the nitty-gritty details of Lingo and how other languages may differ.
Even though this “book work” may seem tedious, it will allow you to breathe new life into all your Director projects once you are out in the field. (Don’t forget to save your test Director movie periodically).
I leave this chapter with a reminder that I can point you in the right direction and even provide a map of the terrain and a steady compass, but you are ultimately your own navigator for the journey that lies ahead.
This exchange took place in the Director support forum:
Where else but Lingo can you find out what is TRUE
with a mere nine keystrokes?
Get Lingo in a Nutshell now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.