coady / multimethod

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

`dict[K, V]` does not accept empty dict `{}` #21

Closed hzhangxyz closed 3 years ago

hzhangxyz commented 3 years ago

code:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from multimethod import multimethod

@multimethod
def f(a: dict[int, int]):
    pass

f({1:2})
f({})

run and get output

Traceback (most recent call last):
  File "/mnt/d/Home/Downloads/./main.py", line 11, in <module>
    f({})
  File "/mnt/d/Home/Downloads/multimethod/multimethod/__init__.py", line 188, in __call__
    return self[tuple(map(self.get_type, args))](*args, **kwargs)
  File "/mnt/d/Home/Downloads/multimethod/multimethod/__init__.py", line 184, in __missing__
    raise DispatchError(msg, types, keys)
multimethod.DispatchError: ('f: 0 methods found', (<class 'dict'>,), [])

similarly, I cannot pass empty list [] into list[int]

hzhangxyz commented 3 years ago

It seems it is not a very good choice to implement to accepting empty dict as dict[K, V] if there are two kind dict: dict[K1, V1] and dict[K2, V2]

There are following three condition:

  1. don't allow empty dict as argument
  2. report error when ambiguous, allow it when only one dict[K, V] is implemented
  3. choice any one of function if more than one function can access empty dict

currently, multimethod use condition 1 and it seems condition 2 is not easy to implement?(maybe) but it is the behavior how c++ does I don't know whether condition 3 is a good choice.

coady commented 3 years ago

Good summary, and there's another problematic case. If empty collections are supported, an empty list called on a method with List[float] and List[int] would organically raise an ambiguity error.

But what if it's List[bool] and List[int]? The existing behavior would default to List[bool] because issubclass(bool, int). It would require a special hack in the dispatch algorithm itself to consider that ambiguous, because that function signature is a total ordering, and therefore ambiguity should be impossible.

So there's a conundrum where the only sensible behavior is likely to be surprising to the average user.

coady commented 3 years ago

There's a prototype in branch empty to experiment with, but I'm not sure about this. It could just be a limitation of a dynamically typed language.

cunningjames commented 3 years ago

I suppose you could just fall back on a catchall:

@multimethod
def foo(xs: List[int]): ...

@multimethod
def foo(xs: List[bool]): ...

@multimethod
def foo(xs: List):
    # deal with the empty case explicitly
    if not len(xs):
        return "unknown"
    raise TypeError(...)

It'd be cool if Literal[[]] worked, but the following throws an error:

@multimethod
def foo(xs: Literal[[]]):
    return "unknown"

# ~/.pyenv/versions/3.8.5/envs/hacking/lib/python3.8/site-packages/multimethod/__init__.py in __new__(cls, tp, *args)
#      45         origin = getattr(tp, '__extra__', getattr(tp, '__origin__', tp))
#      46         args = tuple(map(cls, getattr(tp, '__args__', None) or args))
# ---> 47         if set(args) <= {object} and not (origin is tuple and args):
#      48             return origin
#      49         bases = (origin,) if type(origin) is type else ()
# 
# TypeError: unhashable type: 'list'
coady commented 3 years ago

The problem is with Literal[...] is that the dispatch is based solely on types. overload would have to be used for that.