Procedures enable you to replace a commonly used sequence of commands with a single new command. Known as subroutines or functions in other programming languages, Tcl procedures can be called with or without arguments. You will also learn about variable and procedure scope, which determines when and where variables and procedures are visible. Together, procedures and an understanding of variable and procedure scope give you the tools you need to start implementing your Tcl scripts in a more modular and easy-to-maintain manner.
This chapter’s game, Fortune Teller, is a poor man’s implementation of the classic UNIX game, fortune
. Primarily a vehicle for demonstrating the use of Tcl procedures, Fortune Teller also uses Tcl’s list functionality discussed in Chapter 5. To play this game, execute the script fortune.tcl in this chapter’s code directory. Although the fortunes you see might differ, the output should resemble the following:
$ ./fortune.tcl Everything that you know is wrong, but you can be straightened out. $ ./fortune.tcl Your supervisor is thinking about you. $ ./fortune.tcl Live in a world of your own, but always welcome visitors.
Tcl procedures replace and parameterize a commonly or frequently used collection of commands with a single command. Procedures enable you to create your own Tcl commands and, if you are so inclined, to replace core Tcl commands with your own implementations (not recommended when you’re starting out, but certainly possible). Procedures eliminate blocks of repetitive code, making scripts easier to edit, read, and understand. Programs using procedures are easier to edit because if you change a procedure, you only edit a single block of code; blocks of repetitive code, on the other hand, require multiple edits, introducing the possibility of typos and, more than likely, bugs. Procedures make programs easier to read because repeated blocks of code in a program not only make it longer, but they also create what amounts to distracting visual noise. In the absence of this visual noise, I find it easier to understand what a script is doing.
Procedures separate use of a command from its implementation, making it possible to modify the implementation without having to edit multiple files. While this simplifies editing (I’d rather edit one file than, say, ten), it also simplifies debugging. I don’t know about you, but I don’t want to grovel through a bunch of code blocks to track down a typo or thinko. It’s much simpler to modify a single procedure. Yet another virtue of procedures is that you can use them in multiple scripts. After you have written and debugged a procedure, you can reuse it in multiple programs.
A thinko is the mental or logical equivalent of a typo. For an interesting discussion of the origin of this geeky idiom, see its entry in the Jargon File at http://www.catb.org/esr/jargon/html/T/thinko.html.
A number of the example scripts in the previous chapters generated a random number between a minimum and maximum value, inclusive. I’ve had to write (well, cut-and-paste) the code several times and have hard-coded the minimum and maximum values. For example:
From guess_rand.tcl: set target [expr {int(1 + (rand() * 19))}];
From blackjack.tcl: set index [expr {int(rand() * [llength $list])}];
From break.tcl: set num [expr int(1 + (rand() * 21))];
From cards.tcl: set index [expr {int(rand() * [llength $list])}];
From jeopardy.tcl: set i [expr {int(1 + (rand() * 5)) }];
Without going into detail about why and how it works, the algorithm underlying all of these commands is, in pseudo-code:
random_num = minimum_val + (rand() * (maximum_val – minimum_val))
If your random number generator returns values between 0 and 1 inclusive, you can use this algorithm as is. However, if your random number generator does not return 1, which is the case with Tcl’s rand()
function, you need to add 1 to the expression maximum_val–minimum_val
. Thus, the algorithm becomes:
random_num = minimum_val + (rand() * (maximum_val–minimum_val + 1))
It would be much simpler and more general to create a procedure (call it RandomInt
) that accepts two parameters specifying a minimum value and a maximum value and that returns a random integer between (and including) those parameters. After I show you the syntax for defining procedures, that’s exactly what I’ll do.
But I’m getting ahead of myself. In addition to abstracting a block of code into a single, possibly parameterized command, Tcl procedures have two other features that you’ll grow to appreciate: They can have default parameters, and they can accept a variable number of arguments. Default parameters are formal parameters which assume a predefined value if you omit the corresponding argument when you call the procedure. In the case of RandomInt
, for example, I could define it so that the minimum value defaults to 1 unless specified, so that instead of writing RandomInt 1 100
, I can write RandomInt 100
. If I want a random number between 10 and 20, I would write RandomInt 10 20
.
Procedures that accept a variable number of arguments add an additional level of generality to your procedures. Suppose that you have a procedure that formats and prints its two arguments in a particular manner. Later on, you discover that you need a similar procedure to format and print three arguments. Later still, you realize you need to do the same with four arguments. Rather than write three separate procedures to handle each case, you can write a single procedure that accepts at least two arguments but can accept an arbitrary number of arguments in excess of two. And, before you ask, yes, you can write procedures which have default parameters and which accept a variable number of arguments.
The syntax for creating a procedure is:
proc name params body
The proc
command creates a new Tcl procedure named name
with the formal parameters specified by params
. The commands specified in body
are executed each time name
is invoked. If name
already exists as a command or procedure, the new procedure replaces it. The params
argument is required in the procedure definition, but can be an empty list ({}
), so it is possible to create a procedure that doesn’t accept any arguments. If the params
argument isn’t empty, each argument is a list consisting of one or two elements, the first of which is the argument’s name and the second of which, if present, is that argument’s default value. If the last item in params
is the keyword args, then each actual argument in excess of the defined formal parameters will be assigned to a list variable named args
(which is local to the procedure).
I’ll start with a simple procedure, the RandomInt
procedure I promised (see random_int.tcl in this chapter’s code directory):
proc RandomInt {min max} { set i [expr int($min + (rand() * ($max – $min + 1)))]; return $i; } puts "Number between 0 and 100: [RandomInt 1 100]"; puts "Number between 1 and 4: [RandomInt 4]"; puts "Number between 1000 and 2000: [RandomInt 1000 2000]";
RandomInt
accepts two arguments: min
and max
, which generate a random integer between and including those values, and return the generated value. Here’s RandomInt
in action:
$ ./random_int.tcl
Number between 0 and 100: 86
Number between 1 and 4: 3
Number between 1000 and 2000: 1805
The rule for using default parameter values is:
Parameters with default values must appear after all parameters that do not have default values.
Default parameters must appear at the end of the parameter list because the interpreter assigns actual arguments to formal parameters sequentially. If the first parameter has a default value and subsequent ones don’t, there is no way for Tcl to determine to which formal parameter to assign a given argument.
The next version of RandomInt
uses a default value, defining min
to have a default value of 1 (see random_def.tcl in this chapter’s code directory):
proc RandomInt {max {min 1}} { set i [expr int($min + (rand() * ($max – $min + 1)))]; return $i; } puts "Number between 0 and 100: [RandomInt 100 0]"; puts "Number between 1 and 4: [RandomInt 4]";
The difference with this definition of RandomInt
is that min is defined as {min 1}
, which means that if you call RandomInt
with a single argument, that argument will be assigned to max
, while min
will be assigned the default value of 1. The other difference is that min
is the second parameter, resulting in an ugly, counterintuitive calling interface for cases that need to specify the (non-default) minimum value.
Here’s the output of random_def.tcl:
$ ./random_def.tcl
Number between 0 and 100: 98
Number between 1 and 4: 3
To create a procedure accepting a variable number of arguments, specify args
as the final element of the formal parameter list. You must write code in the procedure body to process the arguments that are not assigned to formal parameters. Arguments not assigned to formal parameters are assigned to the procedure-local args
list variable. I’ll explain variable and procedure scope in the next section, “Understanding Variable and Procedure Scope.” Again, I’ll start with a simple procedure that prints its arguments one argument per line:
proc PrintArgs {args} { foreach arg $args { puts $arg; } } PrintArgs "5 arguments" Ace King Queen Jack; PrintArgs "11 arguments" 1 2 3 4 5 6 7 8 9 10; PrintArgs;
The body of PrintArgs
consists of a simple foreach
loop that iterates through the args
list and prints each element. It doesn’t specify a return value, so the default return value is the value of the last executed command, which in this case is the empty string (puts
’ return value). The argument list is the special parameter args
, so you can pass zero or more arguments to PrintArgs
. Here’s an example of PrintArgs
at work (see print_args.tcl in this chapter’s code directory):
$ ./print_args.tcl
5 arguments
Ace
King
Queen
Jack
11 arguments
1
2
3
4
5
6
7
8
9
10
Notice that the PrintArgs
invocation that has an empty argument list results in no output. Another feature to notice is the list-oriented nature of the arguments. Specifically, the first arguments of the first two PrintArgs
calls are the two-element sublists 5 arguments
and 11 arguments;
the foreach
loop handles these sublists as a single element, as you would expect.
A slightly more useful procedure is ReverseArgs
, which returns its argument list in reverse order:
proc ReverseArgs {args} { proc ReverseArgs {args} { for {set i [expr [llength $args] – 1]} {$i >= 0} {incr i–1} { lappend reversed [lindex $args $i] } return $reversed; } puts [ReverseArgs Ace King Queen Jack]; puts [ReverseArgs 1 2 3 4 5 6 7 8 9 10]; #puts [ReverseArgs];
The ReverseArgs
procedure’s argument is the special parameter args
, which means that it accepts zero or more arguments. However, because of the way the procedure body is defined, you must invoke ReverseArgs
with at least one argument or else the return command will raise an error that the reversed
variable you are trying to return doesn’t exist. The reversal is accomplished by iterating backward through the list. The for loop does the bulk of the work, iterating from the end of the args
list (using the expression end-$i
and incrementing the counter variable i
on each iteration) to its beginning. On each iteration, I use the lindex
command to peel the next element off the end of the list. I use lappend
to assign the value to the reversed
list. After grabbing element 0 (which is actually the last element in this case), ReverseArgs
returns the reversed list, which can then be printed or otherwise used by the calling command.
The output that follows shows how ReverseArgs
works (see reverse_args.tcl in this chapter’s code directory):
$ ./reverse_args.tcl
Jack Queen King Ace
10 9 8 7 6 5 4 3 2 1
I leave discovering what happens if you call ReverseArgs
with no arguments as an exercise for you.
In general, scope determines where and when variables and procedures are visible. When referring to variables, scope controls the range of commands and procedures in which a given variable can be accessed. For procedures, the default scoping rules are simple:
Procedure names not defined in a user-defined namespace have global scope which means that you can use a procedure anywhere in your script.
Procedure names and variables names exist in different namespaces, which means you can have a variable named count and a procedure named count
in the same script.
I don’t discuss user-defined namespaces in this book, but you should be aware that Tcl procedures can have non-global scope if they are defined in user-defined namespaces.
Although Tcl’s grammar allows you to have procedures and variables with the same name, I don’t recommend taking advantage of this feature in practice unless you have a compelling reason to do so. The Tcl interpreter can easily and efficiently disambiguate identically named procedures and variables; your mental interpreter might not be so readily adept.
For variables, scoping rules are slightly more complicated, but only slightly:
Variables defined outside of any procedure are global variables and can be used anywhere in the script, except inside procedures. Global variables are not, by default, visible inside procedures.
Variables defined inside a procedure are said to be local to that procedure. That is, a variable named count
in the procedure FooProc
is different from a variable named count in BarProc
.
To use a global variable inside of a procedure, you must use the global
command to make that variable visible to the procedure.
So much for the theory and rules. Practically speaking, consider a script that defines a variable named count
. Suppose that this same script has a procedure which also defines a variable named count and a procedure named count:
proc SetCount {} { set count 9; puts "In SetCount, count is $count"; } proc count {} { set count 0; puts "In count, count is $count"; } set count 10; puts "Before count, count is $count"; count; puts "After count, count is $count"; puts "Before SetCount, count is $count"; SetCount; puts "After SetCount, count is $count";
The procedure SetCount
sets a variable named count
to 9;
the count
procedure sets its count variable to 0;
the script itself sets the global count
variable to 10
. When control returns to the main script after the procedures terminate, the global count variable retains its original value of 10
(see local.tcl in this chapter’s code directory):
$ ./local.tcl
Before count, count is 10
In count, count is 0
After count, count is 10
Before SetCount, count is 10
In SetCount, count is 9
After SetCount, count is 10
As you can see, the value of the global variable count
is unaffected by either count
or SetCount
. Similarly, the Tcl interpreter has no problem distinguishing between the two variables named count and the procedure named count
.
If your intent is to modify the global count, use the global
command inside the procedure to add the global variable to the procedure’s scope. The script global.tcl in this chapter’s code directory shows you how to use global
. The only change from the previous script is the definition of SetCount:
proc SetCount {} { global count; set count 9; puts "In SetCount, count is $count"; }
At the top of the procedure body, I inserted the command global count;
, which adds the global variable named count
to SetCount's
scope. The effect is clear in the script’s output:
$ ./global.tcl
Before count, count is 10
In count, count is 0
After count, count is 10
Before SetCount, count is 10
In SetCount, count is 9
After SetCount, count is 9
global'
s syntax is:
global varName
?...?
global
adds each varName
specified to the current scope. The global
command must be used inside a procedure—using it in the top-level code has no effect—so if you need to modify a global variable in multiple procedures, you need to use the global command with that variable in each procedure.
Those readers with a programming background, particularly C, are no doubt wondering whether Tcl passes variables by reference or by value. By default, Tcl passes variables by value. Moreover, Tcl lacks a notion of passing a variable by reference, that is, of passing the memory address of a variable to a procedure, because Tcl lacks (fortunately or otherwise) pointers. Tcl does
support an effectively equivalent operation, pass by name. If you need to pass a variable’s name to a procedure, use the upvar
command. upvar
is more advanced a topic than I’m covering in this book, though, so I refer curious readers to the man page (man 3tcl upvar
) for the gory details on upvar
.
Honestly, Fortune Teller is a simple game. You learned everything you need to know to write it yourself in the previous six chapters, except for the use of procedures. Its sole purpose in life is to illustrate the most salient features of defining and using Tcl procedures.
#!/usr/bin/tclsh # fortune.tcl # Display a randomly selected fortune # Block 1 # Return a random integer between min and max, inclusive proc RandomInt {min max} { set i [expr int($min + (rand() * ($max–$min + 1))]; return $i; } # Block 2 # Show the fortune at the specified index proc ShowFortune {index} { global fortunes; puts [lindex $fortunes $index]; } # Block 3 # A list of fortunes to get started set fortunes [list {Avert misunderstanding by calm, poise, and balance.} {Day of inquiry. You will be subpoenaed.} {Everything that you know is wrong, but you can be straightened out.} {Good news. Ten weeks from Friday will be a pretty good day.} {Live in a world of your own, but always welcome visitors.} {So you're back... about time...} {Tomorrow will be cancelled due to lack of interest.} {You are fairminded, just and loving.} {You have a deep interest in all that is artistic.} {You may get an opportunity for advancement today. Watch it!} {You will be divorced within a year.} {You will contract a rare disease.} {You will live to see your grandchildren.} {You'll be sorry...} {Your supervisor is thinking about you.}]; # Block 4 # A single command shows the fortune ShowFortune [RandomInt 0 [llength $fortunes]];
Block 1 reuses the RandomInt
procedure to return a randomly selected integer between two numbers. Nothing new here. Block 2 defines a gratuitous procedure named ShowFortune
, which shows the user his fortune. ShowFortune
accepts a single argument, index
, which specifies the element from the fortunes array
to display. ShowFortune
uses the global
command to access the global variable fortunes
, which is necessary because the fortunes array
is a global variable. Speaking of the global fortunes array
, Block 3 defines it with 15 quips. A significant improvement, which will be possible after you read the next chapter (Chapter 8, “Accessing Files and Directories”), would be to read the list of fortunes from a file rather than tediously defining them inline.
After all of the set-up work is complete, actually displaying the user’s fortune is anti-climactic, being reduced to a single command that calls both of the procedures defined at the beginning of the program. Here again, I took advantage of Tcl’s command substitution and nested command capabilities: the result of the command [llength $fortunes]
becomes the second argument to the RandomInt
procedure, whose own result becomes the index
argument to ShowFortune
, which displays the fortune selected by the index.
Here are some exercises you can try to practice what you learned in this chapter:
7.1 Modify the RandomInt
procedure to throw an error if min
is greater than or equal to max
. Test the behavior.
7.2 Modify block 4 of fortune.tcl to display fortunes until the user indicates to stop by pressing a key, such as “q” for quit or “x” for exit.
Procedures eliminate blocks of repetitive code, making scripts easier to edit, read, and understand. Procedures and variables reside in different namespaces, so it is possible, although not necessarily advisable, to have procedures and variables with the same name. By default, variables in Tcl have global scope but are not visible inside procedures. To make global variables visible inside a procedure, you must use the global
command with that variable inside the procedure. Variables inside procedures are local to the procedure and thus do not clash with global variables, or like-named variables in other procedures.