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:
overload,
then the next procedure in the list is tried.
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:
The commandiquo(4,3,2); Error, invalid input: iquo expects its 3rd argument, r, to be of type name, but received 2
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.
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.
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.
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
<-- ERROR in ugly (now at top level) = ugly,
"invalid input: %1 uses a %-2 argument, %3 (of type %4), which is missing",
ugly, 2, n, posint}
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:
Simpler is to reference each parameter before doing any computation. In this case, insertif _npassed <> _nparams then error "invalid input:" end if;
Better still would be to not use the overload mechanism for this sort of thing.L;n;
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.
Comments
?
hmm. a bit confusing
overload
Joe, thank you for the detailed analysis. For me, the conclusion is clear: do not use
overload. The semantics will eventually lead to hard-to-track bugs. (Maybe someday Maple could get a replacement, sayOverload, that worked with cleaner semantics.)