PEP 646: Update based on typing-sig discussions (#1781)

* PEP 646: Simplify based on typing-sig discussions

Feature removals:
* Remove `Map` (to be added in a separate PEP)
* Remove support for all forms of concatenation other than simply prefixing of a single `TypeVarTuple` (fancier forms of concatenation to also be added in a separate PEP)

Semantic changes:
* Try switching `Ts` back to mean the 'unpacked' version (such that `Ts = int, str; Tuple[Ts] = Tuple[int, str]`) to see whether everything still works. (Might revert this, pending future discussion in typing-sig.)

* PEP 646: Clarify that prefixing is not allowed in Callable

* Update PEP 646 based on discussions in typing-sig

Semantic changes:
* Switch back to `Ts` meaning types packed in a `Tuple`
* Disallow unpacked usages of `Ts`
* Unparameterised `Tensor` behaves like `Tensor[Any, ...]`

Clarifications:
* `TypeVarTuple`s are invariant
* `TypeVarTuple`s can not be bound to unknown-length types (e.g. `Tuple[int, ...]`
* `TypeVarTuple`s can not be bound to unions of types
* Empty `*args` behaves like `Tuple[()]`
* `TypeVarTuple`s can not appear in slices

Readability changes:
* Reorder some sections for better flow

* Update PEP 646 with Eric's suggestions and other tweaks

Semantic changes:
* Revert to `*args: *Ts` for new behaviour, and `*args: Tuple[*Ts]` for old behaviour

Fixes:
* Unpacking in Aliases/Ideal Array Type sections
* Various typo/grammar fixes

Other:
* State explicitly that TypeVarTuples can be used in callback protocols
This commit is contained in:
Matthew Rahtz 2021-02-07 16:29:17 +00:00 committed by GitHub
parent 6f091c5969
commit 68794b15c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 321 additions and 500 deletions

View File

@ -1,4 +1,4 @@
PEP: 0646
PEP: 646
Title: Variadic Generics
Author: Mark Mendoza <mendoza.mark.a@gmail.com>,
Matthew Rahtz <mrahtz@google.com>,
@ -26,20 +26,6 @@ to catch shape-related bugs in code that uses these libraries.
Motivation
==========
There are two main use-cases for variadic generics. [#hkt]_
The primary motivation is to enable typing of array shapes in numerical
computing libraries such as NumPy and TensorFlow. This is the use-case
much of the PEP will focus on.
Additionally, variadic generics allow us to concisely specify the type
signature of ``map`` and ``zip``.
We discuss each of these motivations below.
Array Shapes
-------------
In the context of numerical computation with libraries such as NumPy and
TensorFlow, the *shape* of arguments is often just as important as the
argument *type*. For example, consider the following function which converts a
@ -87,56 +73,10 @@ and so on throughout their code) and for the authors of array libraries (who wou
Variadic generics are necessary for a ``Array`` that is generic in an arbitrary
number of axes to be cleanly defined as a single class.
``map`` and ``zip``
-------------------
PEP 612 [#pep-612]_ introduced ``ParamSpec``, enabling parameter types of one
callable to be forwarded to another callable. However, in many cases we actually
wish to *transform* parameter types before using them elsewhere in the
signature.
Consider, for example, the signature of ``map`` for a particular choice of
function and iterables:
::
def func(int, str) -> float: ...
iter1: List[int]
iter2: List[str]
def map(func: Callable[[int, str], float],
iter1: Iterable[int],
iter2: Iterable[str]) -> Iterable[float]: ...
Note that the number of iterables passed to ``map`` is dependent
on the supplied function, and that the types of those iterables
must correspond to the types of supplied function's arguments.
A similar example is ``zip``:
::
iter1: List[int]
iter2: List[str]
def zip(iter1: Iterable[int],
iter2: Iterable[str]) -> Iterable[Tuple[int, str]]: ...
Neither of these signatures can be specified in the general case using
existing typing mechanisms. The signature of ``zip``, for example, is
currently specified [#zip-sig]_ with a number of overloads.
Specification
=============
In order to support the above use-cases, we introduce:
* ``TypeVarTuple``, serving as a placeholder not for a single type but
for an *arbitrary* number of types, and behaving like a number of
``TypeVar`` instances packed in a ``Tuple``.
* A new use for the star operator: unpacking of each individual type
from a ``TypeVarTuple``.
* Two new type operators, ``Unpack`` and ``Map``.
In order to support the above use cases, we introduce ``TypeVarTuple``. This serves as a placeholder not for a single type but for an *arbitrary* number of types, and behaving like a number of ``TypeVar`` instances packed in a ``Tuple``.
These are described in detail below.
@ -155,130 +95,293 @@ Type variable tuples are created with:
Ts = TypeVarTuple('Ts')
A type variable tuple behaves in a similar way to a parameterized ``Tuple``.
For example, in a generic object instantiated with type parameters
``int`` and ``str``, ``Ts`` is equivalent to ``Tuple[int, str]``.
Type variable tuples behave like a number of individual type variables packed in a
``Tuple``. To understand this, consider the following example:
::
Shape = TypeVarTuple('Shape')
class Array(Generic[*Shape]): ...
Height = NewType('Height', int)
Width = NewType('Width', int)
x: Array[Height, Width] = Array()
The ``Shape`` type variable tuple here behaves like ``Tuple[T1, T2]``,
where ``T1`` and ``T2`` are type variables. To use these type variables
as type parameters of ``Array``, we must **unpack** the type variable tuple using
the star operator: ``*Shape``. The signature of ``Array`` then behaves
as if we had simply written ``class Array(Generic[T1, T2]): ...``.
In contrast to ``Generic[T1, T2]``, however, ``Generic[*Shape]`` allows
us to parameterise the class with an *arbitrary* number of type parameters.
That is, in addition to being able to define rank-2 arrays such as
``Array[Height, Width]``, we could also define rank-3 arrays, rank-4 arrays,
and so on:
::
Time = NewType('Time', int)
Batch = NewType('Batch', int)
y: Array[Batch, Height, Width] = Array()
z: Array[Time, Batch, Height, Width] = Array()
Type variable tuples can be used anywhere a normal ``TypeVar`` can.
For example, in class definitions, function signatures, and variable annotations:
This includes class definitions, as shown above, as well as function
signatures and variable annotations:
::
Shape = TypeTupleVar('Shape')
class Array(Generic[Shape]):
def __init__(self, shape: Shape):
self.shape: Shape = shape
def __abs__(self) -> Array[Shape]: ...
def __add__(self, other: Array[Shape]) -> Array[Shape]: ...
Height = NewType('Height', int)
Width = NewType('Width', int)
shape = (Height(480), Width(640))
x: Array[Tuple[Height, Width]] = Array(shape)
x.shape # Inferred type is Tuple[Height, Width]
y = abs(x) # Array[Tuple[Height, Width]]
z = x + y # Array[Tuple[Height, Width]]
Variance and ``bound``: Not (Yet) Supported
'''''''''''''''''''''''''''''''''''''''''''
To keep this PEP minimal, ``TypeTupleVar`` does not yet support
the ``bound`` argument or specification of variance, as ``TypeVar``
does. We leave the decision of how these arguments should be implemented
to a future PEP, when use-cases for variadic generics have been
explored more in practice.
Unpacking: Star Operator
''''''''''''''''''''''''
Note that the fully-parameterised type of ``Array`` above is
rather verbose. Wouldn't it be easier if we could just write
``Array[Height, Width]``?
To enable this, we introduce a new function for the star operator:
to 'unpack' type variable tuples. When unpacked, a type variable tuple
behaves as if its component types had been written
directly into the signature, rather than being wrapped in a ``Tuple``.
Rewriting the ``Array`` class using an unpacked type variable
tuple, we can instead write:
::
Shape = TypeTupleVar('Shape')
class Array(Generic[*Shape]):
def __init__(self, shape: Shape):
self.shape: Shape = shape
def __init__(self, shape: Tuple[*Shape]):
self._shape: Tuple[*Shape] = shape
def __add__(self, other: Array[*Shape]) -> Array[*Shape]: ...
def get_shape(self) -> Tuple[*Shape]:
return self._shape
def __abs__(self) -> Array[*Shape]: ...
def __add__(self, other: Array[*Shape]) -> Array[*Shape]) ...
shape = (Height(480), Width(640))
x: Array[Height, Width] = Array(shape)
x.shape # Inferred type is Tuple[Height, Width]
z = x + x # Array[Height, Width]
y = abs(x) # Inferred type is Array[Height, Width]
z = x + x # ... is Array[Height, Width]
Unpacking: ``Unpack`` Operator
''''''''''''''''''''''''''''''
Type Variable Tuples Must Always be Unpacked
''''''''''''''''''''''''''''''''''''''''''''
Because the new use of the star operator requires a syntax change and is
therefore incompatible with previous versions of Python, we also introduce the
``typing.Unpack`` type operator for use in existing versions of Python. ``Unpack``
takes a single type variable tuple argument, and behaves identically to the star
operator, but without requiring a syntax change. In any place you would normally
write ``*Ts``, you can also write ``Unpack[Ts]``.
Note that in the previous example, the ``shape`` argument to ``__init__``
was annotated as ``Tuple[*Shape]``. Why is this necessary - if ``Shape``
behaves like ``Tuple[T1, T2, ...]``, couldn't we have annotated the ``shape``
argument as ``Shape`` directly?
This is, in fact, deliberately not possible: type variable tuples must
*always* be used unpacked (that is, prefixed by the star operator). This is
for two reasons:
* To avoid potential confusion about whether to use a type variable tuple
in a packed or unpacked form ("Hmm, should I do ``-> Shape``,
or ``-> Tuple[Shape]``, or ``-> Tuple[*Shape]``...?")
* To improve readability: the star also functions as an explicit visual
indicator that the type variable tuple is not a normal type variable.
``Unpack`` for Backwards Compatibility
''''''''''''''''''''''''''''''''''''''
Note that the use of the star operator in this context requires a grammar change,
and is therefore available only in new versions of Python. To enable use of type
variable tuples in older versions of Python, we introduce the ``Unpack`` type
operator that can be used in place of the star operator to unpack type variable tuples:
::
# Unpacking using the star operator in new versions of Python
class Array(Generic[*Shape]): ...
# Unpacking using ``Unpack`` in older versions of Python
class Array(Generic[Unpack[Shape]]): ...
Variance, Type Constraints and Type Bounds: Not (Yet) Supported
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
To keep this PEP minimal, ``TypeVarTuple`` does not yet support specification of:
* Variance (e.g. ``TypeVar('T', covariant=True)``)
* Type constraints (``TypeVar('T', int, float)``)
* Type bounds (``TypeVar('T', bound=ParentClass)``)
We leave the decision of how these arguments should behave to a future PEP, when variadic generics have been tested in the field. As of this PEP, type variable tuples are
**invariant**.
Behaviour when Type Parameters are not Specified
''''''''''''''''''''''''''''''''''''''''''''''''
When a generic class parameterised by a type variable tuple is used without
any type parameters, it behaves as if its type parameters are ``Any, ...``
(an arbitrary number of ``Any``):
::
def takes_array_of_any_rank(arr: Array): ...
x: Array[Height, Width]
takes_array_of_any_rank(x) # Valid
y: Array[Time, Height, Width]
takes_array_of_any_rank(y) # Also valid
This enables gradual typing: existing functions with arguments annotated as being,
for example, a plain ``tf.Tensor``, will still be valid even if called with
a parameterised ``Tensor[Height, Width]``.
Type Variable Tuples Must Have Known Length
'''''''''''''''''''''''''''''''''''''''''''
Note that in the ``takes_array_of_any_rank`` example in the previous section,
``Array`` behaved as if it were ``Tuple[int, ...]``. This situation - when
type parameters are not specified - is the *only* case when a type variable
tuple may be bound to an unknown-length type. That is:
::
def foo(x: Tuple[*Ts]): ...
x: Tuple[float, ...]
foo(x) # NOT valid; Ts would be bound to ``Tuple[float, ...]``
(If this is confusing - didn't we say that type variable tuples are a stand-in
for an *arbitrary* number of types? - note the difference between the
length of the type variable tuple *itself*, and the length of the type it is
*bound* to. Type variable tuples themselves can be of arbitrary length -
that is, they can be bound to ``Tuple[int]``, ``Tuple[int, int]``, and
so on - but the length of the types they are bound to must be of known length -
that is, ``Tuple[int, int]``, but not ``Tuple[int, ...]``.)
Type Variable Tuple Equality
''''''''''''''''''''''''''''
If the same ``TypeVarTuple`` instance is used in multiple places in a signature
or class, a valid type inference might be to bind the ``TypeVarTuple`` to
a ``Tuple`` of a ``Union`` of types:
::
def foo(arg1: Tuple[*Ts], arg2: Tuple[*Ts])
a = (0,)
b = ('0',)
foo(a, b) # Can Ts be bound to Tuple[int | str]?
We do *not* allow this; type unions may *not* appear within the ``Tuple``.
If a type variable tuple appears in multiple places in a signature,
the types must match exactly:
::
def pointwise_multiply(
x: Array[*Shape],
y: Array[*Shape]
) -> Array[*Shape]: ...
x: Array[Height]
y: Array[Width]
z: Array[Height, Width]
pointwise_multiply(x, x) # Valid
pointwise_multiply(x, y) # Error
pointwise_multiply(x, z) # Error
Multiple Type Variable Tuples: Not Allowed
''''''''''''''''''''''''''''''''''''''''''
As of this PEP, only a single type variable tuple may appear in a type parameter list:
::
class Array(Generic[*Ts1, *Ts2]): ... # Error
(``Union`` is the one exception to this rule; see `Type Variable Tuples with ``Union```.)
Type Prefixing
--------------
Type variable tuples don't have to be alone; normal types can be
prefixed to them:
::
Shape = TypeVarTuple('Shape')
Batch = NewType('Batch', int)
def add_batch_axis(x: Array[*Shape]) -> Array[Batch, *Shape]: ...
def del_batch_axis(x: Array[Batch, *Shape]) -> Array[*Shape]: ...
x: Array[Height, Width]
y = add_batch(x) # Inferred type is Array[Batch, Height, Width]
z = del_batch(y) # Array[Height, Width]
Normal ``TypeVar`` instances can also be prefixed:
::
T = TypeVar('T')
Ts = TypeVarTuple('Ts')
def prefix_tuple(
x: T,
y: Tuple[*Ts]
) -> Tuple[T, *Ts]: ...
z = prefix_tuple(x=0, y=(True, 'a'))
# Inferred type of z is Tuple[int, bool, str]
As of this PEP - that is, we may expand the flexibility of concatenation in future PEPs - prefixing is the only form of concatenation supported. (That is, the type variable tuple must appear last in the type parameter list.)
``*args`` as a Type Variable Tuple
''''''''''''''''''''''''''''''''''
----------------------------------
PEP 484 states that when a type annotation is provided for ``*args``, each argument
must be of the type annotated. That is, if we specify ``*args`` to be type ``int``,
then *all* arguments must be of type ``int``. This limits our ability to specify
the type signatures of functions that take heterogeneous argument types.
If ``*args`` is annotated as an unpacked type variable tuple, however, the
types of the individual arguments become the types in the type variable tuple:
If ``*args`` is annotated as a type variable tuple, however, the types of the
individual arguments become the types in the type variable tuple:
::
Ts = TypeVarTuple('Ts')
def args_to_tuple(*args: *Ts) -> Ts: ...
def args_to_tuple(*args: *Ts) -> Tuple[*Ts]: ...
args_to_tuple(1, 'a') # Inferred type is Tuple[int, str]
Note that the type variable tuple must be unpacked in order for this new
behaviour to apply. If the type variable tuple is not unpacked, the old
behaviour still applies:
If no arguments are passed, the type variable tuple behaves like an
empty tuple, ``Tuple[()]``.
Note that, in keeping with the rule that type variable tuples must always
be used unpacked, annotating ``*args`` as being a plain type variable tuple
instance is *not* allowed:
::
# *All* arguments must be of type Tuple[T1, T2],
# where T1 and T2 are the same types for all arguments
def foo(*args: Ts) -> Ts: ...
def foo(*args: Ts): ... # NOT valid
x: Tuple[int, str]
y: Tuple[int, str]
foo(x, y) # Valid
Also note that if the type variable tuple is wrapped in a ``Tuple``,
the old behaviour still applies: all arguments must be a ``Tuple``
parameterised with the same types.
z: Tuple[bool]
foo(x, z) # Not valid
::
def foo(*args: Tuple[*Ts]): ...
foo((0,), (1,)) # Valid
foo((0,), (1, 2)) # Error
foo((0,), ('1',)) # Error
Following `Type Variable Tuples Must Have Finite Length When Bound`, note
that the following should *not* type-check as valid (even though it is, of
course, valid at runtime):
::
def foo(*args: Tuple[*Ts]): ...
def bar(x: Tuple[int, ...]):
foo(*x) # NOT valid
Finally, note that a type variable tuple may *not* be used as the type of
``**kwargs``. (We do not yet know of a use-case for this feature, so prefer
``**kwargs``. (We do not yet know of a use case for this feature, so we prefer
to leave the ground fresh for a potential future PEP.)
::
# NOT valid
def foo(**kwargs: Ts): ...
def foo(**kwargs: *Ts): ...
Type Variable Tuples with ``Callable``
''''''''''''''''''''''''''''''''''''''
--------------------------------------
Type variable tuples can also be used in the arguments section of a
``Callable``:
@ -286,116 +389,98 @@ Type variable tuples can also be used in the arguments section of a
::
class Process:
def __init__(target: Callable[[*Ts], Any], args: Tuple[*Ts]): ...
def __init__(
target: Callable[[*Ts], Any],
args: Tuple[*Ts]
): ...
def func(arg1: int, arg2: str): ...
Process(target=func, args=(0, 'foo')) # Passes type-check
Process(target=func, args=('foo', 0)) # Fails type-check
Process(target=func, args=(0, 'foo')) # Valid
Process(target=func, args=('foo', 0)) # Error
However, note that as of this PEP, if a type variable tuple does appear in
the arguments section of a ``Callable``, it must appear alone.
That is, `Type Prefixing` is not supported in the context of ``Callable``.
(Use cases where this might otherwise be desirable are likely covered through use
of either a `ParamSpec` from PEP 612, or a type variable tuple in the `__call__`
signature of a callback protocol from PEP 544.)
Type Variable Tuples with ``Union``
'''''''''''''''''''''''''''''''''''
-----------------------------------
Finally, type variable tuples can be used with ``Union``:
Type variable tuples can also be used with ``Union``:
::
def f(*args: *Ts) -> Union[*Ts]:
def f(*args: Tuple[*Ts]) -> Union[*Ts]:
return random.choice(args)
f(1, 'foo') # Inferred type is Union[int, str]
If the type variable tuple is empty (e.g. if we had ``*args: *Ts``
More than one type variable tuple may appear in the the parameter list
to ``Union``:
::
def cond_random_choice(
cond: bool,
cond_true: Tuple[*Ts1],
cond_false: Tuple[*Ts2]
) -> Union[*Ts1, *Ts2]:
if cond:
return random.choice(cond_true)
else:
return random.choice(cond_false)
# Inferred type is Union[int, str, float]
cond_random_choice(True, (1, 'foo'), (0.0, 'bar'))
If the type variable tuple is empty (e.g. if we had ``*args: Tuple[*Ts]``
and didn't pass any arguments), the type checker should
raise an error on the ``Union`` (matching the behaviour of ``Union``
at runtime, which requires at least one type argument).
``Map``
Aliases
-------
To enable typing of functions such as ``map`` and ``zip``, we introduce the
``Map`` type operator. Not to be confused with the existing operator
``typing.Mapping``, ``Map`` is analogous to ``map``, but for types:
Generic aliases can be created using a type variable tuple in
a similar way to regular type variables:
::
from typing import Map
IntTuple = Tuple[int, *Ts]
IntTuple[float, bool] # Equivalent to Tuple[int, float, bool]
def args_to_lists(*args: *Ts) -> Map[List, Ts]: ...
args_to_lists(1, 'a') # Inferred type is Tuple[List[int], List[str]]
``Map`` takes two operands. The first operand is a parameterizable
type (or type alias [#type_aliases]_) such as ``Tuple``, ``List``, or a
user-defined generic class. The second operand is a type variable tuple.
The result of ``Map`` is a ``Tuple``, where the Nth type in the ``Tuple`` is
the first operand parameterized by the Nth type in the type variable tuple.
Because ``Map`` returns a parameterized ``Tuple``, it can be used anywhere
that a type variable tuple would be. For example, as the type of ``*args``:
::
# Equivalent to 'arg1: List[T1], arg2: List[T2], ...'
def foo(*args: *Map[List, Ts]): ...
# Ts is bound to Tuple[int, str]
foo([1], ['a'])
As a return type:
As this example shows, all type parameters passed to the alias are
bound to the type variable tuple. If no type parameters are given,
or if an explicitly empty list of type parameters are given,
type variable tuple in the alias is simply ignored:
::
# Equivalent to '-> Tuple[List[T1], List[T2], ...]'
def bar(*args: *Ts) -> Map[List, Ts]: ...
# Ts is bound to Tuple[float, bool]
# Inferred type is Tuple[List[float], List[bool]]
bar(1.0, True)
# Both equivalent to Tuple[int]
IntTuple
IntTuple[()]
And as an argument type:
Normal ``TypeVar`` instances can also be used in such aliases:
::
# Equivalent to 'arg: Tuple[List[T1], List[T2], ...]'
def baz(arg: Map[List, Ts]): ...
# Ts is bound to Tuple[bool, bool]
baz(([True], [False]))
T = TypeVar('T')
Foo = Tuple[T, *Ts]
``map`` and ``zip``
'''''''''''''''''''
# T is bound to `int`; Ts is bound to `bool, str`
Foo[int, bool, str]
``Map`` allows us to specify the signature of ``map`` as:
::
Ts = TypeVarTuple('Ts')
R = TypeVar(R)
def map(func: Callable[[*Ts], R],
*iterables: *Map[Iterable, Ts]) -> Iterable[R]: ...
def func(int, str) -> float: ...
# Ts is bound to Tuple[int, str]
# Map[Iterable, Ts] is Iterable[int], Iterable[str]
# Therefore, iter1 must be type Iterable[int],
# and iter2 must be type Iterable[str]
map(func, iter1, iter2)
Similarly, we can specify the signature of ``zip`` as:
::
def zip(*iterables: *Map[Iterable, Ts]) -> Iterator[Ts]): ...
l1: List[int]
l2: List[str]
zip(l1, l2) # Iterator[Tuple[int, str]]
Note that the same rules for `Type Prefixing` apply for aliases.
In particular, only one ``TypeVarTuple`` may occur within an alias,
and the ``TypeVarTuple`` must be at the end of the alias.
Overloads for Accessing Individual Types
----------------------------------------
``Map`` allows us to operate on types in a bulk fashion. For situations where we
require access to each individual type, overloads can be used with individual
``TypeVar`` instances in place of the type variable tuple:
For situations where we require access to each individual type, overloads can be used with individual ``TypeVar`` instances in place of the type variable tuple:
::
@ -404,7 +489,7 @@ require access to each individual type, overloads can be used with individual
Axis2 = TypeVar('Axis2')
Axis3 = TypeVar('Axis3')
class Array(Generic[*Shape]): ...
class Array(Generic[Shape]): ...
@overload
def transpose(
@ -421,232 +506,6 @@ overloads for each possible rank is, of course, a rather cumbersome
solution. However, it's the best we can do without additional type
manipulation mechanisms, which are beyond the scope of this PEP.)
Concatenating Other Types to a Type Variable Tuple
--------------------------------------------------
If an unpacked type variable tuple appears with other types in the same type parameter
list, the effect is to concatenate those types with the types in the type variable
tuple. For example, concatenation in a function return type:
::
Batch = NewType('int')
Height = NewType('int')
Width = NewType('int')
class Array(Generic[*Shape]): ...
def add_batch(x: Array[*Shape]) -> Array[Batch, *Shape]: ...
x: Array[Height, Width]
y = add_batch(x) # Inferred type is Array[Batch, Height, Width]
In function argument types:
::
def batch_sum(x: Array[Batch, *Shape]) -> Array[*Shape]: ...
x: Array[Batch, Height, Width]
y = batch_sum(x) # Inferred type is Array[Height, Width]
And in class type parameters:
::
class BatchArray(Generic[Batch, *Shape]):
def sum(self) -> Array[*Shape]: ...
x: BatchArray[Batch, Height, Width]
y = x.sum() # Inferred type is Array[Height, Width]
Concatenation can involve both prefixing and suffixing, and
can include an arbitrary number of types:
::
def foo(x: Tuple[*Ts]) -> Tuple[int, str, *Ts, bool]: ...
It is also possible to concatenate type variable tuples with regular
type variables:
::
T = TypeVar('T')
def first_axis_sum(x: Array[T, *Shape]) -> Array[*Shape]: ...
x: Array[Time, Height, Width]
y = first_axis_sum(x) # Inferred type is Array[Height, Width]
Finally, concatenation can also occur in the argument list to ``Callable``:
::
def f(func: Callable[[int, *Ts], Any]) -> Tuple[*Ts]: ...
def foo(int, str, float): ...
def bar(str, int, float): ...
f(foo) # Valid; inferred type is Tuple[str, float]
f(bar) # Not valid
And in ``Union``:
::
def f(*args: *Ts) -> Union[*Ts, float]: ...
f(0, 'spam') # Inferred type is Union[int, str, float]
Concatenating Multiple Type Variable Tuples
-------------------------------------------
We can also concatenate *multiple* type variable tuples, but only in cases
where the types bound to each type variable tuple can be inferred
unambiguously. Note that this is not always the case:
::
# Type checker should raise an error on definition of func;
# how would we know which types are bound to Ts1, and which
# are bound to Ts2?
def func(ham: Tuple[*Ts1, *Ts2]): ...
# Ts1 = Tuple[int, str], Ts2 = Tuple[bool]?
# Or Ts1 = Tuple[int], Ts2 = Tuple[str, bool]?
ham: Tuple[int, str, bool]
func(ham)
In general, some kind of extra constraint is necessary in order
for the ambiguity to be resolved. This is usually provided by
an un-concatenated usage of the type variable tuple elsewhere in
the same signature.
For example, resolving ambiguity in an argument:
::
def func(ham: Tuple[*Ts1, *Ts2], spam: Ts2): ...
# Ts1 is bound to Tuple[int], Ts2 to Tuple[str, bool]
ham: Tuple[int, str, bool]
spam: Tuple[str, bool]
func(ham, spam)
In a return type:
::
def func(ham: Ts1, spam: Ts2) -> Tuple[*Ts1, *Ts2]): ...
ham: Tuple[int]
spam: Tuple[str, bool]
# Return type is Tuple[int, str, bool]
func(ham, spam)
Note, however, that the same cannot be done with generic classes:
::
# No way to add extra constraints about Ts1 and Ts2,
# so this is not valid
class C(Generic[*Ts1, *Ts2]): ...
Generics in Multiple Type Variable Tuples
-----------------------------------------
If we *do* wish to use multiple type variable tuples in a type signature
that would otherwise not resolve the ambiguity, it is also possible
to make the type bindings explicit by using a type variable tuple directly,
without unpacking it. When then instantiating, for example, the class in
question, the types corresponding to each type variable tuple must
be wrapped in a ``Tuple``:
::
class C(Generic[Ts1, Ts2]): ...
# Ts1 = Tuple[int, str]
# Ts2 = Tuple[bool]
c: C[Tuple[int, str], Tuple[bool]] = C()
Similarly for functions:
::
def foo(x: Tuple[Ts1, Ts2]): ...
# Ts1 = Tuple[int, float]
# Ts2 = Tuple[bool]
x: Tuple[Tuple[int, float], Tuple[bool]]
foo(x)
Aliases
-------
Generic aliases can be created using a type variable tuple in
a similar way to regular type variables:
::
IntTuple = Tuple[int, *Ts]
IntTuple[float, bool] # Equivalent to Tuple[int, float, bool]
As this example shows, all type arguments passed to the alias are
bound to the type variable tuple. If no type arguments are given,
the type variable tuple holds no types:
::
IntTuple # Equivalent to Tuple[int]
Type variable tuples can also be used without unpacking:
::
IntTuple = Tuple[int, Ts]
IntTuple[float, bool] # Equivalent to Tuple[int, Tuple[float, bool]]
IntTuple # Tuple[int, Tuple[]]
At most a single distinct type variable tuple can occur in an alias:
::
# Invalid
Foo = Tuple[Ts1, int, Ts2]
# Why? Because there would be no way to decide which types should
# be bound to which type variable tuple:
Foo[float, bool, str]
# Equivalent to Tuple[float, bool, int, str]?
# Or Tuple[float, int, bool, str]?
The same type variable tuple may be used multiple times, however:
::
Bar = Tuple[*Ts, *Ts]
Bar[int, float] # Equivalent to Tuple[int, float, int, float]
Finally, type variable tuples can be used in combination with
normal type variables. In this case, the number of type arguments must
be equal to or greater than the number of distinct normal type variables:
::
Baz = Tuple[T1, *Ts, T2, T1]
# T1 bound to int, T2 bound to bool, Ts empty
# Equivalent to Tuple[int, bool, int]
Baz[int, bool]
# T1 bound to int
# Ts bound to Tuple[float, bool]
# T2 bound to str
# So equivalent to Tuple[int, float, bool, str, int]
Baz[int, float, bool, str]
An Ideal Array Type: One Possible Example
=========================================
@ -693,6 +552,11 @@ in a complete array type.
# E.g. Array1D[np.uint8]
Array1D = Array[DataType, Ndim[Literal[1]]]
Final Notes
===========
**Slice expressions**: type variable tuples may *not* appear in slice expressions.
Rationale and Rejected Ideas
============================
@ -710,40 +574,8 @@ by simply defining aliases for each possible number of type parameters:
However, this seems somewhat clumsy - it requires users to unnecessarily
pepper their code with 1s, 2s, and so on for each rank necessary.
Naming of ``Map``
-----------------
One downside to the name ``Map`` is that it might suggest a hash map. We
considered a number of different options for the name of this operator.
* ``ForEach``. This is rather long, and we thought might imply a side-effect.
* ``Transform``. The meaning of this isn't obvious enough at first glance.
* ``Apply``. This is inconsistent with ``apply``, an older Python function
which enabled conversion of iterables to arguments before the star
operator was introduced.
In the end, we decided that ``Map`` was good enough.
Nesting ``Map``
---------------
Since the result of ``Map`` is a parameterised ``Tuple``, it should be
possible to use the output of a ``Map`` as the input to another ``Map``:
::
Map[Tuple, Map[List, Ts]]
If ``Ts`` here were bound to ``Tuple[int, str]``, the result of the
inner ``Map`` would be ``Tuple[List[int], List[str]]``, so the result
of the outer map would be ``Tuple[Tuple[List[int]], Tuple[List[str]]]``.
We chose not to highlight this fact because of a) how confusing it is,
and b) lack of a specific use-case. Whether to support nested ``Map``
is left to the implementation.
Naming of ``TypeVarTuple``
--------------------------
Construction of ``TypeVarTuple``
--------------------------------
``TypeVarTuple`` began as ``ListVariadic``, based on its naming in
an early implementation in Pyre.
@ -752,12 +584,15 @@ We then changed this to ``TypeVar(list=True)``, on the basis that a)
it better emphasises the similarity to ``TypeVar``, and b) the meaning
of 'list' is more easily understood than the jargon of 'variadic'.
We finally settled on ``TypeVarTuple`` based on the justification
that c) this emphasises the tuple-like behaviour, and d) type variable
tuples are a sufficiently different kind of thing to regular
type variables that we may later wish to support keyword arguments
to its constructor that should not be supported by regular
type variables (such as ``arbitrary_len`` [#arbitrary_len]_).
Once we'd decided that a variadic type variable should behave like a ``Tuple``,
we also considered ``TypeVar(bound=Tuple)``, which is similarly intuitive
and accomplishes most what we wanted without requiring any new arguments to
``TypeVar``. However, we realised this may constrain us in the future, if
for example we want type bounds or variance to function slightly differently
for variadic type variables than what the semantics of ``TypeVar`` might
otherwise imply. Also, we may later wish to support arguments that should not be supported by regular type variables (such as ``arbitrary_len`` [#arbitrary_len]_).
We therefore settled on ``TypeVarTuple``.
Backwards Compatibility
=======================
@ -776,9 +611,6 @@ TODO
Footnotes
==========
.. [#hkt] A third potential use is in enabling higher-kinded types that take
an arbitrary number of type operands, but we do not discuss this use
here.
.. [#batch] 'Batch' is machine learning parlance for 'a number of'.
@ -791,11 +623,6 @@ Footnotes
shape begins with 'time × batch', then ``videos_batch[1][0]`` would select the
same frame.
.. [#type_aliases] For example, in ``asyncio`` [#asyncio]_, it is convenient
to define a type alias
``_FutureT = Union[Future[_T], Generator[Any, None, _T], Awaitable[_T]]``.
We should be able to apply ``Map`` to such aliases - e.g. ``Map[_FutureT, Ts]``.
References
==========
@ -810,10 +637,6 @@ References
.. [#syntax-proposal] Shape annotation syntax proposal:
https://docs.google.com/document/d/1But-hjet8-djv519HEKvBN6Ik2lW3yu0ojZo6pG9osY/edit
.. [#zip-sig] ``typeshed/builtins.pyi``: https://github.com/python/typeshed/blob/27dfbf68aaffab4f1ded7dc1b96f6f82f536a09d/stdlib/2and3/builtins.pyi#L1710-L1733
.. [#asyncio] ``typeshed/asyncio/tasks.pyi``: https://github.com/python/typeshed/blob/193c7cb93283ad4ca2a65df74c565e56bfe72b7e/stdlib/3/asyncio/tasks.pyi#L45-L154
.. [#arbitrary_len] Discussion on Python typing-sig mailing list: https://mail.python.org/archives/list/typing-sig@python.org/thread/SQVTQYWIOI4TIO7NNBTFFWFMSMS2TA4J/
@ -825,8 +648,7 @@ Thank you to **Alfonso Castaño**, **Antoine Pitrou**, **Bas v.B.**, **David Fos
**Sergei Lebedev** and **Vladimir Mikulik** for helpful feedback and suggestions on
drafts of this PEP.
Thank you especially to **Lucio**, for suggesting the star syntax, which has made
multiple aspects of this proposal much more concise and intuitive.
Thank you especially to **Lucio**, for suggesting the star syntax, which has made multiple aspects of this proposal much more concise and intuitive.
Resources
=========
@ -865,4 +687,3 @@ CC0-1.0-Universal license, whichever is more permissive.
fill-column: 70
coding: utf-8
End: