Yuval asked me off-list if I would comment on the various dispatch algorithms
that have been proposed.

I guess I have used MMD more than most people in this discussion. Indeed,
having both written Class::Multimethods and supervised a PhD that involved
adding MMD to C++, one might have assumed that I've already "served my
sentence" ;-).

Nevertheless, all that experience has convinced me that the simpler the
dispatch rules, the more usable the resulting MMD mechanism is. That's why
I've consistently advocated uniform Manhattan distance over
"left-most-best-fit". That's why I've always recommended an explicit C<is
default> marker. That's why I was opposed to any particular numerical epsilon
value. That's why I don't favour special treatment for junctions or multiple
inheritance trees.

The goal is always the same: to find a parameter list that most accurately
matches the argument list, taking into account the type generalizations
introduced by inheritance and the type specializations introduced by C<where>
clauses.

So, in my view the MMD mechanism ought to be something like:

    1. Gather all visible variants with a compatible number of
       parameters (taking into account the requirements of any C<where>
       constraints)

    2. If there are no such variants, throw a "no such multi" exception

    3. Work out the Manhattan distance from the argument list to each
       variant's parameter list.

    4. If there is a unique minimum, call that variant

    5. Otherwise, discard every variant whose Manhattan distance
       isn't minimal

    5. Work out the degree of specialization of each remaining argument
       list (i.e. the total number of C<where> specializations on the
       variant's complete set of parameters)

    6. If there is a unique maximum, call that variant

    7. Otherwise, if there is a compatible variant with an <is default>
       trait, call that variant

    8. Otherwise, throw an "ambiguous call" exception.

This is a much less dwimmy solution than Yuval's or Luke's, but it has the
advantage that those eight steps reduce to eight words:

    Unique least-inherited most-specialized match, or default

which will fit into most people's heads and still DWIM most of the time.

Note that specializations enter into the decision process at two points:
initially, they must be satisfied if the variant is to be considered "viable";
later, they are used as tie-breakers when resolving ambiguities.

Using them to select the initial set of viable candidates is critical. If I 
have:

        multi sub foo (Int $x where { $^x < 10 }) {...}
        multi sub foo (Num $x)                    {...}

then I almost certainly want a call to:

        foo(42);

to successfully call the second variant, rather than throwing an exception like:

        Can't call multi sub foo (Int $x where { $^x < 10 }) when $x is 42.


Keeping C<where> clauses as the sole "more specialized" marker also gives the
developer *more* control than adding extra rules for junctions would. For
example, someone might prefer to treat all junctions as being equally special:

        multi sub Foo(Int&Str) {...}
        multi sub foo(Int)     {...}

or treat junctions as more special:

        multi sub Foo((Int|Str) where Any) {...}
        multi sub foo(Int)                 {...}

or treat junctions as less special:

        multi sub Foo(Int|Str)       {...}
        multi sub foo(Int where Any) {...}

In each case, these variations in "significance" are now explicitly and
consistently marked.

Damian

Reply via email to