Writing Procedures to be called as Commands or Functions

Table of Contents

  1. Introduction
  2. Command or Function?
  3. Writing a Procedure to be called as a Function
    1. Function sind
    2. Function lam
    3. Function get_bin
    4. Function fact
    5. Function factorial
  4. How NAP Functions Work
  5. Writing a Procedure to be called as a Command
    1. Command write_expr
    2. Command get_binary

Introduction

One can write a Tcl procedure which defines a new NAP function or replaces a built-in NAP function. Of course it is also possible to write a Tcl procedure which is called in the normal Tcl manner (as a Tcl command) to do something related to NAP.

The following sections include various examples of procedure definition directly from the command-line. Of course in practice one would normally create such code in files, which would be sourced.

Tcl has facilities for automatically defining undefined commands when an attempt is made to execute them. In particular, the array auto_index contains the commands to define indexed commands.

Users with small libraries of their own procedures may prefer to simply source the relevant files as part of Tcl startup. The startup files distributed with NAP automatically source any file called my.tcl in the home directory. This file can contain source commands to define one's own procedures.

Command or Function?

Before writing a procedure to perform some NAP task, one needs to decide whether it is to be called as a command or as a function. The first question to ask is "Is the sole purpose to define a NAO?". If the answer is "no" then it should be a command. If the answer is "yes" then it should probably be a function, provided the arguments are not too complex. If there are many optional string arguments then a command would probably be better. Such a command can be called from within a NAP expression using the Tcl bracket [] facility.

Writing a Procedure to be called as a Function

The following examples comprise a variety of function definitions starting from the simplest imaginable and ending with some sophistication.

Function sind

Let's begin with a simple function defined by a simple expression with one argument. How about the sine of an angle in degrees? Let's call it "sind". The procedure can be defined on one line as follows:
% proc sind degrees {nap "sin(1r180p * degrees)"}
Note that "1r180p" is the constant π/180. Now let's test function "sind":
% nap "x = 0 .. 180 ... 30"
::NAP::76-76
% nap "y = sind x"
::NAP::83-83
% [nap "transpose(x /// y)"]
            0             0
           30           0.5
           60     0.8660254
           90             1
          120     0.8660254
          150           0.5
          180  1.224606e-16

Function lam

Now let's define a function (with two arguments x and y) defined by the above expression
"transpose(x /// y)".
This is the transpose of the laminated arguments, so let's call it "lam".

% proc lam {
    x
    y
} {
    nap "z = x /// y"
    nap "transpose z"
}
There are two lines in the body of this procedure. The result of the final line defines the result of the function. Testing:
% [nap "lam(x,y)"]
            0             0
           30           0.5
           60     0.8660254
           90             1
          120     0.8660254
          150           0.5
          180  1.224606e-16

Function get_bin

Now let's define a function "get_bin" for binary input using the "nap_get" command:

% proc get_bin {
    filename
    {datatype {'f32'}}
    {swap 0}
} {
    # convert all arguments to strings
    set filename [[nap "filename"]]
    set datatype [[nap "datatype"]]
    set swap     [[nap "swap"]]
    set channel [open $filename]
    nap "in = [nap_get [lindex {binary swap} $swap] $channel $datatype]"
    close $channel
    nap "in"; # Define result
}
Note that the arguments "datatype" and "swap" have default values. Also note how all three arguments are converted from NAP expressions to Tcl strings.

Now let's test it. The following uses the OOC binary method to write six f64 values to the file "double.dat". Then this file is read using function "get_bin".

% set file [open double.dat w]
filee1eb10
% [nap "{1.5 -3 0 2 4 5}"] binary $file
% close $file
% nap "x = get_bin('double.dat', 'f64')"
::NAP::27-27
% $x all
::NAP::27-27  f64  MissingValue: NaN  References: 1
Dimension 0   Size: 6      Name: (NULL)    Coordinate-variable: (NULL)
Value:
1.5 -3 0 2 4 5

Function fact

Now let's define a factorial function called "fact". Of course we cannot resist the temptation to use recursion:

% proc fact n {
    if {[$n] > 1} {
	nap "n * fact(n-1)"
    } else {
	nap "1"
    }
}
This works fine for scalar arguments:
% [nap "fact 4"]
24
% [nap "fact 1"]
1
% [nap "fact 0"]
1
But the following shows that it fails for a vector argument!
% [nap "fact {0 1 4 6}"]
1

Function factorial

One can define a proper elemental factorial function as follows:

% proc factorial n {
    if {[[nap "max(reshape(n)) > 1"]]} {
	nap "n > 1 ? n * factorial(n-1) : 1"
    } else {
	nap "1"
    }
}
% [nap "factorial {0 1 4 6}"]
1 1 24 720
Note the double brackets in the if command. The inner brackets produce an OOC-name. The outer brackets execute this OOC to produce the string "0" or "1".

How NAP Functions Work

As an example, consider the expression "a(b)", which is of course equivalent to "a b". NAP checks whether "a" is a Tcl variable. If not, it is assumed to be a function. In this case NAP first looks for a Tcl procedure called "::NAP::a." If this does not exist then NAP looks for a built-in NAP function called "a". If this does not exist then NAP looks for a Tcl procedure called "a".

The following example shows that a procedure with the global name "sin" does not override the built-in function with that name, whereas defining it within the NAP namespace "::NAP::" does override:

% proc sin x {nap "2*x"}
% [nap "sin 1"]
0.841471
% proc ::NAP::sin x {nap "2*x"}
% [nap "sin 1"]
2

It is possible to call some procedures as either functions or commands. The following example defines and uses the same function "sind" defined above:

% proc sind degrees {nap "sin(1r180p * degrees)"}
% [nap "sind 30"]; # call as function
0.5
% nap "s = [sind 30]"; # call as command within NAP expression
::NAP::80-80
% $s
0.5
% [sind 30] all; # call as direct OOC
::NAP::86-86  f64  MissingValue: NaN  References: 0
Value:
0.5

But there is a problem calling procedures as commands if the result is referenced by a variable which is local to the procedure. At the end of the procedure Tcl deletes such local variables. This causes the referenced NAOs to be deleted. For example we could redefine function "sind" as follows:

% proc sind degrees {
    nap "result = sin(1r180p * degrees)"
    nap "result"
}
% [nap "sind 30"]
0.5
% [sind 30]
invalid command name "::NAP::32-32"

Note that the call as a function still worked but not the call as a command. NAP operates in a special mode while executing a procedure called as a function. The deletion of NAOs referenced by local variables is delayed until after the result has been saved. This is one advantage of calling procedures as functions rather than commands.

Writing a Procedure to be called as a Command

Command write_expr

First let's define a procedure whose Tcl result is empty and of no interest. It is obvious that such a procedure cannot be called as a function. The purpose of the procedure is to write to a text file the result of a NAP expression, which can of course contain variables and therefore must be executed in the caller's namespace. The following defines and tests the procedure:
% proc write_expr {
    expr
    filename
} {
    set channel [open $filename w]
    puts $channel [[uplevel nap \"$expr\"] value]
    close $channel
}
% nap "to = 5"
::NAP::52-52
% write_expr "1 .. to /// {0 7}" matrix.txt
% cat matrix.txt; # display contents of file 'matrix.txt'
1 2 3 4 5
0 7 0 7 0

Command get_binary

Next let's define a procedure called "get_binary" which is intended to be called as a command, but does essentially the same thing as the above function "get_bin". This will help us to compare the two techniques in a situation where each has some advatages and some disadvantages. We assume the file "double.dat" still exists. The following example defines and tests procedure "get_binary":

% proc get_binary {
    filename
    {datatype f32}
    {swap 0}
} {
    set channel [open $filename]
    nap "in = [nap_get [lindex {binary swap} $swap] $channel $datatype]"
    close $channel
    nap "+in"; # Define result as copy of 'in' to prevent premature deletion
}
% nap "x = [get_binary double.dat f64]"
::NAP::63-63
% $x all
::NAP::63-63  f64  MissingValue: NaN  References: 1
Dimension 0   Size: 6      Name: (NULL)    Coordinate-variable: (NULL)
Value:
1.5 -3 0 2 4 5
Note that "get_binary" is simpler to define and simpler to use than "get_bin". The main reason for this is the fact that all three arguments are used as strings rather than NAOs. One disadvantage of the command approach is the need to define the result as "+in" rather than simply "in".

Author: Harvey Davies       © 2002, CSIRO Australia.       Legal Notice and Disclaimer
CVS Version Details: $Id: writing_procs.html,v 1.3 2004/11/11 04:14:57 dav480 Exp $