Search code examples
pythonpython-typingmypyhigher-kinded-types

Abstracting over type constructors in Python via type annotations


I want to statically enforce that a method of a class returns a value wrapped in some abstract type, that I know nothing about:

E.g. given the abstract class

F = ???    

class ThingF(Generic[F]):
    @abstractmethod
    def action(self) -> F[Foo]:
        ...

I want to to be able to statically check that this is invalid:

class ThingI(ThingF[List]):
    def action(self) -> Foo:
        ...

because action does not return List[Foo].

However the above declaration for ThingF does not even run, because Generic expects its arguments to be type variables and I cannot find a way to make F a type variable "with a hole".

Both

F = TypeVar('F')

and

T = TypeVar('T')
F = Generic[T]

do not work, because either TypeVar is not subscriptable or Generic[~T] cannot be used as a type variable.

Basically what I want is a "higher kinded type variable", an abstraction of a type constructor, if you will. I.e. something that says "F can be any type that takes another type to produce a concrete type".

Is there any way to express this with Python's type annotations and have it statically checked with mypy?


Solution

  • You can use Higher Kinded Types with dry-python/returns. We ship both primitives and a custom mypy plugin to make it work.

    Here's an example with Mappable aka Functor:

    from typing import Callable, TypeVar
    
    from returns.interfaces.mappable import MappableN
    from returns.primitives.hkt import Kinded, KindN, kinded
    
    _FirstType = TypeVar('_FirstType')
    _SecondType = TypeVar('_SecondType')
    _ThirdType = TypeVar('_ThirdType')
    _UpdatedType = TypeVar('_UpdatedType')
    
    _MappableKind = TypeVar('_MappableKind', bound=MappableN)
    
    
    @kinded
    def map_(
        container: KindN[_MappableKind, _FirstType, _SecondType, _ThirdType],
        function: Callable[[_FirstType], _UpdatedType],
    ) -> KindN[_MappableKind, _UpdatedType, _SecondType, _ThirdType]:
        return container.map(function)
    

    It will work for any Mappable, examples:

    from returns.maybe import Maybe
    
    def test(arg: float) -> int:
            ...
    
    reveal_type(map_(Maybe.from_value(1.5), test))  # N: Revealed type is 'returns.maybe.Maybe[builtins.int]'
    

    And:

    from returns.result import Result
    
    def test(arg: float) -> int:
        ...
    
    x: Result[float, str]
    reveal_type(map_(x, test))  # N: Revealed type is 'returns.result.Result[builtins.int, builtins.str]'
    

    It surely has some limitations, like: it only works with a direct Kind subtypes and we need a separate alias of Kind1, Kind2, Kind3, etc. Because at the time mypy does not support variadic generics.

    Source: https://github.com/dry-python/returns/blob/master/returns/primitives/hkt.py Plugin: https://github.com/dry-python/returns/blob/master/returns/contrib/mypy/_features/kind.py

    Docs: https://returns.readthedocs.io/en/latest/pages/hkt.html

    Announcement post: https://sobolevn.me/2020/10/higher-kinded-types-in-python