coady / multimethod

Multiple argument dispatching.
https://coady.github.io/multimethod
Other
277 stars 24 forks source link

Provide optional default/catch all method #54

Closed adam-urbanczyk closed 2 years ago

adam-urbanczyk commented 2 years ago

Would it be in scope of multimethod to fallback to one of the methods (possibly explicitly marked) in case of a dispatch error?

It would help to maintain backward compatibility for our codebase for people specifying positional arguments with keywords.

coady commented 2 years ago

That's supported. Registering a function with no annotations - or using explicit .register() with no args - will inherently match all calls.

adam-urbanczyk commented 2 years ago

Great, thanks! Could it work with annotations and methods? Or is it needed to add a dummy method then to handle the default case? I'm essentially trying to get something like this:

class A:
    @multimethod(default=True)
    def f(self, a: int):
        print(a)
coady commented 2 years ago

Could it work with annotations and methods? Or is it needed to add a dummy method then to handle the default case?

That's what I'd recommend, just because I don't understand the use case. Is a supposed to be an int or not?

But if you want, explicitly registering the types will ignore annotations.

    @f.register()  # no types given
    def _(self, a: int):
        print(a)
adam-urbanczyk commented 2 years ago

So the use case is as follows: a large codebase starts to use multimethods, but there is a small group of users that relied on positional arguments used as keyword arguments., so it would be great if the original method (which btw already had type annotations) could be also marked as a default one so that any call relying on keyword args would end up being dispatched to that method.

In the example above I'd like that both f(1) and f(a=1) worked without an additional _ method.

coady commented 2 years ago

In the example above I'd like that both f(1) and f(a=1) worked without an additional _ method.

Ok, but in that case f(1.0) would also match. There's another option: multidispatch now supports dispatching on keyword arguments.

class A:
    @multidispatch
    def f(self, a: int):
        print(a)

A().f(0)
A().f(a=0)
adam-urbanczyk commented 2 years ago

I somehow assumed that it is not meant for methods. Thanks, looks like the solution!

adam-urbanczyk commented 2 years ago

I might have jumped to conclusions too quickly. multidispatch does not seem to like changing signatures. Here is a more complete use case I'm after:

from multimethod import multimethod, multidispatch

class A:
    @multimethod
    def f(self, a: int, c: str='s'):
        print(1)

    @multimethod
    def f(self, a: int, b: int, c: str='b'):
        print(2)

    @multimethod
    def f(self, *args, **kwargs):
        _f = next(iter(self.f.values()))
        return _f(self, *args, **kwargs)

A().f(0,'s')
A().f(0)
A().f(0,c='s')

A().f(0,1,c='s')
A().f(0,1,'s')
A().f(0,1)

A().f(a=0,c='s') #prints 1

In essence, I want to get rid of the explicit definition of the third method, but still want to keep the behavior. Do I need to subclass multimethod or is such a case already supported?

coady commented 2 years ago

multidispatch does not seem to like changing signatures.

Yes, it uses the base implementation for performance. I don't see a way that it could handle different signatures without doing a linear scan through all the functions, which is what overload does.

Speaking of which, this example would be called "overloading" in other languages, because the signatures are varying, not the types. So one question is should both functions have the same name, or conversely should there be one function where b is optional?

I think extending overload to allow types as well as predicates is reasonable. Then this would work:

    @overload
    def f(self, a: int, c: str='s'):
        print(1)

    @overload
    def f(self, a: int, b: int, c: str='b'):
        print(2)
adam-urbanczyk commented 2 years ago

Thanks, sounds like a good solution!