diff --git a/pep-9999.rst b/pep-9999.rst new file mode 100644 index 00000000000..22fe2837e97 --- /dev/null +++ b/pep-9999.rst @@ -0,0 +1,212 @@ +PEP: 9999 +Title: Inline variance operators for generic type variables: ``+T`` and ``-T`` +Author: Joren Hammudoglu +Status: Draft +Type: Standards Track +Content-Type: text/x-rst +Created: 24-Jul-2021 +Post-History: 24-Jul-2021 + + +Abstract +======== + +PEP 484 proposes a syntax for defining the `covariance and contravariance +`_ +of `user-defined generic types +`_:: + + T_co = typing.TypeVar('T_co', covariant=True) + T_contra = typing.TypeVar('T_contra', contravariant=True) + + + # a covariant generic type + class EventDispatcher(typing.Generic[T_co]): + def dispatch() -> T_co: ... + + # a contravariant generic type + class EventListener(typing.Generic[T_contra]): + def receive(event: T_contra) -> None: ... + + +This PEP proposes a concise syntax for specifying the variance of +generic types by overloading the ``+`` and ``-`` prefix operators +on type variables:: + + T = typing.TypeVar('T') + + + # a covariant generic type + class EventDispatcher(typing.Generic[+T]): + def dispatch() -> T: ... + + # a contravariant generic type + class EventListener(typing.Generic[-T]): + def receive(event: T) -> None: ... + + + +Rationale +========= + +Although defining the variance of generic type by using type variables +that are constructed with the ``covariant=True`` or ``contravariant=True`` +keyword arguments works well enough, it has downsides: + +- In PEP 484 it states: + + Covariance or contravariance is not a property of a type variable, + but a property of a generic class defined using this variable. + + This is why type checkers do no allow using co- or contravariant type + variables outside of generic classes. + So, instead of defining variance on the type variable itself, it + would make more sense to mark the type variable's variance when + defining the generic type, and only there. + +- Because variance is a property of the generic type itself, type + checkers do not allow using co- or contravariant type variables + outside of generic classes. But as the variance is currently a + property of the type variables, they must be used within generic + classes. + +- Creating separate type variables for different variances often + results in up to 3x code duplication, especially for type variables + with constraints or an upper bound. + + +Proposal +======== + + +All of these issues can be solved by overloading the ``+`` and ``-`` +prefix operators of ``typing.TypeVar`` instances, so that ``+T`` is an +alias for a covariant type variable, and ``-T`` for a contravariant +one. These variance operators should only be used within the parameter +list of ``typing.Generic`` and ``typing.Protocol`` when subclassing them. + +This syntax is practically identical to type parameter variance in the +Scala programming language [1]_. + + + +Specification +============= + +The new type variable variance syntax should be used within the generic +parameter list of ``typing.Generic`` or ``typing.Protocol`` base classes, +and only there. It is not allowed to be used on type variables that +are already marked as covariant or contravariant, either with +``covariant=True``, ``contravariant=True`` or with the new ``+T`` or +``-T`` syntax. + +Simplified Syntax +----------------- + +Instead of specifying the variance on the typevar itself:: + + T_co = typing.TypeVar('T_co', covariant=True) + T_contra = typing.TypeVar('T_contra', contravariant=True) + + class SupportsInvertOld(typing.Protocol[T_co]): + def __invert__(self) -> T_co: ... + + class SupportsPartialOrderOld(typing.Protocol[T_contra]): + def __le__(self, other: T_contra): ... + + +The new syntax uses the ``+`` and ``-`` prefix operators to specify +covariant and contravariant generic types inline:: + + T = typing.TypeVar('T') + + class SupportsInvert(typing.Protocol[+T]): + def __invert__(self) -> T: ... + + class SupportsPartialOrder(typing.Protocol[-T]): + def __le__(self, other: T): ... + + +Differences with current syntax +------------------------------- + +The new typevar operators return a transparent wrapper around the +original type variable, which can be accessed with the ``__origin__`` +attribute on the returned wrapper. e.g.:: + + (+T).__origin__ is T + (+T).__covariant__ is True + (+T).__contravariant__ is False + (+T).__name__ == T.__name__ + (+T).__constraints__ == T.__constraints__ + (+T).__bound__ is T.__bound__ + + +Thus, type variables defined with ``covariant=True`` and +``contravariant=True``, are not equivalent to ``+T`` and ``-T``. + + +``+T`` and ``-T`` are not valid type annotations, and should only be +used within the generic type parameter list of ``typing.Generic`` +or ``typing.Protocol``. e.g.:: + + class Spam(typing.Generic[+KT]): ... + class Eggs(typing.Protocol[-KT, +VT]): ... + class HamSet(typing.Sequence[T], typing.Generic[+T]): ... + +are valid uses. + + +All variance rules that apply to user-defined classes should apply +in the same way with the new syntax, as they do with the current syntax, +and vice-versa. + + + +Rejected Ideas +============== + +For more detauls about discussions, see links below: + +- `Discussion in python/typing `_ + +1. Using ``T_co = +TypeVar('T_co')`` instead of ``T_co = TypeVar('T_co', covariant=True)`` +------------------------------------------------------------------------------------------ + +PROS: + +- This requires minimal changes to the syntax +- Replaces the need to type ``covariant=True`` or ``contravariant=True`` + with a concise operator. + + +CONS: + +- The ``+`` and ``-`` copy the type variable, but type variables + should be unique. +- It is not obvious what to do with the name of the type variable. +- Co- and contravariance are properties of the generic class, not of + the individual type variables. + + +References +========== + +.. [1] Scala Variance + https://docs.scala-lang.org/scala3/book/types-variance.html + + +Copyright +========= + +This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive. + + +.. + Local Variables: + mode: indented-text + indent-tabs-mode: nil + sentence-end-double-space: t + fill-column: 70 + coding: utf-8 + End: