Skip to content

Higher-Kinded TypeVars #548

Open
Open

Description

@tek

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:

A = TypeVar('A')
B = TypeVar('B')
F = TypeVar('F')

class M(Generic[F[X], A]):
    def x(fa: F[A], f: Callable[[A], B]) -> F[B]:
        return map(f, fa)

M().x([1], str)

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):

  • @JelleZijlstra has indicated interest in sponsoring a PEP, conditional on a prototype implementation in a major type checker and a well-specified draft PEP.
  • Drafting the PEP takes place in @nekitdev's fork of the peps repo. The stub PEP draft so far contains a few examples of the proposed syntax.
  • That same repo's GitHub Discussions forum forum has been designated as the place to discuss the PEP (and presumably the prototype implementation?). Some limited further discussions have taken place there.
    • If you want to be notified of new discussion threads, I think you have to set the whole repo as "watched" in GitHub?

Activity

  1. gvanrossum commented on Apr 26, 2020

    @gvanrossum
    Member

    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 Optional[Coroutine[...]] return type and derive both the regular Redis class and the RedisPipeline class from it.

  2. samuelcolvin commented on Apr 26, 2020

    @samuelcolvin
    Sponsor

    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.

  3. gvanrossum commented on Apr 26, 2020

    @gvanrossum
    Member
  4. samuelcolvin commented on Apr 26, 2020

    @samuelcolvin
    Sponsor

    humm, but the example above under "Basic example" I would argue IS type-safe.

    All the methods which end return self.execute(...) return what execute returns - either a Coroutine or None.

    Thus I don't see how this as any more "unsafe" than normal use of generics.

  5. jab commented on Apr 29, 2020

    @jab

    @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 $SELF_TYPE would of course use some actually legal syntax that allowed composing the self type with the other parameterized types.

  6. gvanrossum commented on Apr 30, 2020

    @gvanrossum
    Member

    Okay, I think that example is helpful. I recreated it somewhat simpler (skipping the inheritance from Mapping and the property decorators):

    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?

  7. jab commented on Apr 30, 2020

    @jab

    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.

  8. gvanrossum commented on Apr 30, 2020

    @gvanrossum
    Member

    Ah, so the @abstractmethod is also a red herring.

    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. :-(

  9. sobolevn commented on Apr 30, 2020

    @sobolevn
    Member

    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 dry-python/returns.

    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 T[K] we can emulate this by using HKT[T, K] where HKT is a basic generic instance processed by a custom mypy plugin. I am working on this plugin for already some time now, but there's still nothing to show. You can track the progress here: https://pypi.org/project/kinds/ (part of dry-python libraries)

  10. samuelcolvin commented on Apr 30, 2020

    @samuelcolvin
    Sponsor

    But I still don't get the connection with @samuelcolvin's RedisPipeline class. :-(

    Sorry if I wasn't clear. I'll try again to explain:

    I have a class with many (~200) methods, they all return coroutines with different result types (None, bytes, str, int or float). I want a subclass with the same internal logic but where all those methods return None - I can do this in python, but not with type hints currently (here's the actual code if it helps)

    So roughly I want:

    T = TypeVar('T', bytes, str, int, float, 'None')
    Result = Coroutine[Any, Any, T]
    
    class Foo:
        def method_1(self, *args) -> Result[str]:
            ...
    
        def method_2(self, *args) -> Result[None]:
            ...
    
        def method_3(self, *args) -> Result[bool]:
            ...
    
        ...
        ...
    
        def method_200(self, *args) -> Result[int]:
            ...
    
    class Bar:
    
        def method_1(self, *args) -> None:
            ...
    
        def method_2(self, *args) -> None:
            ...
    
        def method_3(self, *args) -> None:
            ...
    
        ...
        ...
    
        def method_200(self, *args) -> None:
            ...

    Except I don't want to have to redefine all the methods on Bar. Assuming I could create my own AlwaysNone type which even when parameterised told mypy the result would always be None

    class AlwaysNoneMeta(type):
        def __getitem__(self, item) -> None:
            return None
    
    class AlwaysNone(metaclass=AlwaysNoneMeta):
        pass

    I think the feature requested here could solve my problem.

    In other words if mypy could understand

    def generate_cls(OuterType):
        class TheClass:
            def method_1(self, *args) -> OuterType[str]:
                ...
            ...
        
        return TheClass
    
    Foo = generate_cls(Result)
    Bar = Generate_cls(AlwaysNone)

    I'd be in the money, but quite understandably it can't.


    I've currently got a working solution where i generate a .pyi stub file with definitions for all these methods but changing the return type to None (see here) so I'm not in immediate need of this anymore.

  11. Kentzo commented on Apr 30, 2020

    @Kentzo

    It looks like you want completely different signatures whose likeness is limited to their names and arguments:

    async def do_work(redis_client: Redis):
        await redit_sclient.get(...)
    
    do_work(RedisPipeline())  # user expects type checker to warn about misuse

    It's understandable you're looking to reuse some common "template", but I fail to see what it has to do with higher-kinded types.

  12. samuelcolvin commented on May 6, 2020

    @samuelcolvin
    Sponsor

    Hi @Kentzo, that's incorrect, these are not completely different signatures.

    Please review the code here; as you can see these are functions which may return an awaitable object, not coroutines. This approach and similar, while not necessarily obvious to beginners, is relatively common in libraries which make extensive use of asyncio and the standard library asyncio code.

    If you're not sure how it works, feel free to submit an issue on that project and I'll endeavour to explain it to you.

  13. Kentzo commented on May 6, 2020

    @Kentzo

    In your initial example:

    class Redis:
        def execute(self, command) -> Coroutine[Any, Any, Union[None, str, int, float]]:
            ...
    
    class RedisPipeline(Redis):
        def execute(self, command) -> None:

    These are different signatures. Functions annotated to accept Redis as an argument would expect execute to return a coroutine.

  14. samuelcolvin commented on May 6, 2020

    @samuelcolvin
    Sponsor

    This is off-topic, please create an issue on async-redis if you want to discuss this more.

    In short, yes they're different signatures, but I think my explanation above gives enough detail on what I'm doing and why I think it relates to this issue.

  15. 3noch commented on Aug 28, 2020

    @3noch

    +1 on this. It's hard to say the use cases are rare when it's not even possible to express... There may be countless use cases hiding behind duck typing that simply can't be checked by the type system and therefore certainly aren't recognized as such. Having a type safe generic "Mappable" class as shown in the OP would be really useful.

  16. gvanrossum commented on Aug 28, 2020

    @gvanrossum
    Member

    It's hard to say the use cases are rare when it's not even possible to express

    That makes no sense. You can express the OP's example just fine in untyped Python, you just can't express the types correctly. So if there were "countless" real examples we would encounter frequent user requests for type system extensions to allow typing this correctly. I haven't seen a lot of those.

    Do you have an actual (non-toy) example?

  17. sobolevn commented on Aug 28, 2020

    @sobolevn
    Member

    You actually can have Higher Kinded Types in Python with full mypy support.

    Here's how Mappable looks like from OP:

  18. 3noch commented on Aug 28, 2020

    @3noch

    @sobolevn That's awesome! Thank you!

    @gvanrossum Inexpressible in the type system is what I was referring to. My point was that writing this off as rare seems premature since Python's types system is not a hard stop on your ability to keep going. So it might be actually quite common and how would we know? Certainly you're right that it's not a common request though which is a good point and slightly different than mine. However, it's all moot now because it can be done! Awesome!

  19. BvB93 commented on Aug 29, 2020

    @BvB93
    Contributor

    Do you have an actual (non-toy) example?

    An example would be the dict.fromkeys(...) method, as the output type is variable with respect to both the dict (sub-)class as well as the key type (see python/typeshed#3800). This example above can, admittedly, be more or less resolved by explicitly re-annotating aforementioned method in every single subclass.

    Another less easily resolved example one would be the np.asanyarray(a, dtype=..., ...) function in numpy. Per the documentation: "Convert the input to an ndarray, but pass ndarray subclasses through.".
    What this means is that, similar to dict.fromkeys(...), the output is variable with respect to both any passed ndarray subclasses (the a parameter) as well as the type of the embedded scalars (the dtype parameter).

    Examples

    In [1]: import numpy as np
    
    In [2]: class TestArray(np.ndarray):
       ...:     ...
       ...:
    
    In [3]: array: TestArray = np.array([0, 1, 2]).view(TestArray)
    
    In [4]: array  # array of integers
    Out[4]: TestArray([0, 1, 2])
    
    In [5]: np.asanyarray(array, dtype=float)  # array of floats
    Out[5]: TestArray([0., 1., 2.])
  20. gvanrossum commented on Aug 29, 2020

    @gvanrossum
    Member
  21. NeilGirdhar commented on Sep 16, 2020

    @NeilGirdhar

    Ran into this today:

    from typing import Any, Generic, Mapping, TypeVar
    
    
    ProblemState = TypeVar('ProblemState')
    Action = TypeVar('Action')
    
    
    class Problem(Generic[ProblemState, Action]):
        pass
    
    
    ProblemT = TypeVar('ProblemT', bound=Problem[ProblemState, Action])
    
    
    class Solution(Generic[ProblemT, ProblemState, Action]):
        pass

    I could have worked around it with an intersection operator:

    ProblemU = TypeVar('ProblemU', bound=Problem[Any, Any])
    
    class Solution(Generic[ProblemU, ProblemState, Action]):
        ProblemT = Intersection[ProblemU, Problem[ProblemState, Action]]

    Would intersection help in the general case without requiring as much effort?

  22. florensacc commented on Sep 30, 2020

    @florensacc

    Just ran into this limitation when trying to type methods of a generic class that return an instance of the same generic class but with other arguments. This is actually very common for classes that can easily have some properties changed, like numpy arrays or torch tensors and their device (gpu or cpu) or dtype (int, float, ...):

    DeviceTransferableT = TypeVar("DeviceTransferableT", bound="DeviceTransferable")
    
    class DeviceTransferable(Generic[DeviceT]):
    
        def to_device(self: DeviceTransferableT, device: Type[DeviceT]) -> DeviceTransferableT[DeviceT]:   # error!
              ...  # some great implementation
    
    class SomeOtherClass(DeviceTransferable):
        """Would prefer not to re-implement `to_device` in every inheriting class."""
    

    Is there any other solution?

  23. gvanrossum commented on Sep 30, 2020

    @gvanrossum
    Member

    @florensacc Can you expand the example so that I can actually run it by mypy? A few things are missing and I can’t guess what their definitions should be.

  24. florensacc commented on Sep 30, 2020

    @florensacc

    Here's a full example, which throws two errors: error: Type variable "DeviceTransferableT" used with arguments and Argument 1 to "f" has incompatible type "SomeOtherClass[Device0]"; expected "SomeOtherClass[Device1]".

    import attr
    import abc
    from typing import Type, TypeVar, Generic, ClassVar
    
    
    class Device(abc.ABC):
        device_id: ClassVar[int]
    
    
    class Device0(Device):
        device_id = 0
    
    
    class Device1(Device):
        device_id = 1
    
    
    DeviceT = TypeVar("DeviceT", bound=Device)
    AnotherDeviceT = TypeVar("AnotherDeviceT", bound=Device)
    DeviceTransferableT = TypeVar("DeviceTransferableT", bound="DeviceTransferable")
    
    
    @attr.s()
    class DeviceTransferable(Generic[DeviceT]):
    
        device: DeviceT = attr.ib()
    
        def to_device(self: DeviceTransferableT, device: Type[AnotherDeviceT]) -> DeviceTransferableT[AnotherDeviceT]:
            return attr.evolve(self, device=device)
    
    
    @attr.s()
    class SomeOtherClass(DeviceTransferable[DeviceT]):
        """Would prefer not to re-implement `to_device` in every inheriting class."""
        pass
    
    
    C_in0 = SomeOtherClass(device=Device0())
    
    C_in1 = C_in0.to_device(Device1)
    
    
    def f(c: SomeOtherClass[Device1]) -> None:
        print(f"The device of c is {c.device}, but its type is still SomeOtherClass[Device0]!")
    
    
    f(C_in1)
  25. gvanrossum commented on Sep 30, 2020

    @gvanrossum
    Member

    Oh, @attr.s :-(

    Can you do it without?

  26. florensacc commented on Sep 30, 2020

    @florensacc

    sure, you can remove all attr.s stuff and use this class definition:

    class DeviceTransferable(Generic[DeviceT]):
    
        def __init__(self, device: DeviceT):
            self.device = device
    
        def to_device(self: DeviceTransferableT, device: Type[AnotherDeviceT]) -> DeviceTransferableT[AnotherDeviceT]:
            return type(self)(device=device)

    without the "argument" to the TypeVar this code runs happily and prints my message:
    The device of c is <class '__main__.Device1'>, but its type is still SomeOtherClass[Device0]!

  27. gvanrossum commented on Sep 30, 2020

    @gvanrossum
    Member

    Okay, I get it. If/when we fix this we'll make sure to have a test like that. Thanks!

  28. sobolevn commented on Oct 30, 2020

    @sobolevn
    Member

    We have released a first prototype of Higher Kinded Types emulation for Python.

    It is available for everyone to try! Quick demo: https://gist.github.com/sobolevn/7f8ffd885aec70e55dd47928a1fb3e61

    Here's how a function's signature will look like:

    from returns.primitives.hkt import Kind1, kinded
    
    _InstanceKind = TypeVar('_InstanceKind', bound='HasValue')
    
    @kinded
    def apply_function(
        instance: Kind1[_InstanceKind, _ValueType],
        callback: Callable[[_ValueType], _NewValueType],
    ) -> Kind1[_InstanceKind, _NewValueType]:
        ...

    Source code: https://github.com/dry-python/returns
    Docs: https://returns.readthedocs.io/en/latest/pages/hkt.html and https://sobolevn.me/2020/10/higher-kinded-types-in-python

  29. sebastian-philipp commented on Jan 18, 2021

    @sebastian-philipp

    See ceph/ceph#38953 for a real world use case

  30. benjamindkilleen commented on Apr 1, 2021

    @benjamindkilleen

    Finally found this issue after running into TypeError: 'TypeVar' object is not subscriptable and searching around for the right way to do this. Turns out there isn't. Has there been any change in status for the implementation? The use-case might be limited for the annotations themselves, but if utilized in a couple core libraries, the end-user experience could be greatly improved by the resulting hints in IDEs.

    I empathize with @tek, but unfortunately can't leave Python for the strongly-typed shores of Scala or Haskell, due to Python's ML ecosystem and relatively easy learning curve.

  31. Tinche commented on May 19, 2021

    @Tinche

    Hm, I believe I've finally run into this. Here's my example, let me know if I'm misunderstanding something.

    Let's say you're adding static types to a Mongo ODM/ORM-type framework. You define your model using a class.

    @dataclass
    class User:
        __collection__ = "users"  # This is the "table", or collection in Mongo. Class variable, not instance
    
        _id: ObjectId  # Every Mongo document has an ID, most often ObjectId, but can be maybe str or something else
        username: str  # A random property

    Now you write a function to query for this model. We write a generic protocol for models, parametrizing by the ID type:

    ID = TypeVar("ID")
    
    class MongoModel(Protocol[ID])
        __collection__: str
    
        @property
        def _id(self) -> ID:
            ...

    Our User class is thus a MongoModel. Now the actual querying function. We can query just passing in the ID.

    C = TypeVar("C", bound=MongoModel)
    def find_one(model_cls: Type[C], id: ???) -> C: ...

    There's nothing to put instead of ???, is there?

    My first instinct was to try:

    C = TypeVar("C", bound=MongoModel)
    TID = TypeVar("TID")
    def find_one(model_cls: Type[C[TID], id: TID) -> C: ...

    But no dice, obviously.

    Second approach: I hoped Mypy would realize MongoModel already references ID (since it's defined with it), so I can write this:

    C = TypeVar("C", bound=MongoModel)
    def find_one(model_cls: Type[C], id: ID) -> C: ...
    

    But no, Mypy treats them as two separate type variables (in other words, I can pass in an int and it doesn't catch it).

    And C cannot be changed to TypeVar("C", bound=MongoModel[ID]) either.

  32. MischaPanch commented on Aug 17, 2021

    @MischaPanch

    Just to extend the list of people encountering it, I also ran into this. My example is similar to the ones above in spirit but aggravated by extraction of the generic class at runtime, which is possible since python 3.8. I am not sure whether it is officially recommended but I really like this mechanism :). Unfortunately, using it will very often require subscriptable TypeVars, like in the sketch below.

    For simplicity I slightly changed the real signature. In real use cases I load the registry from a file and not from prepared list, so polymorphism makes a bit more sense there.

    class SettingsRegistry(Generic[TSettings], ABC):
        @classmethod
        def from_serialized_settings(cls: Type[TSettingsRegistry], serialized_settings: List[str]) -> TSettingsRegistry[TSettings]:
             settings_class = get_args(cls.__orig_bases__[0])[0]
             deserialized_settings = [settings_class.deserialize(setting) for setting in serialized_settings]
             return cls(deserialized_settings)

    This way I can avoid code duplication but typing TSettingsRegistry[TSettings] is currently disallowed, of course.

  33. vnmabus commented on Sep 14, 2021

    @vnmabus

    Just another use for this: use the new TypeGuard to check the types of a collection:

    T = TypeVar("T", bound=Collection)
    V = TypeVar("V")
    
    def all_elements_type(
        collection: T[object],
        element_type: Type[V],
    ) -> TypeGuard[T[V]]:
        return all(isinstance(t, element_type) for t in collection)

    This is very useful and currently not possible AFAIK.

  34. added
    topic: featureDiscussions about new features for Python's type annotations
    and removed on Nov 4, 2021
  35. harahu commented on Nov 16, 2021

    @harahu

    Another potential use case example for HKTs:

    https://toolz.readthedocs.io/en/latest/_modules/toolz/dicttoolz.html#valmap

    I've made my own typed version of it, like so:

    K = TypeVar("K")
    V = TypeVar("V")
    M = TypeVar("M")
    
    
    def valmap(
        func: Callable[[V], M],
        d: Mapping[K, V],
        factory: Type[MutableMapping[K, M]] = dict,
    ) -> MutableMapping[K, M]:
        rv = factory()
        rv.update(zip(d.keys(), map(func, d.values())))
        return rv

    but in order to preserve type information, I would much rather want to type it as:

    K = TypeVar("K")
    V = TypeVar("V")
    M = TypeVar("M")
    F = TypeVar("F", bound=MutableMapping)
    
    
    def valmap(
        func: Callable[[V], M],
        d: Mapping[K, V],
        factory: Type[F] = dict,
    ) -> F[K, M]:
        rv = factory()
        rv.update(zip(d.keys(), map(func, d.values())))
        return rv

    @gvanrossum makes the point that this isn't a commonly requested feature. I think that is correct to some extent, although the level of activity in this thread is making me slightly reconsider. I think it isn't commonly requested because it is a fairly advanced technique. But, I think my example, as well as many other examples in this thread are showing that fundamental library code in fairly prevalent use would be able to benefit from HKT in the sense that annotations would more accurately describe actual behavior. Often times this library code is maintained by skilled programmers that could leverage HKT if available. This is a benefit that then trickles down to the wider python ecosystem, including users that have never heard of HKT, but might leverage type checkers. You don't need to be a wizard to enjoy more accurate lib function call return types.

  36. xkortex commented on Dec 6, 2021

    @xkortex

    @harahu - That is an excellent example. I think you described the situation perfectly. I've been in the exact same spot of wanting to write a transformation over a map-like collection, and resorting to some zany nested type signatures, when really all I wanted to do was turn some D[K, V] into a F[K, M] based on some provided type F and a transform func(v: V) -> M.

    HKT is a fairly sophisticated tool that many folks do not even know to reach for, but when you are in a situation which benefits it, many doors are unlocked. HKTs often greatly simplify the type signature, like in the example above. It shows up most frequently when you want to "transmute" some object (typically a container type) into a target (container) type, but you want the internal types to be preserved.

    This pattern does not seem to show up frequently when writing application code. Most software engineers probably do not need to know about it if they don't want to. However, when writing library code for others to consume, the pattern shows up quite a lot. Probably because it's a fairly high level abstraction, and that kind of abstraction lends itself to reuse.

    In my opinion, HKTs would push the envelop for maintainability and ergonomics for maintainers of libraries that embrace the static typing discipline. In the same vein, it would not add any cognitive overhead for those looking to implement libraries, nor those looking to start writing libraries with static types. If anything, I think it has the potential to shorten the learning curve for those with medium experience with type systems (such as myself). I have definitely gotten myself into some really convoluted type signatures trying to express the transformation I wanted. Desire to use HKTs seems to scale with familiarity with type systems and abstracting over generics.

  37. 3noch commented on Dec 14, 2021

    @3noch

    I think it has the potential to shorten the learning curve for those with medium experience with type systems (such as myself).

    💯 When you have generics but not HKT, you end up having to twist yourself into knots trying to express what would otherwise be simple. HKT is merely generics that works everywhere instead of generics that only works in some places.

  38. Tishka17 commented on Dec 23, 2021

    @Tishka17

    I am having two similar protocols:

    class A(Protocol):
       def foo(self)-> int:...
       def bar(self)-> bool:...
    
    class AsyncA(Protocol):
       def foo(self)-> Awaitable[int]:...
       def bar(self)-> Awaitable[bool]:...

    Then only difference is that in second case there are async functions. I would be glad to unite them using Generics. But of cause this does not work:

    AT = TypeVar("AT", Union, Awaitable)
    
    
    class A(Protocol[AT]):
        def foo(self) -> AT[int]: ...
        def bar(self) -> AT[bool]: ...
  39. zzzeek commented on Jan 1, 2022

    @zzzeek

    hey folks, another ORM use case coming in here, this system is in fact what we would have used for sqlalchemy2-stubs had it existed to encapsulate our structure of ColumnElement[TypeEngine[_T]], but as it's not, SQLAlchemy 2.0's typing will likely do what the original sqlalchemy-stubs did which is consider it to be ColumnElement[_T]. this solves most of the practical problems at the expense of looking a little funny and breaking the hierarchy a bit.

    references for me at:

    #999

    sqlalchemy/sqlalchemy#7519

  40. jab commented on Mar 15, 2022

    @jab

    I just found out about the Self type that was recently added to Python 3.11. Could that be extended to add support for higher-kinded types?

    import typing as t
    
    KT = t.TypeVar('KT')
    VT = t.TypeVar('VT')
    
    class BidirectionalMapping(t.Mapping[KT, VT]):
        @property
        def inverse(self: t.Self[KT, VT]) -> t.Self[VT, KT]:
            ...

    cc @JelleZijlstra @pradeep90 @Gobot1234 et al.

  41. mwchase commented on Mar 15, 2022

    @mwchase

    Using parameters on Self is explicitly disallowed by the PEP, so that seems like a non-starter unless the justifications can be addressed.

  42. jab commented on Mar 15, 2022

    @jab

    Thanks. If you're talking about the

    In such cases, we recommend using an explicit type for self

    in the https://peps.python.org/pep-0673/#use-in-generic-classes section, this recommendation doesn't work for the use case here. Since no specific mention of higher-kinded types is mentioned in the PEP, perhaps this use case wasn't even in mind when the PEP was written?

  43. mwchase commented on Mar 15, 2022

    @mwchase

    I meant

    Note that we reject using Self with type arguments, such as Self[int]. This is because it creates ambiguity about the type of the self parameter and introduces unnecessary complexity:

  44. Gobot1234 commented on Mar 15, 2022

    @Gobot1234
    Contributor

    I just found out about the Self type that was recently added to Python 3.11. Could that be extended to add support for higher-kinded types?

    import typing as t
    
    
    
    KT = t.TypeVar('KT')
    
    VT = t.TypeVar('VT')
    
    
    
    class BidirectionalMapping(t.Mapping[KT, VT]):
    
        @property
    
        def inverse(self: t.Self[KT, VT]) -> t.Self[VT, KT]:
    
            ...

    cc @JelleZijlstra @pradeep90 @Gobot1234 et al.

    Yes I would be in support of this, it was something I considered but as HKT wasn't/isn't a thing I didn't think it would be a good idea to include in the PEP

  45. pradeep90 commented on Mar 15, 2022

    @pradeep90
  46. erictraut commented on Mar 15, 2022

    @erictraut
    Collaborator

    I agree with @pradeep90. I think this would create significant confusion and complexity. It would also deviate from the Self type in other languages.

  47. jab commented on Mar 15, 2022

    @jab

    @pradeep90 writes:

    In your case, it would look like: ..

    I think there might be a misunderstanding? I am already doing what you proposed, and it does not help with this issue, as I commented here previously.

  48. jab commented on Mar 15, 2022

    @jab

    @erictraut writes:

    I agree with @pradeep90. I think this would create significant confusion and complexity. It would also deviate from the Self type in other languages.

    Good to know, thank you. Do you have any ideas about how support for HKTs could be added in a way that would match other languages that already support them?

  49. pradeep90 commented on Mar 15, 2022

    @pradeep90

    @pradeep90 writes:

    In your case, it would look like: ..

    I think there might be a misunderstanding? I am already doing what you proposed, and it does not help with this issue, as I commented here previously.

    I hadn't read your earlier comment in the thread because I mistakenly assumed this was a fresh (typing-sig) thread.

    The snippet I shared works in both Pyre and Mypy (playground link). The same goes for your existing code, where the self is not annotated.

    If you're still having trouble, could you repro your case in a Mypy playground snippet?

  50. jab commented on Mar 15, 2022

    @jab

    @pradeep90, please reread #548 (comment). Your playground link does not exercise the functionality this is asking for. You haven't created a subclass of BidirectionalMapping. If you do, you will see that its inherited inverse undesirably returns a BidirectionalMapping, rather than an instance of the subclass.

  51. erictraut commented on Mar 15, 2022

    @erictraut
    Collaborator

    Do you have any ideas about how support for HKTs could be added in a way that would match other languages that already support them?

    I don't consider this an example of HKT. I think of Self as a type that is implicitly specialized with the type parameters of the class that encloses it. In your example, it's effectively bound to the type BidirectionalMapping[KT, VT]. Specializing it a second time therefore doesn't make sense, any more than it would make sense to re-specialize list[int].

  52. jab commented on Mar 15, 2022

    @jab

    What would you consider this, then, and should I open a separate issue for that once I know what to call it?

  53. pradeep90 commented on Mar 15, 2022

    @pradeep90

    You haven't created a subclass of BidirectionalMapping. If you do, you will see that its inherited inverse undesirably returns a BidirectionalMapping, rather than an instance of the subclass.

    Guido's thoughtful snippet demonstrates what you were trying to get at and is the kind of repro I was asking for (mypy playground, for any future readers trying to follow).

    Yeah, this is one of the self cases where you'd need to add explicit stubs to the subclass methods, like you do in frozenbidict. The exact same thing would occur for the generic Container example in the PEP: if we subclassed it, we would need to explicitly specify the subclass method's return type.

    If we eventually get HKTs in the type system, that would have to be the solution here rather than co-opting the Self type. As @erictraut mentioned, Self is intended to be a simple replacement for a handwritten TypeVar and helps with almost all cases except the ones in generic classes where we need to tinker with the generic type parameters (as in the PEP's Container or your BidirectionalMapping).

  54. Gobot1234 commented on Mar 15, 2022

    @Gobot1234
    Contributor

    Maybe I'm missing something but if Self is meant to act like a shorthand for a bound TypeVar so surely the example which could be written as:

    SelfBidirectionalMapping = TypeVar("SelfBidirectionalMapping", bound=BidirectionalMapping[Any, Any])
    
    class BidirectionalMapping(t.Mapping[KT, VT]):
        @property
        def inverse(self: SelfBidirectionalMapping[KT, VT]) -> SelfBidirectionalMapping[VT, KT]:
            ...
    
    
    m: BidirectionalMapping[int, str]
    y = m.inverse
    reveal_type(y)  # => `BidirectionalMapping[str, int]`

    Could be written using Self as shown in #548 (comment)?

  55. jab commented on Mar 16, 2022

    @jab

    Thanks @Gobot1234, but the SelfBidirectionalMapping[KT, VT] in that snippet (and similarly the SelfBidirectionalMapping[VT, KT]) cause error: Type variable "SelfBidirectionalMapping" used with arguments.

    In any case, I don't think this is getting the job done.

    Here's a more complete example based on your snippet, demonstrating that we do need HKTs (or some other not-yet-implemented feature) to make this use case work:

    from collections import UserDict
    import typing as t
    
    
    KT = t.TypeVar('KT')
    VT = t.TypeVar('VT')
    
    SelfBidirectionalMapping = t.TypeVar("SelfBidirectionalMapping", bound=BidirectionalMapping[t.Any, t.Any])
    
    
    class BidirectionalMapping(t.Mapping[KT, VT]):
        @property
        def inverse(self: SelfBidirectionalMapping[KT, VT]) -> SelfBidirectionalMapping[VT, KT]:  # results in "error: Type variable "SelfBidirectionalMapping" used with arguments"
            ...
    
    
    class bidict(UserDict[KT, VT], BidirectionalMapping[KT, VT]):
        pass
    
    
    b = bidict({'answer': 42})
    reveal_type(b)  # correct: bidict[str, int]
    reveal_type(b.inverse)  # wrong: bidict[str, int]  (should be: bidict[int, str])

    mypy-play link here: https://mypy-play.net/?mypy=latest&python=3.7&gist=5272ebc066abb4b6b32df0a336c78b00&flags=strict

  56. Gobot1234 commented on Mar 16, 2022

    @Gobot1234
    Contributor

    Thanks @Gobot1234, but the SelfBidirectionalMapping[KT, VT] in that snippet (and similarly the SelfBidirectionalMapping[VT, KT]) cause error: Type variable "SelfBidirectionalMapping" used with arguments.

    In any case, I don't think this is getting the job done.

    Here's a more complete example based on your snippet:

    from collections import UserDict
    
    import typing as t
    
    
    
    
    
    KT = t.TypeVar('KT')
    
    VT = t.TypeVar('VT')
    
    
    
    SelfBidirectionalMapping = t.TypeVar("SelfBidirectionalMapping", bound=BidirectionalMapping[t.Any, t.Any])
    
    
    
    
    
    class BidirectionalMapping(t.Mapping[KT, VT]):
    
        @property
    
        def inverse(self: SelfBidirectionalMapping[KT, VT]) -> SelfBidirectionalMapping[VT, KT]:  # results in "error: Type variable "SelfBidirectionalMapping" used with arguments"
    
            ...
    
    
    
    
    
    class bidict(UserDict[KT, VT], BidirectionalMapping[KT, VT]):
    
        pass
    
    
    
    
    
    b = bidict({'answer': 42})
    
    reveal_type(b)  # correct: bidict[str, int]
    
    reveal_type(b.inverse)  # wrong: bidict[str, int]  (should be: bidict[int, str])

    mypy-play link here

    This is meant to use HKT is it not? The error (currently) is to be expected

  57. mwchase commented on Mar 17, 2022

    @mwchase

    Going way back, I think there's a potential workaround for the Awaitable-or-None case, but it requires code reorganization because of the lack of higher-kinded types. Something like

    # Assume Command[T] is some kind of hacky parametric newtype or container
    class Redis:
        def get(self, *args) -> Command[str]:
            ...
            return Command[str](command)
    
        def set(self, *args) -> Command[None]:
            ...
            return Command[None](command)
    
        def exists(self, *args) -> Command[bool]:
            ...
            return Command[bool](command)
    
        # ... and many MANY more ...
    
    class ConnectionExecutor:
        def execute(self, command: Command[T]) -> Coroutine[Any, Any, T]:
            return self.connection.execute(...)
    
    class PipelineExecutor:
        def execute(self, command: Command[T]) -> None:
            self.pipeline.append(command)
    

    And then you can have an executor variable and a redis variable, and instead of redis.set(*args) # type: ???, you have executor.execute(redis.set(*args)), which knows what the return type is because we've got a sensible value from the Redis object's method, passed into the executor, which has a known type.

    (I don't know if it makes sense to still have the Redis class in that case, or to just have a module with a bunch of top-level functions.)

    (Anyway, note that this means that the wrapper methods explicitly can't wrap multiple calls or anything like that, but writing methods like that is already broken under the original code, so...)

    Assuming it still makes sense for all of those classes to exist, then the HKT issue can be made to re-manifest by saying "I'd rather have the Redis and Executor classes combined into one."
    I tried to sketch out what typing that would look like, and it got so messy that I'm sure I was overcomplicating it somehow, so I'm going to stop for now.

  58. SimpleArt commented on Mar 26, 2022

    @SimpleArt

    I just recently came across this issue myself and realized that there could be an extremely simple fix using intersection types assuming they would work with type variables.

    Example:

    from collections import UserList
    from typing import List, TypeVar
    
    T = TypeVar("T")
    ListType = TypeVar("ListType", bound=List)
    
    def append(L: ListType & List[T], x: T) -> ListType & List[T]:
        L.append(x)
        return L
    
    L: UserList[int] = append(UserList[int](), 0)  # Should pass, as it is both UserList and List[int].

    In this case, ListType & List[T] is the desired behavior for ListType[T], the HKT desired in this thread.

    This would however require type intersections to support generics i.e. UserList[int] == UserList & List[int].

  59. joelberkeley commented on Mar 26, 2022

    @joelberkeley

    @SimpleArt that's neat and I can imagine that would work in some cases, but I don't believe it solves the full problem. For example, unless I'm mistaken, you can't use it in functor*

    class Functor(ABC):
        def map(self: F[A], f : Callable[[A], B]) -> F[B]:
            ...

    because the type variable F can't be applied to type variables A or B in the way List can in your example.

    *someone tell me if I've got functor wrong - i'm not clear what it looks like in python

  60. SimpleArt commented on Mar 26, 2022

    @SimpleArt

    @joelberkeley It would, I'm suggesting F[A] in your example be interpreted as F & F.bound[A], where F.bound is the bound for your type variable e.g. TypeVar("F", bound=List).

    In this case it looks like F is the previously mentioned Self type. Fully written out, your example would be:

    from __future__ import annotations
    from abc import ABC, abstractmethod
    from typing import Callable, Generic, TypeVar
    
    A = TypeVar("A")
    B = TypeVar("B")
    Self = TypeVar("Self", bound="Functor")
    T = TypeVar("T")
    
    
    class Functor(ABC, Generic[T]):
    
        @abstractmethod
        def map(self: Self & Functor[A], f: Callable[[A], B]) -> Self & Functor[B]:
            ...

    Self ensures functorB = functorA.map(f) makes functorB retains the same subclass as functorA.

    Functor[A] -> Functor[B] ensures functorB = functorA.map(f) makes functorB retains the correct type variable instead of just Any.

  61. joelberkeley commented on Mar 26, 2022

    @joelberkeley

    F doesn't have a bound in my code, but maybe let's wait for someone else to chime in because I'm not very confident in my definition of Functor (specifically, whether Functor should be generic in F, the "container type" if you will).

    What I do believe, however, is that Functor isn't generic in T (the "element type" if you will) so you wouldn't be able to write Functor[A]. Indeed, I think it's core to the definition of Functor that it's not generic in T

  62. SimpleArt commented on Mar 26, 2022

    @SimpleArt

    @joelberkeley Perhaps we are misunderstanding each other. The self parameter must* have type Functor in your example, that's how Python methods work. That means Functor must be a subtype of F[A], which according to what you inherited that won't be the case

    Perhaps what you really wanted was another separate argument for the F[A]? Or like I suggested, it is Functor which should be Generic? Or maybe you meant Functor[F[A]]?

  63. joelberkeley commented on Mar 26, 2022

    @joelberkeley

    I think what's confusing me is that I'm trying to inherit Functor in its container type, rather than make instances of it (which is the more usual approach). In that case, Functor is like

    class Functor(ABC, Generic[F]):
        def map(self, f : Callable[[A], B], xs : F[A]) -> F[B]:
            ...
    
    class ListFunctor(Functor[List]):
        def map(self, f : Callable[[A], B], xs : List[A]) -> List[B]:
            return [f(x) for x in xs]
    
    strs : List[str] = ListFunctor().map(str, [1, 2])

    Hopefully that clears up the confusion.

  64. SimpleArt commented on Mar 26, 2022

    @SimpleArt

    @joelberkeley Then this is what it would look like with my suggestion:

    from __future__ import annotations
    from abc import ABC, abstractmethod
    from typing import Callable, Generic, List, TypeVar
    
    T = TypeVar("T")
    
    
    class Kind1(Generic[T]):
        """A generic with 1 parameter."""
        pass
    
    
    A = TypeVar("A")
    B = TypeVar("B")
    F = TypeVar("F", bound=Kind1)
    
    
    class Functor(ABC, Generic[F]):
    
        @abstractmethod
        def map(self: Functor[F], f: Callable[[A], B], xs: F & Kind1[A]) -> F & Kind1[B]:
            ...

    This ensures the following types should be preserved:

    def map(self: Functor[F], f: Callable, xs: F) -> F:
    
    def map(self: Functor, f: Callable[[A], B], xs: Kind1[A]) -> Kind1[B]:
  65. joelberkeley commented on Mar 26, 2022

    @joelberkeley

    I believe that by bounding F on Kind1, you're implicitly recreating the List example. For example, you can't do this with types you don't own, which don't subclass Kind1, including stdlib types. That might be resolvable with a Protocol but you'll have other problems if you try to do that such as Protocols requiring covariant type parameters. I don't believe it's appropriate to require covariance in a general functor.

  66. SimpleArt commented on May 19, 2022

    @SimpleArt

    Interestingly, some linters such as mypy already treat self as an intersection of types. Here's a minimal example:

    from typing import Generic, TypeVar
    
    Self = TypeVar("Self", bound="Parent")
    T1 = TypeVar("T1")
    T2 = TypeVar("T2")
    
    
    class Parent(Generic[T1]):  # Binds `self: Parent[T1]` to methods implicitly.
    
        def add(self: Self, x: T1) -> Self:  # Binds `self: Self` tightly to subclasses.
            ...
    
    
    class Child(Parent[T1], Generic[T1, T2]):
        ...
    
    
    x = Child[int, str]()  # Child[int, str]
    y = x.add(0)           # Passes, and `Child[int, str]` still.
    z = x.add("abc")       # Fails, incompatible `A[int]` with `x: str`.

    In this example, when doing x.add, self is implicitly bound as Parent[int] from the Parent class itself. It is also simultaneously bound as Child[int, str] via Self. In this way, we have two types being bound to self simultaneously.

  67. nekitdev commented on Jul 23, 2022

    @nekitdev

    I'm currently writing a PEP to propose HKTs with the args argument to the TypeVar. Any suggestions are welcomed!

    Here's how a functor would look like:

    from typing import Callable, Protocol, TypeVar
    
    F = TypeVar("F", bound="Functor[T]", args=1)
    
    T = TypeVar("T", covariant=True)
    U = TypeVar("U")
    
    
    class Functor(Protocol[T]):
        def map(self: F[T], function: Callable[[T], U]) -> F[U]:
            ...
  68. nekitdev commented on Jul 23, 2022

    @nekitdev

    Following the bidict example, one could do:

    from collections import UserDict
    from typing import Mapping, TypeVar
    
    BM = TypeVar("BM", bound="BidirectionalMapping[K, V]", args=2)
    
    K = TypeVar("K")
    V = TypeVar("V")
    
    
    class BidirectionalMapping(Mapping[K, V]):
        @property
        def inverse(self: BM[K, V]) -> BM[V, K]:
            ...
    
    
    class bidict(UserDict[K, V], BidirectionalMapping[K, V]):
        ...
    
    
    bd = bidict(value=42)
    
    reveal_type(bd)  # bidict[str, int]
    
    reveal_type(bd.inverse)  # bidict[int, str]
  69. nekitdev commented on Jul 23, 2022

    @nekitdev

    The create example mentioned by @sobolevn is a bit more difficult to express, though, since T is not bound to anything, therefore referring to its type constructor is not exactly trivial.

  70. nekitdev commented on Jul 23, 2022

    @nekitdev

    In general, any code that needs to be generic over types and type constructors needs HKTs in one way or another.

    As the simplest example, the wrapping type:

    from typing import Callable, Generic, TypeVar
    
    from attrs import frozen
    
    W = TypeVar("W", bound="Wrap[T]", args=1)
    
    T = TypeVar("T", covariant=True)
    U = TypeVar("U")
    
    
    @frozen()
    class Wrap(Generic[T]):
        value: T
    
        def map(self: W[T], function: Callable[[T], U]) -> W[U]:
            return type(self)(function(self.value))
  71. nekitdev commented on Jul 23, 2022

    @nekitdev

    I'm pretty sure it's possible to run into issues related to how __new__ is handled by type checkers, though, which might add onto the resulting complexity.

  72. erictraut commented on Jul 23, 2022

    @erictraut
    Collaborator

    It's not clear to me how this would compose with PEP 695 if both were to be accepted. In particular, if the notion of defining TypeVars as global variables were deprecated, how would your proposal work? You will need to consider the scoping rules carefully.

    It may be instructive to look at other languages that support HKTs. You'll notice that I did a survey of type parameter syntax for other languages as part of PEP 695 (see Appendix A). I think it would be useful to do a similar survey of other languages that support HKTs. To my knowledge, very few of them do. It would be interesting to understand why that's the case — and why you think this added level of complexity is justified in the Python type system when most languages have not gone this route. I've seen a few examples of where it might be useful, but they strike me as edge cases. I'd want to see evidence that such a feature would be useful to more than just a handful of users because it will not be easy to implement such a feature within the various Python type checkers.

  73. nekitdev commented on Jul 23, 2022

    @nekitdev

    Indeed, PEP 695 does make this feature much more complex to implement.

  74. joelberkeley commented on Jul 23, 2022

    @joelberkeley

    @nekitdev given the similarity with scala's syntax in that PEP, you may find scala's syntax for higher-kinded types useful

  75. nekitdev commented on Jul 23, 2022

    @nekitdev

    IMO Scala's HKT syntax fits well with PEP 695, though, as Eric has mentioned, HKTs are rather complex to implement type-checker-wise.

  76. nekitdev commented on Jul 23, 2022

    @nekitdev

    Then the monad would look something like this:

    from typing import Callable, Protocol
    
    
    class Functor[F[_]](Protocol):
        def map[T, U](self: F[T], function: Callable[[T], U]) -> F[U]:
            ...

    @erictraut

    EDIT: renamed to Functor[T]

  77. erictraut commented on Jul 23, 2022

    @erictraut
    Collaborator

    That syntax approach looks good to me. A variation that may be worth considering is to use ... rather than _.

  78. nekitdev commented on Jul 23, 2022

    @nekitdev

    The _ syntax is a bit more extensive than ...; for example, T[_, _] is a HKT with two generic types.
    This essentially would allow an arbitrary amount of generic types in HKTs.

  79. nekitdev commented on Jul 23, 2022

    @nekitdev

    I'm not fully sure how implementing the aforementioned Functor would look like. Open for discussion here, I guess.

  80. erictraut commented on Jul 23, 2022

    @erictraut
    Collaborator

    I was suggesting T[..., ...] for an HKT with two generic types. The character sequence ... is already a defined token in Python that generally means "this is a stand-in for something that will be supplied elsewhere". For example, consider how it's used in a Callable type annotation or a ParamSpec specialization.

    By contrast, the character _ is not a dedicated token but an identifier, and it typically means "I don't care about the value of this identifier". PEP 634 did extend _ to mean a wildcard pattern, so maybe it's OK to extend its meaning for this use case as well, but ... strikes me as a closer fit in terms of established semantics.

  81. nekitdev commented on Jul 23, 2022

    @nekitdev

    Alright, I agree that ... might be nicer (and cuter, for that matter). Back to implementing Functor, though:

    from attrs import frozen
    
    @frozen()
    class Option[O[...]](Functor[T]):
        value: Optional[T]
    
        def map[U](self: O[T], function: Callable[[T], U]) -> O[U]:
            value = self.value
    
            if value is None:
                return self
    
            return type(self)(function(value))  # preserve types, allowing to inherit from `Option[T]`

    I feel like this should make sense here.

  82. joelberkeley commented on Jul 23, 2022

    @joelberkeley

    That's an interesting problem: Scala and Haskell don't have subclassing of concrete classes. Do any languages have that and higher-kinded types? What about higher-kinded types and covariance? Maybe it's no different to interface inheritance with HKTs

    BTW I don't think you'd parametrize over O[...] in Option. There would be a number of problems with that, e.g. how would you type hint it (Option[Option[Option ad infinitum], int])? You might drop O[...] in that line and use Self but then given that might you use Self in Functor? How does Self fit into all this?

    also are PEP details on topic for this thread? I don't want to spam people

  83. nekitdev commented on Jul 23, 2022

    @nekitdev

    On that idea, we could throw things away all together and allow parametrizing Self instead.

  84. nekitdev commented on Jul 23, 2022

    @nekitdev

    For instance, the Functor would be as follows:

    class Functor[T](Protocol):
        def map[U](self, function: Callable[[T], U]) -> Self[U]:
            ...
    
    
    @frozen()
    class Option(Functor[T]):
        value: Optional[T] = None
    
        def map[U](self, function: Callable[[T], U]) -> Self[U]:
            value = self.value
    
            if value is None:
                return self
    
            return type(self)(value)
  85. Gobot1234 commented on Jul 23, 2022

    @Gobot1234
    Contributor

    I think being able to parameterise Self only solves part of the problem, it would still leave functions like all untypeable

  86. nekitdev commented on Jul 23, 2022

    @nekitdev

    Could you by any chance give an example?

  87. joelberkeley commented on Jul 23, 2022

    @joelberkeley

    Anywhere Self isn't defined, like a free function

    def map[F[...]: Functor, A, B](x: F[A], f: Callable[[A], B]) -> F[B]:
        return x.map(f)
  88. Gobot1234 commented on Jul 23, 2022

    @Gobot1234
    Contributor

    So I was thinking this would be useful to rewrite all to look like

    class AlwaysFalsey(Protocol):
        def __bool__(self) -> Literal[False]: ...
    
    I = TypeVar("I", bound=Iterable[T])
    def all(x: I[T | AlwaysFalsey]) -> TypeGuard[I[T]]: ...

    I think there are more examples where having this in functions is useful, so if you were going to go the effort of allowing Self to be subscripted (which in most type checkers is equivalent to a bound TypeVar) I don't see the point in not adding this.

  89. nekitdev commented on Jul 23, 2022

    @nekitdev

    Hm, indeed, if we want to bring the full power of HKTs, we can't limit it so these examples won't work.

  90. nekitdev commented on Jul 23, 2022

    @nekitdev

    The ultimate solution I can think of is parametrizing bounded TypeVars, even though it doesn't exactly go nicely with PEP 695's attitude.

  91. nekitdev commented on Jul 24, 2022

    @nekitdev

    So the syntax would be something like

    type Unary[T, U] = Callable[[T], U]
    
    
    def map_magical[I: Iterable[T], T, U](function: Unary[T, U], iterable: I[T]) -> I[U]:
        return type(iterable)(map(function, iterable))
    
    
    # NOTE: type checkers would need to verify that
    # type(instance)(...) gives an actual new instance
    
    array = map_magical(str, [1, 2, 3])  # inferred `list[str]`

    @erictraut what do you think?

  92. johnynek commented on Jul 24, 2022

    @johnynek

    That's an interesting problem: Scala and Haskell don't have subclassing of concrete classes. Do any languages have that and higher-kinded types? What about higher-kinded types and covariance? Maybe it's no different to interface inheritance with HKTs

    Just a note: Scala does permit subclasses of concrete classes.

  93. erictraut commented on Jul 24, 2022

    @erictraut
    Collaborator

    That syntax looks reasonable. The approach wouldn't work with the current (prototype) implementation of PEP 695 because the compiler constructs the TypeVars for a generic class, function or type alias prior to evaluating any expressions that use those TypeVars. In your example, the upper bound expression for TypeVar I references TypeVar T, which hasn't yet been allocated. If we wanted to support this, the compiler would need to allocate all of the TypeVars prior to evaluating their upper bound expression, then bind the objects to their corresponding names, and then evaluate their bound expressions and "inject" the result into the pre-constructed TypeVar objects. A bit messy, but doable.

  94. nekitdev commented on Jul 24, 2022

    @nekitdev

    I see, if we want to keep the allocation simpler, perhaps the following order could be required:

    [T, I[T], U]

    (still, this will probably introduce some additional things needed for the compiler to do)

    Allocating all of them is also an option, which imo is a bit closer to several-step compilers, in some sense.

    By the way, would this feature need its own PEP or could 695 introduce it as well?

  95. erictraut commented on Jul 24, 2022

    @erictraut
    Collaborator

    HKT support would definitely require its own PEP. It shouldn't be tied to PEP 695 other than to make sure that the two compose and there's a plan for how they work together if both are accepted. HKT support will require significant spec'ing, refining, socializing, and prototyping.

  96. nekitdev commented on Jul 24, 2022

    @nekitdev

    I'm open to work on such a PEP, but it definitely needs lots of discussions on its specification and implementation. Honestly, 696 seems like a nice number for it, either way! :)

  97. Gobot1234 commented on Jul 24, 2022

    @Gobot1234
    Contributor

    I'm open to work on such a PEP, but it definitely needs lots of discussions on its specification and implementation. Honestly, 696 seems like a nice number for it, either way! :)

    696 is already taken by yours truly 😉

  98. JelleZijlstra commented on Jul 24, 2022

    @JelleZijlstra
    Member

    Right, 696 is going to be typevar defaults. I may be willing to sponsor a PEP for HKT, but given the complexity of the feature I'd like to see a prototype in a major type checker first, as well as a well-specified draft PEP.

  99. nekitdev commented on Jul 24, 2022

    @nekitdev

    Alright, I'm going to work on the PEP along with the implementation in mypy (though contributors are definitely welcome, haha) type checker.

  100. AlexWaygood commented on Jul 24, 2022

    @AlexWaygood
    Member

    I'd be happy to help co-write or proofread a HKT PEP. It's a feature I'm quite interested in.

    I agree with the other voices in this thread, however, that this would be a significant new feature with a high degree of complexity, that we'd need to take a large degree of care over.

  101. nekitdev commented on Jul 24, 2022

    @nekitdev

    Okay, I'm going to fork peps repository and add anyone willing to participate as contributors. For discussions, we can either use GitHub Discussions or, alternatively, one can contact me on Discord at nekitdev#3556 (link to my server in case my name changes).

  102. jab commented on Jul 25, 2022

    @jab

    Thank you @nekitdev, @AlexWaygood, @JelleZijlstra, @erictraut, et al., this is exciting progress! If the bidict example should make it into the PEP as one of the motivating examples and there's anything I can do to help with that, please don't hesitate to let me know. And likewise let me know if there's anything I can do to help with testing any mypy or pyright reference implementations of this against a version of bidict that makes use of the new feature.

  103. mscuthbert commented on Dec 8, 2022

    @mscuthbert

    It looks like the work on the new PEP has gotten a little stalled?

    Just to give yet another example, music21--the most used library for processing musical scores--has both a number (n) of different ways of iterating across musical scores (parts first, position first, etc.) all of which are subclasses of the main StreamIterator class and use the same iter method, and then a number (m) of different ways of filtering which objects are returned (notes only, metadata only, custom classes, etc.) almost all of which all subclasses of the same StreamFilter class and use the same filter function. As I've been working to extensively type the library, it means that I've had to go from 2 methods that work well together to min(m, n) functions to get the correct typing. (To make matters worse, there are 3 @overload versions of each method). The lack of HKT is preventing me from wanting to implement new iteration and filtering methods in the next version. It would be great to get something like the args=1 syntax working together with the PEP695 proposals. Happy to help with the writing, though most implementation details might be beyond my understanding of the corner-cases of Functor world.

  104. nekitdev commented on Dec 9, 2022

    @nekitdev

    The new PEP was indeed stalled for a while. I'll try to work on it more in the future, though I'm quite busy with my university atm. Thanks for providing another example, though! Will definitely consider adding it to the PEP.

  105. nekitdev commented on Dec 10, 2022

    @nekitdev

    So, just to be sure, below is what syntax I'm currently planning.

    In classes (and their methods), without PEP 695:

    from typing import Callable, Generic, TypeVar
    
    from attrs import frozen
    
    T = TypeVar("T", covariant=True)
    U = TypeVar("U")
    
    
    @frozen()
    class Option(Generic[T]):
        value: T
    
        def map(self: Self[T], function: Callable[[T], U]) -> Self[U]:
            return type(self)(function(self.value))

    In functions, without PEP 695:

    from typing import Callable, Iterable, TypeVar
    
    T = TypeVar("T")
    U = TypeVar("U")
    
    I = TypeVar("I", bound=Iterable[T], args=1)
    
    
    def map_iterable(function: Callable[[T], U], iterable: I[T]) -> I[U]:
        return type(iterable)(map(function, iterable))

    In classes (and their methods), with PEP 695:

    from attrs import frozen
    
    type Unary[T, U] = Callable[[T], U]  # extra funky!
    
    @frozen()
    class Option[T]:
        value: T
    
        def map[U](self: Self[T], function: Unary[T, U]) -> Self[U]:
            return type(self)(function(self.value))

    In functions, with PEP 695:

    type Unary[T, U] = Callable[[T], U]
    
    
    def map_iterable[I: Iterable[T], T, U](function: Unary[T, U], iterable: I[T]) -> I[U]:
        return type(iterable)(map(function, iterable))

    PEP 695 authors may need to consider implementing functionality
    that would count the number of type arguments (args) based on the definition, for instance:

    type args
    Iterable[T] 1
    Dict[Q, T] 2
    Tuple[T, U, V] 3

    It is worth noting that that definitions like Dict[Q] should be rejected by type checkers because of missing arguments.

    Also there should be a change in how type checkers treat generics in expressions like type(item)(...).

  106. mscuthbert commented on Dec 11, 2022

    @mscuthbert

    What is "Unary" signifying in this case? and is attrs necessary to explain the example (for those of us who don't use it, it seems to obfuscate rather than explain. -- does "classes with PEP 695" work just as well if written as:

    @dataclass
    class Option[T]:
        value: T
    
        def map[U](self: Self[T], function: Callable[[T], U]) -> Self[U]:
            return type(self)(function(self.value))

    I like this, but I can see the objection that Self already implies "Option[T]" so "Self[T]" looks like "Option[T][T]" what about considering something like this if objections are raised:

    @dataclass
    class Option[T]:
        value: T
    
        def map[U](self: Self, function: Callable[[T], U]) -> HigherKind[Self][U]:  # or HigherKind[Self, [U]]
            return type(self)(function(self.value))

    other terms could be NonGeneric[Self][U] (if the first syntax is allowed) or ChangedGeneric[Self, [U]] (for the second). I sort of feel that the process of stripping and replacing the GenericType should be explicit.

  107. joelberkeley commented on Dec 11, 2022

    @joelberkeley

    I agree about using attrs. Would much prefer keeping to the stdlib.

    Can I also suggest avoiding calling classes (such as type(self)(...)) in the examples because I'm almost certain that's a very significant hole in Python's type system that could cause confusion when defining the PEP?

    I'm interested in helping with the PEP but need to see how much time I have

  108. nekitdev commented on Dec 12, 2022

    @nekitdev

    I agree with skipping the attrs usage in favor of the standard library. I'm simply too used to the library :p
    As for the type(self)(...), it's arguably the reason the PEP exists — to note that we want to be generic over type constructors.

  109. joelberkeley commented on Dec 13, 2022

    @joelberkeley

    As for the type(self)(...), it's arguably the reason the PEP exists

    I don't follow. The signature of type(self)(...) is defined by the __init__ method, which can be essentially anything. The "type contructor"-ness is the application of the class as a type-level thing to types e.g. type -> type or Foo[int], rather than the application of the class as an object constructor int -> Foo or Foo(1), or indeed int -> bool -> str -> Foo and Foo(0, True, "").

    The hole I'm referring to is (and I may get this example wrong first time as I'll try to use your example rather than how i'm used to presenting it)

    @dataclass
    class Foo(Option[T]):
        bar: int

    which in python typing is valid, but map but will TypeError due to an incorrect number of arguments. Where would the type checker raise an error there? My guess is it has to be at type(self)(...) because who knows what signature type(self) will have once subclassed.

  110. Gobot1234 commented on Dec 13, 2022

    @Gobot1234
    Contributor

    I'd like to share one of my use cases for HKT which I don't think has really been considered

    @dataclass
    class Item[OwnerT: BaseUser]: 
        id: int
        owner: OwnerT
    
    @dataclass
    class Trade[ItemT: Item, OwnerT: BaseUser]:
        sending: Sequence[ItemT]  # from our inventory
        receiving: Sequence[ItemT]  # from their inventory

    Item's are in a user's inventory and you can't send trades for items that are in another user's inventory. A common error that I see could be solved with HKT is swapping around the sending and receiving arguments and HKT could enforce at type checking time that you don't have this issue. The code would look something like

    @dataclass
    class Trade[ItemT: Item, OwnerT: BaseUser]:
        sending: Sequence[ItemT[OurUser]]
        receiving: Sequence[ItemT[OtherUser]]

    then using it

    bad_trade = Trade(receiving=our_inventory.items)  # error: list[Item[OurUser]] cannot be assigned to list[Item[OtherUser]]

    PEP 695 authors may need to consider implementing functionality that would count the number of type arguments (args) based on the definition, for instance:

    type args
    Iterable[T] 1
    Dict[Q, T] 2
    Tuple[T, U, V] 3
    It is worth noting that that definitions like Dict[Q] should be rejected by type checkers because of missing arguments.

    Just chiming in here, I think this is a bad idea (for now) for multiple reasons.

    1. it would increase the complexity of an already very complex PEP.
    2. PEP 696 would make Dict[Q] a valid subscription at runtime and it'd be perfectly valid at type checking time unless you enabled some form of missing type parameter check (which is normally gated behind strict mode).

    Other than that I really like the syntax and can't wait for this to be implemented, keep up the good work :)

  111. schuelermine commented on Dec 15, 2022

    @schuelermine

    I would find this proposal very useful.

  112. parched commented on Dec 16, 2022

    @parched

    The original comment mentions Scala and Haskell reference implementations, but what I really want is just generic bounds like even the mainstream Java

    public class A<T> {
    	public T t;
    };
    
    public class B<R extends A<?>> {
    	public R r;
    };

    and C# have

    public class A<T> {
    	public T t;
    };
    
    public class B<R, S> where R : A<S> {
    	public R r;
    };
  113. joelberkeley commented on Dec 16, 2022

    @joelberkeley

    I agree that would be useful, but I don't believe it's higher-kinded types. There are no typevars that take another typevar there. Your example is rather a limitation on how python does typevar bounds. It's similar to this SO post of mine.

    What might be confusing here is that the title says "Higher-Kinded TypeVars", but the description says "aka type constructors, generic TypeVars". I don't think this is technically correct. Higher kinded types aren't the same as type constructors. Python already has type constructors e.g. list, a type -> type. A higher-kinded type is a type that abstracts over a type constructor. We'll need to be clear about this in the PEP

  114. parched commented on Dec 16, 2022

    @parched

    Ok, would it be worth having a separate PEP just for generic bounds? It looks like most of the Mypy issues that link here are actually just asking for generic bounds not HKT. This one also. I wonder how many of the 👍 on this issue are just for generic bounds.

  115. schuelermine commented on Dec 16, 2022

    @schuelermine

    You linked my issue, so I’d like to chime in again: I believe my issue is a different thing than this issue, as stated by @joelberkeley. I also voiced my approval of this issue before. I want to make clear that this was not mistaken, I want both to be implemented.

  116. qexat commented on Apr 10, 2023

    @qexat

    It's been more than 5 years since this issue was opened. What is its current state? I'm very interested, because I've faced more than one situation where HKTs were required to properly annotate the code I've written. Thank you in advance to anyone who will enlighten me.

  117. Gobot1234 commented on Apr 10, 2023

    @Gobot1234
    Contributor

    It's been more than 5 years since this issue was opened. What is its current state? I'm very interested, because I've faced more than one situation where HKTs were required to properly annotate the code I've written. Thank you in advance to anyone who will enlighten me.

    If you want to help, you might be able to contribute to https://github.com/nekitdev/peps

  118. mscuthbert commented on Apr 12, 2023

    @mscuthbert

    If you want to help, you might be able to contribute to https://github.com/nekitdev/peps

    Specifically the file there is pep-9999.rst

  119. Asaurus1 commented on Sep 23, 2023

    @Asaurus1

    Ok, would it be worth having a separate PEP just for generic bounds? It looks like most of the Mypy issues that link here are actually just asking for generic bounds not HKT. This one also. I wonder how many of the 👍 on this issue are just for generic bounds.

    +1 here for generic bounds. I am running into issues with a project right now that would be easily solved with them.

  120. vnmabus commented on Nov 16, 2023

    @vnmabus

    I just wanted to say that until this is fixed, it would be impossible to type any library that makes use of the array API standard and has functions that are generic not only on the array type, but also on the dtype or shape parameters. E.g.:

    def astype[A: ArrayProtocol, D: DTypeProtocol](array: A[Any], dtype: D) -> A[D]:
        ...

    What I try to say is that this is not a marginal use case, and will likely become more ubiquitous as more array-related libraries start following the standard.

  121. smheidrich commented on Dec 19, 2023

    @smheidrich
    Contributor

    I don't know if everyone saw, but generic TypeVar bounds have a(nother?) dedicated issue at #1226 and have also been implemented in Mypy fork basedmypy. Maybe some of the people who only need generic TypeVar bounds and not the full power of HKTs¹ want to get involved there.

    ¹ Roughly, as far as I understand it: When you only want to annotate some things as T[U] while referring to U by itself elsewhere in the same signature, but don't need to have a different parametrization T[W] in it as well (T, U and W all being TypeVars and T having a generic bound).

  122. rianhunter commented on Dec 21, 2023

    @rianhunter

    +1 for this feature. I can do this easily in C++ or Haskell:

    A = TypeVar('A')
    class Baz(Protocol[A]):
      pass
    
    B = TypeVar('B', bound=Baz[A]) # <- throws unbound typevar error here
    
    class Foo[B]:
      def bar(self) -> B:
        ...

    If there is another way to do this that's currently supported that would be great.

  123. omer54463 commented on Jan 10, 2024

    @omer54463

    According to PEP-695, which proposed the new (3.12) generics API:

    The specified constrained types must be concrete. An attempt to use a generic type should be flagged as an error by a type checker. This is consistent with the existing rules enforced by type checkers for a TypeVar constructor call.

    Any idea why? Generic bound types seem absolutely logical (I even assumed it was possible before trying it and getting disappointed).
    The reason it isn't supported yet might be relevant here.

  124. erictraut commented on Jan 10, 2024

    @erictraut
    Collaborator

    Any idea why?

    PEP 695 was intended to provide a new syntax for existing mainstream typing features. The PEP was already quite large and involved (with nearly a year of planning that went into it). It didn't make sense to introduce new orthogonal features as part of the PEP.

    Almost all modern languages support generics, but very few support higher-kinded types. HKTs are an advanced (and somewhat esoteric) capability that can be useful in certain situations, but they add significant complexity to the type system.

  125. omer54463 commented on Jan 11, 2024

    @omer54463

    Almost all modern languages support generics, but very few support higher-kinded types.

    Correct me if I'm wrong, but the example by @rianhunter should be easily achievable in most languages that support generics.
    Is it a simple, specific case of HKT? Because according to references to this issue from other issues, that's most of the demand.

  126. antonagestam commented on Jan 11, 2024

    @antonagestam

    @omer54463 I don't know what would be specific about it, but it's also hard to tell what the expectations are. If the expectation is for the return type of Foo.bar to somehow vary over A, it's not clear to me with the syntax given how it's expected to vary. In my opinion it's an incomplete example because it doesn't show what the expectations are on usage of these classes.

    Perhaps it would be useful to instead provide an actual working piece of C++ to show the capability you mean to compare with?

  127. omer54463 commented on Jan 13, 2024

    @omer54463

    @antonagestam Sure!

    Here is a simple C++ program to demonstrate the issue:

    #include <concepts>
    
    template <typename I, typename T>
    concept Interface = requires(I interface) {
      { interface.interface_method() } -> std::same_as<T>;
    };
    
    template <typename T, Interface<T> I> class Container {
    public:
      Container(I interface) : interface(interface) {}
      I &get() { return this->interface; }
    
    private:
      I interface;
    };
    
    class Concrete {
    public:
      Concrete() {}
      int interface_method() { return 0; }
      int non_interface_method() { return 0; }
    };
    
    int main(void) {
      auto container = Container<int, Concrete>(Concrete());
      container.get().interface_method();
      container.get().non_interface_method(); // Works!
      return 0;
    }

    The call to non_interface_method works because we don't just have T = int - we have I = Counter<int> too.
    This way, the member's type, Counter<int>, is fully known.
    This isn't possible in python:

    from typing import Generic, Protocol, TypeVar
    
    T = TypeVar("T")
    
    class Interface(Protocol[T]):
        def interface_method() -> T:
            raise NotImplementedError
    
    T = TypeVar("T")
    
    class Container(Generic[T]):
        interface: Interface[T]
    
        def __init__(self, interface: Interface[T]) -> None:
            self.interface = interface
    
        def get(self) -> Interface[T]:
            return self.interface
    
    class Concrete:
        def interface_method() -> int:
            return 0
    
        def non_interface_method() -> int:
            return 0
    
    def main() -> None:
        container = Container(Concrete())
        container.get().interface_method()
        container.get().non_interface_method()   # Unrecognized!
    
    if __name__ == "__main__":
        main()

    container.get() returns Interface[int], and not the specific type that implements Interface[int], so we don't have any methods except for the interface.
    It should be possible if we allow something like this:

    ...
    T = TypeVar("T")
    I = TypeVar("I", bound=Interface[T])
    
    class Container(Generic[T, I[T]]):
        interface: I[T]
    ...
  128. omer54463 commented on Jan 13, 2024

    @omer54463

    In addition, even Typevar("I", bound=Interface[int]) is unallowed!
    This is a very weird restriction - there is nothing generic about Interface[int] - all of the parameters are resolved.

  129. antonagestam commented on Jan 13, 2024

    @antonagestam

    @omer54463 With some minor changes this code passes mypy: https://mypy-play.net/?mypy=latest&python=3.12&gist=e88f88086056aa61c8d8d2c3c0678d8a. Note that it respects the specialization in the TypeVar.

  130. omer54463 commented on Jan 14, 2024

    @omer54463

    @antonagestam Woah, it seems my second comment is incorrect. I had no idea.
    I saw your comment and literally got out of bed to check it out.

    Unfortunately, TypeVar("I", bound=Interface[T]) or any similar trick I could think of still doesn't work.
    For example:

    from typing import Generic, Protocol, TypeVar
    
    T = TypeVar("T", covariant=True)
    
    
    class Interface(Protocol[T]):
        def interface_method(self) -> T:
            raise NotImplementedError
    
    
    I = TypeVar("I", bound=Interface[T])  # error: Type variable "example.T" is unbound
    
    
    class Container(Generic[T, I]):
        interface: I
    
        def __init__(self, interface: I) -> None:
            self.interface = interface
    
        def get(self) -> I:
            return self.interface
    
        def interface_method(self) -> T:
            return self.interface.interface_method()
    
    
    class Concrete:
        def interface_method(self) -> int:
            return 0
    
    
    def main() -> None:
        # Works great! Everyone's happy!
        container_1 = Container[int, Concrete](Concrete())
        container_1.interface_method()  # -> int
        container_1.get().interface_method()  # -> int
    
        # WTF! T and I have nothing to do with eachother!
        container_2 = Container[str, Concrete](Concrete())
        container_2.interface_method()  # -> str
        container_2.get().interface_method()  # -> int
    
    
    if __name__ == "__main__":
        main()

    Is there any complicated reason for it to not be supported? Any HKT rabbit hole we must end up going down to solve all edge cases?

  131. sebastian-philipp commented on Jan 15, 2024

    @sebastian-philipp
  132. rianhunter commented on Jan 15, 2024

    @rianhunter

    @rianhunter , I think, we're close already: https://mypy-play.net/?mypy=latest&python=3.12&gist=e88f88086056aa61c8d8d2c3c0678d8a

    No, you can bind Protocol[concrete_type] to a type var but not Protocol[type_var]

  133. jorenham commented on Jan 15, 2024

    @jorenham
    Collaborator

    Like @sobolevn mentioned almost 4 years ago, the closest you can get to a solution, is by using the approach that's used in python-returns:

    Their approach to e.g.

    from typing import TypeVar
    from returns.interfaces.container import Container1
    
    T = TypeVar('T', bound=Container1)
    
    def all_to_str(arg: T[int]) -> T[str]:
        ...

    is to wrap everythin in "containers" (i.e. monads, but don't say that out loud, monads must remain esoteric for some reason):

    >>> from returns.primitives.hkt import Kind1, kinded
    >>> from returns.interfaces.container import ContainerN
    >>> from typing import TypeVar
    
    >>> T = TypeVar('T', bound=ContainerN)
    
    >>> @kinded
    ... def to_str(container: Kind1[T, int]) -> Kind1[T, str]:
    ...     return container.map(str)

    However, this requires a mypy plugin to work, so it almost surely will not work with e.g. pyright.

    See https://returns.readthedocs.io/en/latest/pages/hkt.html for more info.

  134. omer54463 commented on Jan 16, 2024

    @omer54463

    Like @sobolevn mentioned almost 4 years ago, the closest you can get to a solution, is by using the approach that's used in python-returns:

    It would work. It's also pretty ugly and counterintuitive. I really don't want to introduce returns into an existing company's codebase out of fear of the termination of my employment.

    I'm suggesting making the syntax I mentioned a supported feature - is this not an appropriate place for that?

  135. jorenham commented on Jan 16, 2024

    @jorenham
    Collaborator

    @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 typing.TypeVar, instead of the new PEP 695 syntax.

    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.

  136. omer54463 commented on Jan 18, 2024

    @omer54463

    @jorenham I roughly agree about the syntax.
    However, there is still a distinction I want to make:

    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.
    T can be any type, not all types receive type arguments.
    I am not saying this could not be checked and supported, but it's a much bigger feature.

    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.

  137. jorenham commented on Jan 18, 2024

    @jorenham
    Collaborator

    @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 TypeVar syntax here, is just an illustrative example I came up with from the top of my head.


    In your last example, P denotes the parameter that is bound to some kind of higher type, so I'd consider its use in f: P[T] -> ? as HKT.

  138. jorenham commented on Jan 18, 2024

    @jorenham
    Collaborator

    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.
    I can think of two "explicit notations":

    • map_async[R, T: Awaitable[V]](fn: Maps1[V, R], aval: T[V]): T[R] - V occurs in both fn and aval, but is bound to V@Awaitable
    • map_async[V, R, T: Awaitable[V]](fn: Maps1[V, R], aval: T[V]): T[R] - V is bound to V@Awaitable and to itself, implying equivalence of T[V] and T[R], but V and R need not be equivalent

    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. map_async[V, R, T: Awaitable](fn: Maps1[V, R], aval: T[V]) -> T[R]).
    As far as I can tell, it doesn't have any of the problems that the explicit variant has.
    Additionally, it has the benefit that there is only one obvious to do it (and coincidentally, I'm dutch).

  139. omer54463 commented on Jan 19, 2024

    @omer54463

    We can support an explicit syntax similar to the ones you mentioned without contradictions.

    1. If we have [X, Y, T: Awaitable], we can use T[X], T[Y], and so on. Let's call this "implicit".
    2. If we have [X, T: Awaitable[X]] we can use T, but T[X] makes no sense anymore. Let's call this "explicit".

    The signature of the function you gave would then be either:

    1. def map_async[V, R, A: Awaitable](fn: Maps1[V, R], aval: A[V]) -> A[R]: ... - implicit, same Awaitable.
    2. def map_async[V, R, AV: Awaitable, AR: Awaitable](fn: Maps1[V, R], aval: AV[V]) -> AR[R]: ... - implicit, different Awaitable.
    3. def map_async[V, R, AV: Awaitable[V], AR: Awaitable[R]](fn: Maps1[V, R], aval: AV) -> AR: ... - explicit, different Awaitable.

    Though I am not sure "explicit" is the right term here.

  140. jorenham commented on Jan 19, 2024

    @jorenham
    Collaborator

    @omer54463 Your 2nd and 3rd signature specifications with 4 type parameters are invalid.
    To see why, consider the map_async(str, asyncio.Task[int]) example:

    • Maps1[V, R] matches str: Any -> str
      • V binds to Any
      • R binds to str
    • AV[V] binds to asyncio.Task[V]
      • V binds to int, because and int <: Any
    • AR[R] is narrowed to AR[str]
    • AR cannout be bound

    So a type checker has no way of inferring the type of AR, unless you consider Awaitable to be its default, in which case it will always return an Awaitable[R], which defeats the purpose of HKT in this example.

  141. smheidrich commented on Feb 9, 2024

    @smheidrich
    Contributor

    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:

    • @JelleZijlstra has indicated interest in sponsoring a PEP, conditional on a prototype implementation in a major type checker and a well-specified draft PEP.
    • Drafting the PEP takes place in @nekitdev's fork of the peps repo. The stub PEP draft so far contains a few examples of the proposed syntax.
    • That same repo's GitHub Discussions forum forum has been designated as the place to discuss the PEP (and presumably the prototype implementation?). Some limited further discussions have taken place there.
      • If you want to be notified of new discussion threads, I think you have to set the whole repo as "watched" in GitHub?

    Maybe @tek or someone else who is allowed to edit it can put this in the original issue so new arrivals find it quickly?

  142. JelleZijlstra commented on Feb 9, 2024

    @JelleZijlstra
    Member

    Good point, I'll put this summary in the top post.

  143. Badg commented on Mar 5, 2024

    @Badg
    • Drafting the PEP takes place in @nekitdev's fork of the peps repo. The stub PEP draft so far contains a few examples of the proposed syntax.

    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 Builder, and may then themselves be subclassed further. Each of the Builder subclasses define their own required metadata keys, but the PresentationVariantInfo is generic with respect to the actual Builder subclass. Simplifying a lot, it looks something like this:

    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
  144. nekitdev commented on Mar 5, 2024

    @nekitdev

    Hey! Yeah, sorry, I messed up sync with the upstream so had to reclone. I'll push the draft shortly.
    Edit: here is the updated link.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    topic: featureDiscussions about new features for Python's type annotations

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions