The Maple overload command provides a useful mechanism for splitting the implementation of a procedure that operates on different types of arguments into separate procedures. This article describes the mechanism that it uses to select the procedures, illustrates subtle issues, and shows how they can be resolved.

The single argument to overload is a list of procedures (there is also a two argument form that appends procedures to an overloaded procedure; that is not discussed here). It returns a procedure that, when called, attempts to match the arguments to each of the procedures in the original list. Here is the basic operation:

  • If the arguments match a procedure's parameters, the procedure is executed and its result returned.
  • If the arguments do not match, and the procedure has option overload, then the next procedure in the list is tried.
  • If the arguments do not match and is either the last in the list or does not have option overload, then an error is raised.

Let's see how this works with a simple example.

Assign a list of two procedures, the first operates on integers, the second on floats:

div1 := overload([ proc( a::integer, b::integer) option overload; iquo(a,b) end proc,
                   proc( a, b ) option overload; evalf(trunc(a/b)) end proc
                 ]):
div1(5,3);
             1
div1(5,3.0);
             1.0
div1(a,b);
             trunc(a,b)
div1(3);
Error, invalid input: no implementation of div matches the arguments in call, div(3)

These do what we expect. Now let's try something subtle. Instead of using the parameters a and b in the call to iquo in the first procedure, use the keyword _passed, which evaluates to the argument sequence (it is equivalent to using args, an older convention).

div2 := overload([ proc( a::integer, b::integer) option overload; iquo(_passed) end proc,
                   proc( a, b) option overload; evalf(trunc(a/b)) end proc
                 ]):
When called with two arguments, div2 works the same as div1. Let's compare the two when called with three arguments:
(div1,div2)(4,3,2);
                                     1, 1.
The first is an integer, the second a float. That indicates that the call to div1 returns the output of the first procedure in its list, while the call to div2 returns the output of the second. To see what accounts for the difference, call iquo directly with the same three arguments:
iquo(4,3,2);
Error, invalid input: iquo expects its 3rd argument, r, to be of type name, but received 2
The command iquo accepts an optional third argument, however, it must be an unassigned name. Because we are not interested in providing a three-argument version of div, this error could be avoided either by using the explicit parameters, as in div1, or by adding end-of-parameters markers, $, in the procedure definitions:
div3 := overload([ proc( a::integer, b::integer, $) option overload; iquo(_passed) end proc,
                   proc( a, b, $) option overload; evalf(trunc(a/b)) end proc
                ]):
div3(4,3,2);
Error, invalid input: no implementation of div3 matches the arguments in call,
div3(4,3,2)

The unexpected operation of div2(4,3,2) illustrates an important point:

An error raised during the evaluation of an overloaded procedure may invoke the overload selection mechanism and pass evaluation to the next available procedure.
This frequently is not desired and can be difficult to detect if the subsequent procedure successfully evaluates. The next sections explain why this happens and show how to prevent it. The following Maple procedure, fakeoverload, mimics the operation of overload. It takes a single argument, a list of procedures, and returns a procedure that uses the same selection method as employed by overload for applying its arguments to one of the procedures in the list.
fakeverload := proc( procs :: list(procedure) )
    proc()
    local p,cnt;
        cnt := 0;
        for p in procs do
            cnt := cnt+1;
            try
                userinfo(3, procname, nprintf("trying procedure %d", cnt));
                return eval(p)(_passed);
            catch "invalid input:":
                if not member('overload', [attributes(eval(p))]) then
                    error
                end if;
            end try;
        end do;
        error ( "invalid input: no implementation of %1 matches the arguments in call, %1(%2)"
                , procname, _passed );
    end proc;
end proc:

Use it to create a new version of div2 and call it with three arguments.

div2b := fakeoverload([ proc( a::integer, b::integer) option overload; iquo(_passed) end proc,
                        proc( a, b) option overload; evalf(trunc(a/b)) end proc
                      ]):
infolevel[div2b] := 3:
div2b(4,3,2);
div2b:   trying procedure 1
div2b:   trying procedure 2
                                      1.

An inspection of fakeoverload reveals how the selection proceeds. The significant fact is that a try statement is used with the catch string "invalid input:". An exception raised during the evaluation of one of the list procedures that matches this catch string invokes the selection mechanism. This is precisely the mechanism that the real overload procedure uses; see the final sentence of the third bullet of the help page for overload:

A call that generates an exception beginning with "invalid input:" is considered a type mismatch.
Now that we better understand the selection mechanism used by overload, we can see how to avoid accidentally invoking it. Here is a modification of div2:
div2c := overload([ proc( a::integer, b::integer)
                    option overload;
                        try
                            iquo(_passed);
                        catch "invalid input:":
                            error "failure", StringTools:-FormatMessage(lastexception[2..-1]);
                        end try;
                    end proc,
                    proc( a, b) option overload; evalf(trunc(a/b)) end proc
                  ]):
div2c(4,3,2);
Error, failure, invalid input: iquo expects its 3rd argument, r, to be of type
name, but received 2

The first procedure uses a try statement to catch any exception that would trigger the overload selection mechanism, then reraises it, but with the string "failure" prepended so that it does not match the catch-string in overload. To make procedure selection efficient, it is important that all arguments be tested at the start of each procedure. This is particularly true when later procedures can operate without all the arguments of the first (though one should question whether the overload mechanism is the appropriate choice for that case). Here is an egregious example.

ugly := overload([ proc( L::list, n::posint )
                   local k,tmp;
                   option overload, trace;
                       tmp := add(L[k]^2, k=1..nops(L));
                       tmp^n;
                   end proc,

                   proc( L::list )
                   local k,tmp;
                       add(L[k]^2, k=1..nops(L));
                   end proc
                 ]):

ugly([$1..10]);
{--> enter ugly, args = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
                                  tmp := 385

It works, but the summation has to be performed twice. This could have been avoided by ensuring that the number of arguments matches the number of parameters. One way is to an explicit test at the start of the procedure:
if _npassed <> _nparams then error "invalid input:" end if;
Simpler is to reference each parameter before doing any computation. In this case, insert
L;n;
Better still would be to not use the overload mechanism for this sort of thing.

Testing all required parameters for existence before initiating some long routine is good practice regardless whether overload is used; there's nothing more annoying than waiting for a computation to complete then getting an error indicating the procedure call was malformed.


Please Wait...