-
Notifications
You must be signed in to change notification settings - Fork 235
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
Higher-Kinded TypeVars #548
Comments
I think this came up few times in other discussions, for example one use case is python/mypy#4395. But TBH this is low priority, since such use cases are quite rare. |
damn, I searched very thoroughly but did not find this one! 😄 |
Adding your +1 is all fine, but who's going to do the (probably pretty
complicated) implementation work?
|
are you approving the feature? |
I am neither approving nor disapproving. Just observing that it may be a
lot of work for marginal benefits in most codebases.
|
awfully pragmatic. Where's your sense of adventure? 😄 |
Similar to microsoft/TypeScript#1213 Not sure if the discussion over there provides any useful insights to the effort over here. |
Hi @tek. I'm also very interested in this, so I'd like to ask if you had any progress with this and volunteer to help if you want. |
@rcalsaverini sorry, I've been migrating my legacy code to haskell and am abandoning python altogether. but I wish you great success! |
Oh, sad to hear but I see your point. Thanks. |
Just to add another use case (which I think relates to this issue): Using Literal types along with overloading It is one step closer to being possible due to the most recent mypy release's support for honoring the return type of Note: this is basically a stripped-down version of Django's Field class: # in stub file
from typing import Generic, Optional, TypeVar, Union, overload, Type
from typing_extensions import Literal
_T = TypeVar("_T", bound="Field")
_GT = TypeVar("_GT")
class Field(Generic[_GT]):
# on the line after the overload: error: Type variable "_T" used with arguments
@overload
def __new__(cls: Type[_T], null: Literal[False] = False, *args, **kwargs) -> _T[_GT]: ...
@overload
def __new__(cls: Type[_T], null: Literal[True], *args, **kwargs) -> _T[Optional[_GT]]: ...
def __get__(self, instance, owner) -> _GT: ...
class CharField(Field[str]): ...
class IntegerField(Field[int]): ...
# etc...
# in code
class User:
f1 = CharField(null=False)
f2 = CharField(null=True)
reveal_type(User().f1) # Expected: str
reveal_type(User().f2) # Expected: Union[str, None] |
I wonder if this is what I need or if there's currently a work around for my (slightly simpler) case?: I'm building an async redis client with proper type hints. I have a "Commands" class with methods for all redis commands ( This is easy enough to implement in python, but not so easy to type hint correctly. Basic example: class Redis:
def execute(self, command) -> Coroutine[Any, Any, Union[None, str, int, float]]:
return self.connection.execute(...)
def get(self, *args) -> Coroutine[Any, Any, str]:
...
return self.execute(command)
def set(self, *args) -> Coroutine[Any, Any, None]:
...
return self.execute(command)
def exists(self, *args) -> Coroutine[Any, Any, bool]:
...
return self.execute(command)
# ... and many MANY more ...
class RedisPipeline(Redis):
def execute(self, command) -> None:
self.pipeline.append(command) I tried numerous options to make Is there any way around this with python 3.8 and latest mypy? If not a solution would be wonderful - as far as I can think, my only other route for proper types is a script which copy and pastes the entire class and changes the return types in code. |
@samuelcolvin I don't think this question belongs in this issue. The reason for the failure (knowing nothing about Redis but going purely by the code you posted) is that in order to make this work, the base class needs to switch to an
|
I get that, but I need all the public methods to definitely return a coroutine. Otherwise, if it returned an optional coroutine, it would be extremely annoying to use. What I'm trying to do is modify the return type of many methods on the sub-classes, including "higher kind" types which are parameterised. Hence thinking it related to this issue. |
Honestly I have no idea what higher-kinded type vars are -- my eyes glaze over when I hear that kind of talk. :-) I have one more suggestion, then you're on your own. Use a common base class that has an |
Okay, so the simple answer is that what I'm trying to do isn't possible with python types right now. Thanks for helping - at least I can stop my search. |
I suspect that the reason is that it simply isn't type-safe, and you
couldn't do it (using subclassing) in any other typed language either.
|
humm, but the example above under "Basic example" I would argue IS type-safe. All the methods which end Thus I don't see how this as any more "unsafe" than normal use of generics. |
@gvanrossum, I can relate! I wonder if bidict provides a practical example of how this issue prevents expressing a type that you can actually imagine yourself needing. >>> element_by_atomicnum = bidict({0: "hydrogen", 1: "helium"})
>>> reveal_type(element_by_atomicnum) # bidict[int, str]
# So far so good, but now consider the inverse:
>>> element_by_atomicnum.inverse
bidict({"hydrogen": 0, "helium": 1}) What we want is for mypy to know this: >>> reveal_type(element_by_atomicnum.inverse) # bidict[str, int] merely from a type hint that we could add to a super class. It would parameterize not just the key type and the value type, but also the self type. In other words, something like: KT = TypeVar('KT')
VT = TypeVar('VT')
class BidirectionalMapping(Mapping[KT, VT]):
...
def inverse(self) -> $SELF_TYPE[VT, KT]:
... where |
Okay, I think that example is helpful. I recreated it somewhat simpler (skipping the inheritance from from abc import abstractmethod
from typing import *
T = TypeVar('T')
KT = TypeVar('KT')
VT = TypeVar('VT')
class BidirectionalMapping(Generic[KT, VT]):
@abstractmethod
def inverse(self) -> BidirectionalMapping[VT, KT]:
...
class bidict(BidirectionalMapping[KT, VT]):
def __init__(self, key: KT, val: VT):
self.key = key
self.val = val
def inverse(self) -> bidict[VT, KT]:
return bidict(self.val, self.key)
b = bidict(3, "abc")
reveal_type(b) # bidict[int, str]
reveal_type(b.inverse()) # bidict[str, int] This passes but IIUC you want the ABC to have a more powerful type. I guess here we might want to write it as def inverse(self: T) -> T[VT, KT]: # E: Type variable "T" used with arguments Have I got that? |
Exactly! It should be possible to e.g. subclass bidict (without overriding inverse), and have mypy realize that calling inverse on the subclass gives an instance of the subclass (with the key and value types swapped as well). This isn’t only hypothetically useful, it’d really be useful in practice for the various subclasses in the bidict library where this actually happens (frozenbidict, OrderedBidict, etc.). Glad this example was helpful! Please let me know if there’s anything further I can do to help here, and (can’t help myself) thanks for creating Python, it’s such a joy to use. |
Ah, so the And now I finally get the connection with the comment that started this issue. But I still don't get the connection with @samuelcolvin's RedisPipeline class. :-( |
I would also say that this example is really simple, common, but not supported: def create(klass: Type[T], value: K) -> T[K]:
return klass(value) We use quite a lot of similar constructs in As a workaround I am trying to build a plugin with emulated HKT, just like in some other languages where support of it is limited. Like:
Paper on "Lightweight higher-kinded polymorphism": https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-kinded-polymorphism.pdf TLDR: So, instead of writing |
@omer54463 Perhaps I wasn't clear about this, but I meant to show that the currently best "solution" to HKT will probably require significant changes to your codebase, requires you to use mypy and a third-party mypy-plugin, and results in verbose and difficult to read code. The syntax you mentioned earlier uses I guess that the desireable HKT syntax would look like either from returns.interfaces.container import Container1
# explicit notation
def all_to_str[T[V]: Container1[V]](arg: T[int]) -> T[str]:
...
# implicit notation
def all_to_str[T: Container1](arg: T[int]) -> T[str]:
... or some combination of the two. |
@jorenham I roughly agree about the syntax. Your intentions seem to be to simulate something like this: T = TypeVar("T")
V = TypeVar("V")
def f(value: T[V]):
... This makes no sense to me. However, look at this. T = TypeVar("T")
class MyProtocol(Protocol[T]):
...
P = TypeVar("P", bound=MyProtocol)
def f(value: P[T]):
... It's trivial that P gets a generic parameter. It's not because we dictate that when using P, but because the bound type has to be generic. Is P technically an HKT? I'm pretty sure it is. But it's a much simpler case. |
@omer54463 I don't know what you mean with the first example. It makes me suspect that you don't understand PEP 695, so let me "backport" my previous example into the pre- PEP 695 era: from typing import TypeVar
from returns.interfaces.container import Container1
V = TypeVar("V", covariant=True)
# implicit notation
T = TypeVar("T", bound=Container1)
# explicit notation
T = TypeVar("T", bound=Container1[V], params=[V])
def all_to_str(arg: T[int]) -> T[str]: ... The explicit In your last example, |
A slightly more involved, realistic example of what HKT could look like in Python: from collections.abc import Awaitable, Callable
type Maps1[V, R] = Callable[[V], R]
def map_async[V, R, T: Awaitable](fn: Maps1[V, R], aval: T[V]) -> T[R]: ...
# for example:
# map_async(str, asyncio.Task[int]) -> asyncio.Task[str] This example made me realise that the "explicit notation" from my earlier post isn't going to work.
Both cases are self-contractictory, making my earlier proposal for an "explicit notation" a bad idea. This leaves the "implicit notation", i.e. which I used in this post's example (i.e. |
We can support an explicit syntax similar to the ones you mentioned without contradictions.
The signature of the function you gave would then be either:
Though I am not sure "explicit" is the right term here. |
@omer54463 Your 2nd and 3rd signature specifications with 4 type parameters are invalid.
So a type checker has no way of inferring the type of |
The important points about the status of this feature and how to contribute have scrolled out of GitHub's very limited window of comments visible on page load by now, which makes them hard for newcomers to this issue to find. So here is a summary again:
Maybe @tek or someone else who is allowed to edit it can put this in the original issue so new arrivals find it quickly? |
Good point, I'll put this summary in the top post. |
I wanted to post this on the linked PEP draft, but I'm getting a 404. Did it maybe move? (cc: @nekitdev) At any rate, I'm pretty sure I have another use case. I'm currently writing an asset management system that supports the idea of presentation variants, and each presentation variant has a metadata dict. However, the exact format of the metadata dict is determined by the kind of one of the arguments to its constructor. More specifically, I've implemented multiple asset builder classes, which all inherit from a base class StaticNoopBuilderMetadata(TypedDict):
static_renders: dict[ContentVariantInfo, str]
class StaticNoopBuilder[MD: StaticNoopBuilderMetadata](Builder):
def build(self, metadata: MD) -> bytes:
...
@dataclass
class PresentationVariantInfo[B: Builder]:
builder: B
metadata: dict However, what I really want is something like this: @dataclass
class PresentationVariantInfo[B: Builder]:
builder: B
metadata: B.MD Where the type of the metadata can reference the kind of the nested generic B. Unnecessary details: Actually, in my case, I actually need both HKT and an intersection type (cross ref here, though this is a different example I didn't include there). In reality, the AMS supports both a build step (always static for any given asset version, for example, reading a template file into memory) and a render step (actually rendering out the asset, for example, rendering a jinja template). Because both the builder and the renderer specify metadata keys, I really need the intersection of both of their typed dicts. So in that case it would look something like this: class Builder[MD]:
...
class Renderer[MD]:
...
class PresentationVariantInfo[B: Builder, R: Renderer]:
builder: B
renderer: R
# The intersection of the MD type argument for both the builder and the
# renderer
metadata: B.MD & R.MD |
Hey! Yeah, sorry, I messed up sync with the upstream so had to reclone. I'll push the draft shortly. |
Awesome, thanks! To be completely honest, theoretical typing discussions are... a bit too abstract for me at times. So I'm not 100% sure that my use case above is actually and example of HKT. But if it is, feel free to use it! |
I think libraries like einops would also benefit greatly from this. It would be pretty useful to be able to get shape information both into and out of any sort of array manipulation function. |
I'm not sure if further motivating examples are useful, but here's a simple function, not involved in type checking, that I don't believe can be described today: # sketch -- not carefully checked
T = TypeVar('T')
C = TypeVar('C', bound='Collection[T]')
def filter_and_count_dups(c: C[T]) -> Tuple[C[T], Counter[T]]:
'''copy any 'Collection' w/o duplicates, counting what's omitted
Args:
c: any collection of elements
Returns:
(c_wo_dups, dup_counts), where...
c_wo_dups: a copy of 'c', with the 2nd and subsequent
appearances of any given element omitted
dup_counts: counts removed duplicates
'''
seen: Set[T] = set()
ret_list: List[T] = []
ret_counter = Counter[T]()
for elem in c:
if elem in seen:
ret_counter[elem] += 1
else:
ret_list.append(elem)
seen.add(elem)
ret_wo_dups = type(c)(ret_list)
return (ret_wo_dups, ret_counter)
r1 = filter_and_count_dups((1, 2, 4, 3, 4, 4, 2, 1))
reveal_type(r1) # ≈ Tuple[Tuple[int, ...], Counter[int]]
print(repr(r1)) # ≈ ((1, 2, 4, 3), Counter({4: 2, 2: 1, 1: 1}))
r2 = filter_and_count_dups(list('alphabet'))
reveal_type(r2) # ≈ Tuple[List[str], Counter[str]]
print(repr(r2)) # ≈ (['a', 'l', 'p', 'h', 'b', 'e', 't'], Counter({'a': 1})) |
One more use case, consider the following code: class Handler[I, O](Protocol):
def __call__(self, input: I, /) -> O: ...
class Modify(Protocol):
def modify[O](self, f: Callable[[Self], O], /) -> O: ...
# Mixing styles since idk how to properly express it in 695
M = TypeVar("M", bound="Modify")
class Ext[Inner]:
inner: Inner
def modify[O](self: Ext[M], f: Callable[[M], O], /) -> Ext[O]:
return Ext(f(self.inner)) I can extend class MyExt[Inner](Ext[Inner]):
def another_method(self) -> MyExt[SomeOtherType]:
return self.modify(xxx) However, since class Ext[Inner]:
def modify[O](self: Self[M], f: Callable[[M], O]) -> Self[O]:
return type(self)(self.inner.modify(f)) I have encountered that limitation when writing syntactic sugar for my library for handlers. class Predicate[I](Handler[I, bool], Protocol):
... Here, we specialize class PredicateExt[P](Ext[P]):
def and_[I](self: PredicateExt[Predicate[I]], rhs: Predicate[I]) -> PredicateExt[Predicate[I]]:
return self.modify(lambda lhs: And(lhs, rhs)) ... but there's no way to do that without triggering type-checker, unless we had HKTs |
I'm starting to think that by simply making With this, we could write some HKT function from collections.abc import Sequence
from types import GenericAlias
def f[T: Sequence](x: GenericAlias[T, int], /) -> GenericAlias[T, str]: ... Note that type-checkers might complain about the missing type argument in So to be a little bit more specific, the generic class GenericAlias[T, *Ps]:
@property
def __origin__(self) -> type[T]: ...
@property
def __args__(self) -> tuple[*Ps]: ...
... # etc Note that because a variadic type-parameter like Through the But there's one big problem with this approach: >>> type(list[str])
<class 'types.GenericAlias'>
>>> class Spam[T]: ...
...
>>> type(Spam[str])
<class 'typing._GenericAlias'> There are two generic aliases! |
Hi. Here's the code example I have from my project
This obviously does not work. mypy requires I'd suggest the syntax for the feature to be something as simple as
Cheers. |
Can you be more specific?
Since Python 3.12 the new PEP 695 generic syntax is preferred over |
aka type constructors, generic TypeVars
Has there already been discussion about those?
I do a lot of FP that results in impossible situations because of this. Consider an example:
I haven't found a way to make this work, does anyone know a trick or is it impossible?
If not, consider the syntax as a proposal.
Reference implementations would be Haskell, Scala.
optimally, the HK's type param would be indexable as well, allowing for
F[X[X, X], X[X]]
Summary of current status (by @smheidrich, 2024-02-08):
peps
repo. The stub PEP draft so far contains a few examples of the proposed syntax.The text was updated successfully, but these errors were encountered: