Sub::Signatures stumper

Ovid on 2004-12-12T23:40:28

I wound up with a serious problem using multi-method dispatch and subroutine signatures. What happens when I hit the following?

use Sub::Signatures 'strict';

sub foo(SomeClass $bar) {
    $bar->bang;
}

sub foo(OtherClass $bar) {
    $bar->bang;
}

If SomeClass and OtherClass are related to one another via inheritance, I have no way of guaranteeing which is related to which at compile time and this complicates dispatching. In Java, this is not a problem because class relationships are set at compile time and cannot change. Thus, this works:

public String foo(Klonk some_var) {
    some_var.wibble();
}

public String foo(SubclassOfKlonk some_var) {
    some_var.wibble();
}

If I call object.foo(anotherObject);, the method dispatch is unambiguous. A bit of research reveals that Class::Multimethods is doing quite a bit of horrifyingly slow work to resolve this issue at runtime and frankly, I may have no choice but to follow along. I've considered, however, a "mixed" mode where one can do this:

use Sub::Signatures 'mixed';

sub foo($bar, $baz, ARRAY $thing) {
    ...
}

sub foo($bar, $baz, Some::Class $thing) {
    ...
}

In this variant, types are ignored unless specified. Then they're required to be an exact match for the variable at hand. Inheritance will not matter. This allows Ruby style "duck typing" for most variables, dispatching on the number of arguments and optional type checking when needed. It also allows this to be conceptually confusing. I'll have to think about this more, but this is discouraging. I have several hours of work today that are now sitting idly on my hard drive.


Re:

Aristotle on 2004-12-13T12:12:54

Time for a probably naïve question… can't you simply do something like the following?

sub decide_type {
    my( $thing, @possible_type ) = @_;

    my %specifity = map {
        $_ => scalar grep isa( $foo, $_ ), @possible_type
    } @possible_type;

    return List::Util::reduce {
        $specifity{ $a } >  $specifity{ $b } ? $a : $b
    } keys %specifity;
}

decide_type $foo, qw( Klonk SubclassOfKlonk );

I'm not sure this'd fall under “horrifyingly slow”?

In any case you'll still have trouble on your hands with multiple inheritance…

  A   B
   \ /
    C

What happens if I declare two functions whose signatures only differ in that one of the parameters must be an instance of class A for one of them and B for the other — and then pass in an instance of C?

Re:

Ovid on 2004-12-13T16:09:49

That's absolutely a naïve question. Of course, I asked the same question. We're both naïve. I assume the isa() function you have returns how many steps away the type is?

Your solution looks reasonable. I think it can work. Now I have to think about the caching of signatures types and seeing if I can implement that. I don't have much free time during the week, so perhaps next weekend.

What happens if I declare two functions whose signatures only differ in that one of the parameters must be an instance of class A for one of them and B for the other — and then pass in an instance of C?

Class::MultiMethods will croak() if there is an ambiguous dispatch and that's probably the route I will take. Reading through resolving ambiguities in the CMM docs sheds a lot of light on this problem. Somehow, it's not surprising that a former comp-sci professor has spent more time on this problem than I have.

Actually, I had thought about reimplementing my code by writing out CMM instead of my personal parsing routine, but that would require parsing Perl to find the trailing paren (PPI might be able to handle this, but the docs aren't ready ... yet.) Instead, I'm going to keep heading in the direction that I am currently going and then work like mad to profile the code after it's stable. So far it seem reasonably fast, but I do need to benchmark it against comparable code.

Re:

Aristotle on 2004-12-13T17:30:27

The isa() function there is just UNIVERSAL::isa(). That's why I'm wondering whether this approach would be too slow — were it some custom @ISA walking routine, there'd be no question in the first place. That was the entire point to the code I wrote — trying to find the answer to the question “which thing out of a hierarchy of possibilities do I have?” without walking the inheritance graph manually. I'm pretty sure that invoking UNIVERSAL::isa() ten times, and subsequently implicitly having the Perl core traverse the tree ten times, is still much faster than traversing it a single time explicitly. Of course, you could always climb down to the metal and write an @ISA walker in XS…

I like the idea of going the C::MM route instead of doing your own thing. Although that's probably not the route I'd take in the short term either. Once you do go there, it might in fact be worth thinking about redoing the slow parts of C::MM in an optional XS package.

I don't really know…

Re:

Aristotle on 2004-12-13T17:42:13

Actually this can be possibly be optimized with a more explicit loop. Once you know that SubclassOfKlonk has a higher specifity than Klonk, you can discard it.

Obviously that requires more code. Deciding whether the optimization is applicable might be slower than just going about it the brute's way.