Open giladbarnea opened 8 months ago
Hey @giladbarnea! Thanks for opening an issue. :)
Unfortunately type variables are currently not supported. :( It would be awesome to support them, but this is not a trivial undertaking.
If you ignore the warnings, your code will run, but it will not behave as expected:
from typing import TypeVar
from plum import dispatch
T = TypeVar("T")
@dispatch
def f(x: T) -> T:
return 1
>>> f(2) # OK so far...
1
>>> f("2") # Nope! T_T This should error.
1
Thanks for replying!
This is a bit of a bummer, if possible can you tell me a few reasons why supporting TypeVars is difficult? Hopefully I'll find some time and maybe try to fix it.
@giladbarnea in principle supporting TypeVar
s should be possible, and I would really like to support them in the future. To do this, I think some kind of solver would have to be implemented that matches the type variables to concrete types. I've not thought super carefully about it, but this seems like a complex undertaking, since type variables can occur in all sorts of complicated nested types, e.g. Callable[dict[str, Tin], Treturn] -> Treturn
.
Note to the unwise and the unwary: @beartype and thus Plum should superficially support a TypeVar
with either:
TypeVar('FooClass', bound=Foo)
).TypeVar('FooClass', Foo)
).In either case, @beartype and thus Plum should reduce that TypeVar
to that bounds and those constraints. This may require @beartype 0.16.0, which I'm on the precipice of releasing this Friday. Dare I do it? I dare.
As @wesselb suggests, full-blown type variable support at runtime is non-trivial in the extreme and not something anyone wants to voluntarily tackle. You'll either need full-time grant funding for that and/or Asperger's. I have the latter but not the former. Therefore, I'll eventually tackle this – but only after having exhausted every other outstanding feature request and issue in @beartype. It is the highest of the high-hanging fruit. It cannot be plucked without back pain, kidney pain, a blown ACL, and a ruptured Achilles heel. Don't go down that road, @giladbarnea.
@leycec Are warnings expected with bound variables? They seem to work, but just want to make sure the warnings are expected and not from misuse.
from plum import dispatch
from typing import TypeVar
T = TypeVar("T", bound=str)
@dispatch
def f(x: T) -> T:
return x
f('foo') # works with warnings
f(2) # errors, no warning
@ilan-gold The warnings here can be ignored. Your code will run, but may not behave as expected.
Consider the following (very contrived) example:
from plum import dispatch
from typing import TypeVar
class Str2(str):
pass
T = TypeVar("T", bound=str)
@dispatch
def f(x: T) -> T:
return Str2(x)
f("hey") # Works, but shouldn't, because the input is `str` and the output a `Str2`!
f("hey") # Works, but shouldn't, because the input is
str
and the output aStr2
!
Doesn't that work just because Str2
is a subclass of str
?
Right, I see what you're saying. I believe that the way T
is supposed to work is that it binds to exact types.
Consider instead the following example:
from plum import dispatch
from typing import TypeVar
class Str2(str):
pass
T = TypeVar("T", str, int)
@dispatch
def f(x: T) -> T:
return 1
f("1") # Works, but shouldn't, because the input is `str` and the output a `int`!
Wait, this time, aren't the constraints functioning as a union of types now, though?
Wait, this time, aren't the constraints functioning as a union of types now, though?
That's what's currently happening, yes, but it's not the correct behaviour.
The above code should be equal to
@dispatch
def f(x: str) -> str:
...
@dispatch
def f(x: int) -> int:
...
It would be great to properly support type parameters, but unfortunately that's not an easy feat.
Why not get the type of the argument passed to the function, find the index of the type in the constraint, then check the index of the return type in the return constraint? With special consideration of exact types. Like:
def check(func, arg, argvar, _return, returnvar):
argtype = type(arg)
returntype = type(_return)
for i, t in enumerate(returnvar.__constraints__):
if argtype is t and argvar.__contraints__[i] is t:
return True
for i, t in enumerate(returnvar.__constraints__):
if issubclass(argtype, t) and issubclass (argvar.__contraints__[i], t):
return True
return False
Of course, this is just a short mock-up!
@sylvorg, you're totally right that this would be a nice attempt at supporting type parameters. Currently, we don't do any of this.
Should you want to have a go at trying to properly support type parameters, then that would be super exciting. I think the basic cases can be covered in a fairly straightforward way. Getting all the edge cases will likely be tedious and very difficult.
I'll try my best, but it'll take me a while to understand the code base; which files would you recommend I look at, other than types.py
?
To be honest, this might be a pretty major undertaking that touches a large part of the codebase. It's currently not clear to me what the best way of going about it would be.
I'm thinking that we could add a "type parameter resolution stage" in resolver.py
, where Method
s with type parameters are converted into Method
s without type parameters. Not entirely sure.
Is there any function in the codebase you can think of that gets the argument type, the argument annotation, the return type, and the return annotation? Or a series of functions? I could start working from there. Or would they all be in resolver.py
?
Function
in function.py
collects all Method
s (method.py
). Here a method is defined as an implementation of a function for a type signature.
The actual types of the arguments are only considered at the very last stage of dispatch, in resolver.py
. Resolver
chooses which Method
is appropriate for the given arguments. I think that's the point where type parameters could be handled.
Thinking about it, I think all that might be required is a matching algorithm that attempts to determine the values of the type parameters (the first hit suffices) and which then replaces that type parameter by the matched value.
Thinking about it, I think all that might be required is a matching algorithm that attempts to determine the values of the type parameters (the first hit suffices) and which then replaces that type parameter by the matched value.
Sorry, could you expand on this a little bit? What do you mean by "replaces that type parameter by the matched value"?
@sylvorg, basically, if the signature is (list[T], int, T) -> T
and the arguments are ([1], 5, "test")
, then, by matching list[T]
to [1]
, it is clear that T = int
. Therefore, we can substitute int
for T
, giving the "concrete" signature (list[int], int, int) -> int
, and we can use the existing machinery on that concrete signature.
Oof; this is getting a little too confusing for me... 😅 Would we not also have to match the type of the return value as well as the arguments provided to the function?
@sylvorg, yes, that's completely right, and it's one of the main challenges why this is so difficult. :( In general, signatures can depend on T
in arbitrarily complex ways, and you need a generic mechanism that can infer the value of T
for arbitrary arguments.
We could decide to take it step by step and only support type parameters in a limited manner. If we code things up in a robust and sound way and give appropriate warnings to the user, I would be fully on board with that.
Hi, thanks for the awesome library! Super useful.
I've been getting this warning:
Reproduce: