4.2. Operator Overloading[1]

Scilab bears a feature which strongly reminds one of object-oriented programming languages: operator overloading. Yet, Scilab is not object oriented. Strictly speaking operator overloading has nothing to do with object-oriented programming, but as it turns out overlading operators is particularly useful in object-oriented languages. Overloading grafts new functionality onto an existing function-name. Scilab accomplishes this with a special syntax similar to mangled symbol names of a C++-compiler's output.

The overloading of operators is described in Section 3.3 ("Definition of Operations On New Data Types") of in SCI/doc/Intro.ps, and information is also available through help overloading.

Even if you never will overload an operator, knowing the syntax and the function codes helps when deciphering error messages that involve overloaded operators. The following session transcript shows what happens if you request the boolean matrix bm to be converted into a string.


-->bm = [%t, %f; %f, %f]
 bm  =
! T F !
! F F !

-->string(bm)
           !--error   246
impossible to overload this function for given argument type(s)
       undefined function %b_string

Without further knowledge the user is nothing but puzzled by "undefined function %b_string".

4.2.1. Overloading crash course

If we drop the buzzword "Operator overloading", which comes from the OO-camp, and call every operator a function, we are (a) absolutely right in a mathematical sense, and (b) get a good grasp of what is going on. Operators are simply functions written in a special syntax. In most imperative languages (C and descendants, Pascal and descendants) functions are written in prefix notation, i.e. the function name precedes all arguments. In the same languages operators are written in infix notation, i.e. the operator put between the operands.

Lisp as a (functional) language which employs pure prefix syntax all functions and operators are written before all arguments.


(+ 3.9 54.0 -4.5 74.5 -57 -56)
(setq x (list "a" -1 (- 3 10)))
(length x)

The same expressions look more or less differently in Scilab:


3.9 + 54.0 + (-4.5) + 74.5 + (-57) + (-56)
sum( [3.9 54.0 -4.5 74.5 -57 -56] )     -- alternative to previous line
x = list("a", -1, (3 - 10))
length(x)

As becomes clear in the above example, operators are specially written functions, but otherwise behave like ordinary functions.

Overloading has been hyped since to advent of C++. A closer look reveals that even Fortran-77 endows certain intrinsics with an overloaded syntax. What the heck is overloading? To overload a symbol means assigning another meaning to it, augmenting the existing meaning(s). Typically, the symbol is a function name and the additional meaning is an additional function definition.

How can the language decide which definition to take? That depends on the language. The most common scheme to determine which function definition to trigger is the analysis of the actual function arguments. Fortran-77 provides so-called generic functions, sin is one example, which can be called with arguments of several types and the compiler selects the routine that matches that type.


      program f77ovl

      implicit none

      real xr, s1
      double precision xd, s2
      complex xc, s3

*     floating point literals default to real*4 in f77
      xr = 1.0
      s1 = sin(xr)                      -- compiler selects single precision routine

      xd = 1.0d0
      s2 = sin(xd)                      -- compiler selects double precision routine

      xc = (1.0, 0.0)
      s3 = sin(xc)                      -- compiler selects complex routine

*     alternative using explicit call
      s2 = dsin(xd)                     -- user demands double precision routine
      s3 = csin(xc)                     -- user demands complex routine

      end

Modern languages like e.g. C++, F9x, and Ada let the user define functions with the same name as long as they can be uniquely identified by their argument list (C++) or argument list and return value (Ada).

We can define three Maximum functions. The Ada-compiler distinguishes them by their arguments and return values.


function Maximum(X1, X2 : Float) return Float;
function Maximum(F1, F2 : Fraction) return Fraction;
function Maximum(I1, I2 : ArbitraryPrecisionInteger) return ArbitraryPrecisionInteger;

As we know from the beginning of this section, operators are functions written in a special way. Thus it is easy to imagine that an operator can be overloaded just the way a function can. The next Ada example shows how the addition operator can be overloaded.


function "+"(Left, Right : Fraction) return Fraction is
begin
    return -- code for addition of two fractions
end "+";

If you want to learn more about overloading and class construction and object oriented (C-) programming, we recommend Scott Myers' books [Myers:EffCPP:1998] , and [Myers:MoreEffCPP:1996] .

4.2.2. Overload syntax

In Scilab an operator gets overloaded with a new function, if we define this function having a special name in a particular format. For unary operators the format is


    function result = %optype_opcode(argument)

whereas for binary operators except insertion and extraction it is


    function result = %optype1_opcode_optype2(argument1, argument2)

where valid operand-variable-type codes for optype, optype1, and optype2 are defined in Table 4-3 and Table 4-4, and the operator-codes opcode are defined in Table 4-5. The formal function arguments argument, argument1, argument2, and result are usual argument and return-value names. To descibe the syntax in words: a percent-sign starts the definition followed by the type[s] of the operands and the operator seperated by [an] underscore[s].

The syntax for overlading vector/matrix insertion


    target(index1, index2, ..., indexN) = source

and vector/matrix extraction


    target = source(index1, index2, ..., indexN)

is a bit more convoluted as it has to account for the indices:


    // insertion
    function target = %targettype_i_sourcetype(index1, index2, ..., indexN, source, target)
    // extraction
    function [result1, result2, ..., resultM] = %sourcetype_e(index1, index2, ..., indexN, source)
Warning

The online-help of Scilab-2.5, help overloading, is incorrect in its explanation of the argument names to insertion-overloading. It says that target is the next-to-last, and source is the last argument. In fact the two arguments occupy exchanged positions as we have listed them.

Note that for extraction the number of return values, M, is completely independent of the number of index expressions, N.

Table 4-3. List of all operand type codes

Variable type Code string optype Code index
floating point scalar, vector, or matrix s [a] 1
polynomial p[a] 2
boolean b[a] 4
sparse matrix sp 5
sparse boolean matrix spb 6
Matlab® sparse matrix msp 7
matrix of integers (8, 16, or 32bit entries) i 8
string c[a] 10
uncompiled function m[a] 11
compiled function mc 13
function library f 14
untyped list l[a] 15
typed list name of the tlist [b] 16
matrix list ml 17
pointer ptr 128
? ip 129
Notes:
a. This type code is already overloaded by Scilab itself.
b. The formal code string for typed lists is tl.

Two types are particularly well suited for overloading; these are the tlist and its close relative the mlist. tlists are used by Scilab itself to define some sophisticated types like polynomials, or sparse boolean matrices. Table 4-4 summarizes all types t, in use as of version 2.5.1. As the type of a tlist is the list's first element, we sometimes call it, in a Lisp like manner, the head of the tlist. Note that when working with tlists of type t, Scilab calls the predefined function for untyped lists, %l_opcode, or %l_opcode_l until the user provides [a] replacement function[s] with the name %t_opcode, or %t_opcode_t.

Note

Only the first 8 characters of the name of a tlist or mlist are significant when overloading any unary or binary operator! See also Section 4.6.1.

The first column of Table 4-4 states the name of the variable type, column two lists the tlist identification heads, and column three holds the code number, Scilab associates with the specific head. The type-name/type-code – not only for tlists – translation can be queried with the typename function.

Table 4-4. List heads used by Scilab

Variable type Code string Code index
sparse matrix sp 5
sparse boolean matrix spb 6
Matlab® sparse matrix msp 7
linear-state system lss 16
rational function, i.e. quotient of two polynomials r 16
hyper-matrix hm 17
? ip 129

Now that we have defined all operand type codes, we can turn to the operator type codes.

Caution

Several operators listed in Table 4-5 behave specially! Some, like equality and inequality tests, are auto-overloaded, i.e. are available even before the user defines her replacement function. Others cannot be overloaded at all, like unary plus (all types), and insertion/extraction (tlists).

Warning

There is a mistake in the SCI/doc/Intro.ps concerning this operator. The first table in Sec. 3.3, page 64 states that code b defines the row-separator ";". SCI/doc/Intro.ps is wrong here, but the online help is correct in that the row-seperator ";" is overloaded with the code f.

Warning

The online-help of Scilab-2.5, help overloading, is incorrect, and SCI/doc/Intro.ps is correct. u is associated with "*.", and x with ".*".

Table 4-5. Operator type codes

Operator Code Note
.' 0 pure transposition (no complex conjugate)
< 1 less-than
> 2 greater-than
<= 3 less-or-equal
>= 4 greater-or-equal
~ 5 logical-not
+ a binary operator. The unary plus is automatically overloaded for any new type with the identity-transformation or "do nothing". Unary plus cannot be overloaded!
: b range generator
[ , ] c matrix row constructor ","
./ d element-wise division
( ) e extraction form a matrix, like s = v(k). The operator is automatically defined for new types. Extraction from tlists cannot be overloaded, use mlists instead.
[ ; ] f concatenation or matrix column construction ";"
| g logical-or
& h logical-and
( ) i insertion into a matrix, like v(k) = s. The operator is automatically defined for new types. Insertion into tlists cannot be overloaded, use mlists instead.
.^ j element-wise exponentiation
.*. k Kronecker multiplication
\ l left division; solve a linear system of equations
* m matrix multiplication
<>, ~= n unequality test. Both operators are automatically defined for any new type with list semantics, i.e. component-wise comparison and a boolean return vector. Both can be overloaded with user functions.
== o equality test. The operator is automatically defined for any new type with list semantics, i.e. component-wise comparison and a boolean return vector. It can be overloaded with a user function.
disp p unary operator; display results with disp or at the command line
^ p binary operator; matrix exponentiation
.\ q element-wise left division
/ r right matrix division
- s unary %head_s, and binary %head1_s_head2 operator; see also: overloading of unary plus %head_a.
' t unary operator, Hermitian (complex) transposition
*. u element-wise multiplication
/. v element-wise division
\. w element-wise right division
.* x element-wise multiplication
./. y Kronecker division
.\. z Kronecker right division

Almost all unary built-in functions like abs, ceil, floor, imag, int, real, round, sqrt, and string can be overloaded, too. The syntax borrows for the syntax for unary operators. Function names which are not already used by Scilab cannot be used for overloading.


    function result = %optype_functioname(argument)

where functioname is the name of the function.

4.2.3. Overloading example

Tip

Lots of overloading functions can be found in SCI/macros/percent.

After so much theory, definitions and tables we deserve an example that demonstrates operator overloading in action. As usual for sci-BOT the complete example can be found in Section 10.1. The following is not production strength code, most error checks are left out.


function f = frac(p, q, reduce)
// constructor for fractions

select type(p)
case 1 then // constant
    if size(p, '*') ~= 1 then
        error('argument p is non-scalar')
    end
    p0 = p
    q0 = 1
case 16 then // tlist
    // copy constructor behavior
    p0 = p('num')
    q0 = p('denom')
else
    error('argument p has wrong type')
end

if isdef('q') then // q is an optional argument
    select type(q)
    case 1 then // constant
        if size(q, '*') ~= 1 then
            error('argument q is non-scalar')
        end
        q0 = q0 * q
    case 16 then // tlist
        // copy constructor behavior
        p0 = p0 * q('denom')
        q0 = q0 * q('num')
    else
        error('argument q has wrong type')
    end
end

if isdef('reduce') then // (isdef('reduce') & reduce == %t) does not work, for
                        // Scilab performs a complete boolean evaluation
    if reduce == %t then
        [p_red, q_red] = reduce_int(p0, q0)
    else
        p_red = p0
        q_red = q0
    end
else
    [p_red, q_red] = reduce_int(p0, q0) -- reduce_int defined in complete example
end
f = tlist(['frac'; 'num'; 'denom'], p_red, q_red)


function s = %frac_p(f)
// display function for fractions
s = string(f)
disp(s)

//
// addition
//

function r = %frac_a_frac(f1, f2)
d1 = gcd_int(f1('denom'), f2('denom'))
if d1 == 1 then
    r = frac(f1('num')*f2('denom') + f1('denom')*f2('num'), ..
                 f1('denom')*f2('denom'))
else
    t = f1('num')*(f2('denom') / d1) + f2('num')*(f1('denom') / d1)
    d2 = gcd_int(t, d1)
    r = frac(t/d2, (f1('denom') / d1)*(f2('denom') / d2))
end

//
// conversion
//

function fl = frac2float(f)
// convert a fraction to a floating point number
fl = f('num') / f('denom')


function s = %frac_string(f)
// string( frac(...) )
if f('denom') == 1 then
    s = sprintf('%.0f', f('num'))
else
    s = sprintf('%.0f/%.0f', f('num'), f('denom'))
end

After loading these definitions a new type named frac exists. It can be used like this:


f = frac(2, 3);
g = frac(1, 3);
h = frac(-1, 3);
i = frac(12);

f + g
g + h
i

frac2float(h)

Notes

[1]

Thanks go to Bruno Pinçon for carefully proofreading this section.