-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: different dispatch instances on same function #186
base: master
Are you sure you want to change the base?
Conversation
Pull Request Test Coverage Report for Build 10133091731Details
💛 - Coveralls |
Hey @nstarman! Thanks for creating a PR. :) I'm not entirely sure about this change. For a function, from number import Number
@dispatch.abstract
def add(x: Number, y: Number):
"""Add two numbers.
Generic docstring.
"""
@dispatch
def add(x: int, y: int):
return x + y
# At this point, `add._f` is the function given in the abstract implementation. I'm therefore worried that this might result in unexpected behaviour where people expect the attribute In general, I think passing a function to Instead, I'm thinking that this instead we should give an error. What would you think about this? |
I'm not sure I see what you mean. This PR is about making it possible to do @dispatch2
@dispatch1
def f(x: int, y: float):
return x + y where
I did discover a fault. Adding in two dispatch1 = Dispatcher()
dispatch2 = Dispatcher()
@dispatch1.abstract
def f(x: Number, y: Number):
return x - 2 * y
@dispatch2.abstract
def f(x: Number, y: Number):
return x - y
@dispatch2
@dispatch1
def f(x: int, y: float):
return x + y
@dispatch1
def f(x: str):
return x
ns1 = SimpleNamespace(f=f)
@dispatch2
def f(x: int):
return x
ns2 = SimpleNamespace(f=f)
assert ns1.f("a") == "a"
assert ns1.f(1, 1.0) == 2.0
assert ns2.f(1) == 1
assert ns2.f(1, 1.0) == 2.0 Means that |
@nstarman What would you think of changing the pattern to something like this? @dispatch1.chain(dispatch2)
def f(x: int, y: float):
return x + y Then, internally, the |
Interesting! Or what about having a DispatcherBundle class such that it's @(dispatch1 | dispatch2)
def func(...): ... Where class AbstractDispatcher: ...
class Dispatcher(AbstractDispatcher):
def __or__(self, other: Dispatcher) -> DispatcherBundle: ...
class DispatcherBundle(AbstractDispatcher):
def __call__(...):
for dispatcher in self:
...
I think the syntax is more Pythonic. And then the bundle object is reusable and looks like the regular dispatcher. Moving most of the logic to a common base class would mean this is a smallish code addition! Alternatively the |
@nstarman Aha, I like that even better, since now the syntax is symmetric and indeed more Pythonic! Would be very happy to go for that. :) |
d24eee6
to
0793cf8
Compare
0793cf8
to
2993d12
Compare
Signed-off-by: nstarman <[email protected]>
2993d12
to
2583d45
Compare
Just need to add a guard for python 3.8 since @(...) syntax is not supported until py3.9+. |
If I can share a thought about I would caution against including such features into plum itself, and would rather refactor and open up the internals in a reasonable way that makes it easy to build such things on top organically and without fragmenting the ecosystem. One of the reasons is that multiple dispatch in python is already somewhat 'weird' and not native. Plum is trying to make it possible and reasonable, but there's already a long way to go to get there. While other languages offer great inspiration and carefully crafted ideas or frameworks, we lack a coherent view on how to get a 'reasonable MD that works and is intuitive' in Python. However, I do not think that adding many features to plum itself is the best way to get there. What is proposed in this PR is an exotic use case that is intricated. At the same time, I would like plum to foster experimentation of such 'crazy ideas', because maybe at some point we might land on the 'right approach' to MD in Python. To do that, I think that a reasonable part of plum's internal should be expandable and reusable such that you'd be able to build this feature in your own library, on top of plum. In short, I think this is a strong idea about how MD should work, going in a step that does not belong to MD (multiple dispatchers for the same functions).
|
@PhilipVinc, you make some solid arguments! @nstarman, what do you think? In this case, I think that the functionality could be fairly easily implemented with a third-party function resulting in something like this: @chain_dispatchers(dispatch1, dispatch2)
def f(…):
… @nstarman, would you also be happy with a solution like this, or do you think this functionality should belong in Plum? If the latter, possibly we could organise more experimental functions that are not to be used by the average user in e.g. |
I think @PhilipVinc raises good points! |
Happy to go with |
Experimental is ok for me. However I must say I disagree with this vision that 'MD' is implemented through 'objects'. In fact, a question I never asked @wesselb is: why is not a singleton? What was the use case you had in mind for multiple dispatchers? I fail to see in which case it would be useful to have multiple dispatcher objects, and in particular why you want to use it with the same functions? For my own curiosity, could you let me know what is your use case? |
@PhilipVinc. For me this is to support the multiple different versions of the same function that exist in NumPy and other array libraries during the Array API transition. For example |
@PhilipVinc Multiple dispatchers are required to emulate something like how scoping works in Julia. For example, in Julia, if package A defines a function f but does not export it, then you can define your own function f without the methods defined in A. Only once A exports f become these methods visible in your local scope. With a singleton dispatcher, all methods for function f would be visible to everyone and you could accidentally override methods or extend functions that weren’t supposed to be public. @nstarman If |
No description provided.