Chapter 5. User Functions

Table of Contents
5.1. Functions
5.2. Libraries of sci-functions

This chapter treats Scilab's most powerful code abstraction: functions. The first section, Section 5.1, introduces in the darkest details of user-defined functions. The second section, Section 5.2, treats libraries of user-defined functions.

5.1. Functions

Functions are Scilab's the main feature for the abstraction of programming tasks. Thus, they deserve a closer look.

See also Section 2.3

5.1.1. On(e)line Function Definitions

Scilab allows functions to be defined online, this is, at the command line, in two different forms. The first form uses the builtin function deff


deff(function_head, function_body [, compile_flag])

where function_head, function_body, and compile_flag are character strings. Most often these strings are given literally, for example,


deff('y = heavyside_theta(x)', 'if x <= 0, y = 0, else y = 1, end')

If function_body contains statements that include literal strings themselves, the quotes of these strings must be doubled, creating a hard to understand mess. This quoting disaster is avoided by using the second form of online function definition, which uses the keyword function and the syntax of function files (".sci").


function function_head, function_body, endfunction

The crucial difference between function in a function file and in an online definition is that in a file the endfunction keyword is optional, whereas it is mandatory in an online definition.


function a = row_avg(m), s = sum(m, 'rows'), a = sum(s)/size(m, 'cols'), endfunction

The definition of a function with the function and endfunction keywords does not have to fit on a single input line. It can span multiple lines as the interpreter goes into "function definition mode" when it parses function. This mode resembles multi-line input in command shells and Python (though Scilab does not change its prompt to notify the user).


-->function y = foo(x)
-->   y = 1.0 + x + 0.5*x^2
-->endfunction
 
-->foo(4)
 ans  =
    13.

Both forms allow for nested function definitions – see the following section, Section 5.1.2.

5.1.2. Nested Function Definitions

Function definitions can be nested. The usual scoping rules apply. Online nested function definitions with deff are possible, but some kind of awkward, because of the massive number of quotes. deffs in functions are easy to the eye.

Example 5-1 shows a function that defines four functions in its body.

Example 5-1. Function tauc


function [t, rmin, r0] = tauc(E0, M, s, D)
    // Compute the round-trip time t, the minimum distance rmin and the point
    // of vanishing potential for a point-like particle with kinetic energy (at
    // r -> infinity ) E0, mass M in a Morse potential of steepness s and
    // depth D.

    // Morse potential
    deff('U = Umorse(r, steepness, depth)', ..
         'e = exp(-r * steepness); ..
          U = depth*(e^2 - 2*e)')

    // point of vanishing potential
    deff('y = equ0(x)', 'y = Umorse(x, s, D)')

    // reflection point
    deff('y = equ1(x)', 'y = Umorse(x, s, D) - E0')

    deff('tau = integrand(x)', ..
         'tau = sqrt( M / (2*(E0 - Umorse(x, s, D))) )')

    // rationalized units...
    units = 10.0e-10 / sqrt(1.380662e-23 / 1.6605655e-27)

    // calculate endpoints of definite integral
    r0 = fsolve(-10.0, equ0)
    rmin = fsolve(-10.0, equ1)

    // evaluate definite integral
    [t_unscaled, err] = intg(rmin, r0, integrand)
    t = 2 * units * t_unscaled
endfunction
   

As of Scilab version 2.6, nested functions do not work reliably, therefore, constructs like


function foo
    function bar
        ...
    endfunction

    ...
endfunction

should be avoided by using deff when defining bar. The fingerprint of nested functions is error 37, "incorrect function at line ...". The do-not-nest limitation is raised for one-liners, where nesting works without problems.


-->function y = foo(x), ..
-->   function a = bar(b), ..
-->       a = 1.0 + 2.0*b, ..
-->   endfunction, ..
-->   y = bar(x) / x, ..
-->endfunction

5.1.3. Functions Without Parameters or Return Value

The "Introduction to Scilab", SCI/doc/Intro.ps, solely explains functions that have one or more parameters, and return one or more values. Yet, Scilab permits all conceivable combinations of number of parameters and return values, including functions that have no parameters, or no return values.

If only one value is returned the square brackets in the function definition are optional. Therefore, the function head


function [y] = foo(x)

can be abbreviated to


function y = foo(x)

However, this is 100% pure syntactic sugar. What is much more important – and a valuable feature – is the possibility of defining a function that returns nothing as


function ext_print(x)
printf("%f, %g", x, x)

does. In Fortran parlance ext_print would be called a SUBROUTINE, whereas Ada programmers would term it a procedure.

Of similar importance is the definition of parameterless functions.


function t = hires_timer()
cps = 166e6
t = rdtsc() / cps

The parentheses after the function name are optional when defining the function, but not when calling it. Therefore the declaration of the last function could have been abbreviated to function t = hires_timer, but the call to rdtsc could not have been written as t = rdtsc / cps.

For further information about the omission of parenthesis when calling a function, see Section 5.1.8.

5.1.4. Named Parameters

The associations between the formal parameters of a function and its actual parameters may be positional or named. A positional parameter association is simply an actual parameter. All the positional parameter associations in a function call must precede all the named parameter associations. Thus, in the function call (see myplot's definition in Example 5-2)


myplot(x, y, pointtype = 4, style = 'linespoints', linetype = 2)

the first two parameter associations (x, y) are positional, and the last three (style, linetype, pointtype) are named. Two things in the previous line of code are worth noting:

  • When parameters are associated via their names the formal parameter's position is irrelevant.

  • Positional parameter associations have nothing to do with optional parameters. A named parameter can be handled as an optional parameter as well as a positional parameter.

Calling a function with named parameters does not require any special code in the function. Function myplot is an simple user-defined function:

Example 5-2. Function accepting named arguments


function myplot(x, y, style, linetype, pointtype)

// checks for optional parameters would go here :)

select style
case 'lines' then
    plot2d(x, y, linetype)
case 'points' then
    plot2d(x, y, -pointtype)
case 'linespoints' then
    plot2d(x, y, -pointtype, '020')
    plot2d(x, y, linetype, '000')
end
   

To make the two parameters linetype, and pointtype optional parameters, we add a check for the existence of these parameters in the function's, i.e. the local scope. In Example 5-3 myplot gets extended in this direction.

Example 5-3. Function accepting optional arguments


function myplot(x, y, style, linetype, pointtype)

if ~exists('linetype', 'local')         -- quotes around the parameter name are required
    linetype = 1
end
if ~exists('pointtype', 'local')        -- 'local' excludes global variables from search
    pointtype = 1
end

select style
case 'lines' then
    plot2d(x, y, linetype)
case 'points' then
    plot2d(x, y, -pointtype)
case 'linespoints' then
    plot2d(x, y, -pointtype, '020')
    plot2d(x, y, linetype, '000')
end
   

Now myplot can be called in any of the following forms:


myplot(x, y, 'lines')                   -- only positional parameters
myplot(x, y, style = 'linespoints')     -- 3rd parameter is named
myplot(x, y, 'points', 2, 3)            -- override defaults
myplot(x, y, linetype = 5, ..
       style = 'linespoints')           -- named params, one override
myplot(x, y, pointtype = 4, ..
       style = 'linespoints', .
       linetype = 2)                    -- named params where possible

5.1.5. Bulletproof Functions

If we want to write bulletproof Scilab functions, we have to take care that our functions get the right number of arguments which are furthermore of the right type, and correct dimension. This is necessary because of Scilab's dynamic nature allowing us to pass arguments of different types, dimension, etc. to a single function.

We discuss the issues of writing robust function using Example 5-4 as an illustration. The complete function definition is given in Chapter 10.

Example 5-4. Function cat


function [res] = cat(macname)
// Print definition of function 'macname'
// if it has been loaded via a library.

[nl, nr] = argn(0);                       (1)
if nr ~= 1 then
    error('Call with: cat(macro_name)');
end

if type(macname) ~= 10 then               (2)
    error('Expecting a string, got a ' ..
          + typeof(macname));
end
if size(macname, '*') ~= 1 then           (3)
    sz = size(macname);
    error('Expecting a scalar, got a ' ..
          + sz(1) + 'x' + sz(2) + ' matrix')
end

[res, err] = evstr(macname);              (4)
if err ~= 0 then
    select err
    case 4 then
        disp(macname + ' is undefined.');
        return;
    case 25 then
        disp(macname + ' is a builtin function');
        return;
    else
        error('unexpected error', err);
    end // select err
end // err ~= 0

...
   
(1)
First, we check how many actual parameters cat has received. The built-in argn returns the number of left-hand side – or output – variables nl (In this example we do not make use of nl.), and the number of right-hand side – or input – values nr.

Ensuring the correct number of input arguments always is the first step. Otherwise we cannot assume that even accessing a parameter is valid. The number of output values is not as critical, for calling a function with less output variables than specified in the function's signature causes the extra output values to be silently discarded.

After learning the number of actual parameters, we immediately check whether it is in the right range. Our example simply terminates with an error if the number of arguments is incorrect.

(2)
The next thing to address are the types of the arguments. Again we let the function fail with an error if it does not get what it wants, but this is not the only possible way of handling these kinds of errors.

It is conceivable that we convert from one type to another, say from numeric to string. Furthermore, it is possible that the type of the arguments determines the algorithm chosen, a feature normally advertised under the name "function overloading" (see Section 4.2).

(3)
Finally, we examine the arguments' structure. A function can e.g. allow scalars only, or accept scalars and matrices. Here, we enforce a scalar. In other functions certain dimensional relations of several input parameters must be enforced. E.g. the matrix multiplication A * B is only defined for size(A, 'c') == size(B, 'r').
(4)
Now we can start with the real work.

At first glance all this checking gizmos might seem exaggerated. To do it justice we should keep in mind that it is only necessary if a function must work reliably in different environments. All functions that a library exports belong to that class, because the library writer does not know how the functions will be used in the future. Quick-and-dirty functions are a different thing, so are functions that are never called interactively.

5.1.6. Function Variables

Functions are a data type on their own right. Therefore, they themselves can be arguments to other functions, and they can be elements in lists.


-->deff('y = fun(x)', 'if x > 0, y = sin(x); else, y = 1; end')

-->fun(%pi / 2)
 ans       =
    1.  

-->fun(-3)
 ans       =
  - 1.  

-->bar = fun
 bar       =
[x]=bar(y)                              -- bar is a complete copy of fun

-->typeof(bar)
 ans       =
 function   

-->deff('a = fun(u, v, w)', 'a = u^2 + v^2 + 2*u*v - w^2')
Warning :redefining function: fun                     

-->bar(%pi / 4)^2
 ans       =
    0.5

-->fun(2, 3, 4)
 ans       =
    9.

As the example shows, Scilab employs its usual copy-by-value semantics when assigning to function-variables, consistent with the assignment of any other data type.

5.1.7. Functions as Parameters in Function Calls

As mentioned above, user-defined functions can be passed as parameters to (usually different) functions. Builtin functions have to be wrapped in user-defined functions before they can be used as parameters.

The following example defines a functional that implements a property of Dirac's delta distribution.


-->deff('y = delta(a, foo)', 'y = foo(a)')

-->delta(cos)
        !--error    25 
bad call to primitive :cos

-->deff('y = mycos(x)', 'y = cos(x)')

-->delta(0, mycos)
 ans  =
    1.  

The next example is a bit more convoluted, but also closer to the real world. We define a new optimizer function, called minimize, which is based on Scilab's optim function. minimize expects two vectors of data points: xdata and ydata, a vector of initial parameters p_ini, the function to be minimized func, and an objective functional obj.

The advantage of defining separate model and objective functions is an increased flexibility as both can be replaced at will without changing the core minimization function, minimize.


function [f, p_opt, g_opt] = minimize(xdata, ydata, ..
                                      p_ini, func, obj)

// on-the-fly definition of the objective function
deff('[f, g, ind] = _cost(p_vec, ind)', ..
     '[f_val, f_grad] = func(xdata, p_vec); ..
      [f, g] = obj(f_val - ydata, f_grad)');

[f, p_opt, g_opt] = optim(_cost, p_ini);

minimize needs the model function func that returns the value and the gradient at all points x for a given vector of parameters p_vec. Moreover, we need the objective functional obj that gives the "cost", as well as the direction of steepest descent in parameter space.

In the example we choose a quadratic polynomial for the model, my_model, and least squares for the objective lsq.


function [f, g] = my_model(x, p)
g = [ones(x), x, x.*x];
f = p(1) + x.*(p(2) + x*p(3));

function [f, g] = lsq(diff, grad)
f = 0.5 * norm(diff)^2;
g = grad' * diff;

Given these definitions, we can call minimize:


dx = [0.0 1.0 2.0 2.5 3.0]';
dy = [0.0 0.9 4.1 6.1 9.5]';
p_ini = [0.1 -0.2 0.9]';

[f_fin, p_fin, p_fingrad] = ..
    minimize(dx, dy, p_ini, my_model, lsq)

xbasc();                     // clear window
plot2d(dx, dy, -1);          // plot data points ...
xv = linspace(dx(1), dx($), 50)';
yv = my_model(xv, p_fin);
plot2d(xv, yv, 1, '000');    // ... and optimized model function

5.1.8. Omitting Parentheses on Function Call by Glen Fulford

The parentheses of any one-parameter function can be omitted, if the function accepts a string argument. Moreover, the quotes for a literal string argument can be left out, too.

The is especially useful, when working interactively, and loading functions, or scripts. There is no need to type until your fingers bleed by saying


-->getf('foo.sci')

as the next two examples work just as well.


-->getf 'foo.sci'

and even


-->getf foo.sci

is OK. Note that this is not only true for built-in, but also for user-defined functions.

Function exec is an exception to the rule that a semicolon suppresses any output of the preceeding clause, if it is invoked without parenthesis. In fact, exec does echo the commands it executes if used without parenthesis despite a trailing semicolon, this is


-->exec script.sci;

with semicolon gives same results as


-->exec('script.sci')

without semicolon, whereas


-->exec('script.sci');

does not echo the commands of the script file.

5.1.9. Functions in tlists and mlists

Currently the only composite data structures that allows for storage of functions are the typed list, tlist, and the matrix-like list, mlist.

Given the typed-list t = tlist(['funlist_t', 'x0', 'x1', 'fun'], -0.5, 0.5, f), where f is e.g. defined as deff('y = f(x)', 'y = 2.0*x + 1.0'), the non-function components are accessed as usual, i.e.,


-->t("x1")
 ans  =
    0.5  
 
-->t.x0
 ans  =
  - 0.5  

See also Section 4.6.4.

However, function components cannot be called directly, e.g. t("fun")(0) or t.fun(0). Instead, we go on a little detour, either by calling feval or by using a dummy variable.


-->feval(0, t("fun"))
 ans  =
    1.  
 
-->feval(0, t.fun)
 ans  =
    1.  
 
-->_f = t("fun"); _f(0), clear _f
 ans  =
    1.  
 
-->_f = t.fun; _f(0), clear _f   
 ans  =
    1.  

Both workarounds go well with argument vectors to the function. Assigning to a dummy variable is faster than using feval.

5.1.10. macrovar

The macrovar function could be called the functional cousin of the size function. The primary purpose of macrovar is to support the Scilab-to-Fortran translator, but it can be useful for other purposes, too.

macrovar reveals five important attributes of a user function. These are the names of all

  • input variables,

  • output variables,

  • global variables,

  • functions called, and

  • local variables.

One example of an interesting use of macrovar is an integration routine that accepts integrand functions with an arbitrary number of arguments, i.e. over arbitrary many dimensions.


function vol = int_cube(ifun)
// integrate ifun in an appropriate hypercube
// (0, ..., 0), ..., (1, ..., 1)

ifun_var = macrovar(ifun)
ifun_sz  = size(ifun_var(1)) // names of input arguments
ifun_dim = ifun_sz(1)

for d = 1:ifun_dim
     // integrate in one dimension
end