Chapter 7. Writing Tcl Procedures

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.

Fortune Teller

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.

What Is a Procedure?

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.

Note: Logical Errors

Note: Logical Errors

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.

Defining Procedures

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

Defining Procedures with Default Values

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

Defining Procedures with Variable Arguments

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.

Understanding Variable and Procedure Scope

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.

Note: Tcl Variables Are Passed by Value

Note: Tcl Variables Are Passed by Value

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.

Analyzing Fortune Teller

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.

Looking at the Code

#!/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]];

Understanding the Code

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.

Modifying the Code

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.179.65