Functions: Structure, Evaluation and Calling Scheme

There are only two user-created objects in the FISH language that can be executed: functions and operators. The name of a function follows the fish define command, can be followed by an optional list of arguments, and its scope terminates with the end statement. The end statement also serves to return control to the caller when the function is executed. (Note that the exit statement returns control prior to an end statement.) Consider this example that shows function construction and use.

fish define xxx
    aa  = 2 * 3
    bb  = 0
    xxx = aa + bb

The value of xxx is changed when the function is executed. The variable aa is computed locally, but the existing value of bb is used in the computation of xxx. If values are not explicitly given to variables, they default to zero (integer). It is not necessary for a function to assign a value to the variable corresponding to its name.

An alternative method of returning a value is to use the return statement, as shown in the following example:

fish define yyy
    aa  = 2 * 3
    return aa + bb

In this case, the value aa+bb is returned whenever yyy is called, but the actual FISH parameter associated with yyy is not changed.

A function may be invoked in a variety of ways, which include:

  1. as the single word xxx inside a FISH function;

  2. as the variable xxx in a FISH formula — e.g.,:

    new_var = (math.sqrt(xxx) / 5.6)^4;
  3. as a single word [xxx] enclosed in brackets as outlined here;

  4. as a symbolic replacement for a number in a FLAC3D input line; or

  5. as a parameter to a command that accept FISH functions as options (for example, fish history.). In this case the function will generally be invoked as a callback at a later time.

  6. as a special case of (5), a function may be executing during cycling by using the fish callback command or the fish-call keyword to the model solve command. Although in this case we recommend using operators for runtime efficiency.

FISH functions may also take an arbitrary number of arguments. When the function is defined these are indicated by including the arguments in the function definition. For instance:

fish define george(one,two)
    george = one * two

A function may be referenced in another function before it is defined; the FISH compiler simply creates a symbol at the time of first mention, and then links all references to the function when it is defined by a fish define command. A function cannot be deleted, but it can be redefined.

Function calls may be nested to any level (i.e., functions may refer to other functions, which may refer to others, ad infinitum). Function calls may be recursive (i.e., execution of a function can invoke that same function), however a function call nesting limit of 64 is enforced. The next example shows an unbounded recursive function call, which is not allowed, because the name of the defining function is used in such a way that the function will try to call itself without limit. The example will produce an error on execution:

;; Bad recursion
data scalar create (0,0,0)
data scalar create (0,0,1)
data scalar create (1,0,0)
fish define pos_sum
    pos_sum = (0,0,0)
    loop foreach local scalar data.scalar.list
        pos_sum = pos_sum + data.scalar.pos(scalar)

In general, the rule is that a function should avoid retrieving its own value. The same function should be coded as shown:

;; No Recursion
fish define pos_sum
    pos_sum = (0,0,0)
    loop foreach local scalar data.scalar.list
        pos_sum += data.scalar.pos(scalar)

The difference between variables and functions is that a function is always executed whenever its name is mentioned, while a variable simply conveys its current value. However, the execution of a function may cause other variables (as opposed to functions) to be evaluated. This effect is useful, for example, when several histories of FISH variables are required: only one function is necessary in order to evaluate several quantities, as below:

model new
fish define h_var_1
    global bp22 ; pointer to grid point with ID = 22, set during creation...
    global bp45 ; pointer to grid point with ID = 45, set during creation...
    local xx = gp.pos.x(bp45)
    h_var_1 = xx
    h_var_2 = gp.pos.x(bp22)
    h_var_3 = math.abs(h_var_2 - xx)  ; use of xx here avoids recursion
    h_var_4 = gp.vel.x(bp45)
    h_var_5 = gp.vel.x(bp22)
    h_var_6 = math.abs(h_var_5 - h_var_4)

The FLAC3D commands to request histories might be:

fish history h_var_1
fish history h_var_2
fish history h_var_3
fish history h_var_4
fish history h_var_5
fish history h_var_6

The function h_var_1 would be executed by FLAC3D’s history logic at a specified interval. But as a side effect, the values of h_var_2 through h_var_6 would also be updated.