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.
Functions are Scilab's the main feature for the abstraction of programming tasks. Thus, they deserve a closer look.
See also Section 2.3
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.
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
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.
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
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);if nr ~= 1 then error('Call with: cat(macro_name)'); end if type(macname) ~= 10 then
error('Expecting a string, got a ' .. + typeof(macname)); end if size(macname, '*') ~= 1 then
sz = size(macname); error('Expecting a scalar, got a ' .. + sz(1) + 'x' + sz(2) + ' matrix') end [res, err] = evstr(macname);
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 ...
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.
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).
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.
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.
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
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.
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.
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