Ugh, sorry to flip-flop - it's only in finding a big enough chunk of time to really sit down and work on this again that things are becoming clearer...
I think allowing unexpanded type tuple variables would be a mistake
I disagree, past Matthew! :)
First, it makes the mental model really confusing. If we were to say that `Tuple[*Ts]` is valid, but `Ts` on its own isn't, then `Ts` is somehow a tuple of types that is nonetheless not an actual `Tuple`. We would be saying that `Ts` behaves like a parameterised tuple, but can only be used exactly like one in certain cases.
Relatedly, we do still have to allow unexpanded type variable tuples *sometimes* - e.g. `Map`:
```python Map[List, Ts] ```
If we *were* to disallow unexpanded type variables tuples in general, then the rule would have to be something like "Type variable tuples should always be used expanded, except in `Map`", which seems horribly kludgy.
If the issue is that there should only be one way to do it, then fine - we encourage users to use plain `Ts` in the first place, instead of `Tuple[*Ts]`.
b) undermines one of the reasons we thought of using the star operator
here in the first place: to make it clear when the thing in question is a type variable *tuple* rather than just a plain type variable.
I think this is worth giving up for the sake of a more consistent mental model about what kind of a thing a type variable tuple is. Plus, we'll still be using the star in *most* cases.
A final argument in favour of allowing unexpanded type variables is that it saves on keystrokes and verbosity. `-> Ts` is nicer than `-> Tuple[*Ts]`. :)
So in summary, we'd write:
```python class Tensor(Generic[*Shape]): ... t: Tensor[Height, Width]
class Tensor2(Generic[Shape]): ... t2: Tensor[Tuple[Height, Width]]
class MultiTensor(Generic[Shape1, Shape]): ... mt: MultiTensor[Tuple[Time, Batch], Tuple[Height, Width]]
def args_to_tuples(*args: *Ts) -> Ts: ...
class Process: def __init__(target: Callable[[*Ts], Any], args: Ts): ... ```
On Wed, 23 Dec 2020 at 15:02, Matthew Rahtz email@example.com wrote:
P.P.S. Pradeep - did you say you had an example of where a class generic in multiple type variable tuples *would* be necessary?
On Wed, 23 Dec 2020 at 15:01, Matthew Rahtz firstname.lastname@example.org wrote:
P.S. If we did go for Option 3, we could still make classes generic in multiple type tuple variables by using the explicit syntax:
class C(Generic[Tuple[*Ts1], Tuple[*Ts2]): ... c: C[Tuple[int, str], Tuple[float]]
(Aside: this was previously written in the PEP using 'unexpanded' type tuple variables:
class C(Generic[Ts1, Ts2]): ...
But having thought on it for a week, I think allowing unexpanded type tuple variables would be a mistake - it would mean there are two ways to write the following:
def identity(x: Tuple[*Ts]) -> Tuple[*Ts]: ... # could also be written as def identity(x: Ts) -> Ts: ...
This is a) un-Pythonic, and b) undermines one of the reasons we thought of using the star operator here in the first place: to make it clear when the thing in question is a type variable *tuple* rather than just a plain type variable.)
On Wed, 23 Dec 2020 at 14:33, Matthew Rahtz email@example.com wrote:
(Moving some discussion here from the doc)
# Concatenating type variable tuples with other types
In all cases we should also support extra single types before and
after, e.g. Tuple[int, *Ts, str].
In cases where there's only a single type variable tuple, it should be fine to allow an *arbitrary* number of concrete types before and after, shouldn't it?
def foo(t: Tuple[int, str, *Ts, double]) -> Tuple[*Ts]: ... t: Tuple[int, str, float, bool, double] foo(t) # Return has type Tuple[float, bool]
# Multiple type variable tuples
Thinking out loud - let's get straight about all the places this could occur.
## Function arguments
def func(spam: Tuple[*Ts1, *Ts2]): ... spam: Tuple[int, str, bool] func(spam)
This wouldn't work: how would we decide which types were bound to `Ts1` and which were bound to `Ts2`? (Ignore the fact that type variables are only used once in the signature here.)
On the other hand, it *would* work if there were extra constraints - say, from other arguments whose type was unambiguous:
def func(ham: Tuple[*Ts1], spam: Tuple[*Ts1, *Ts2]): ... ham: Tuple[int, float] spam: Tuple[int, float, double, str] func(ham, spam)
**Conclusion: sometimes alright, sometimes not.**
## Function returns
Can this work?
def foo() -> Tuple[*Ts1, *Ts]: return 0, 0.0, '0'
On the face of it, we have the same problem. But in practice, we'd never encounter this example, because `Ts1` and `Ts2` would have had to occur somewhere else in the signature, which would have nailed them down:
def foo(ham: Tuple[*Ts1], spam: Tuple[*Ts2]) -> Tuple[*Ts1, *Ts2]: ... ham: Tuple[int, str] spam: Tuple[float, double] foo(ham, spam) # Inferred type is Tuple[int, str, float, double]
**Conclusion: always fine.**
class C(Generic[*Ts1, *Ts2]): ... c: C[int, str, float] = C()
Same problem as function arguments. And this time, I don't think there's any way to add extra constraints to disambiguate.
**Conclusion: never alright.**
If, for some reason, we did want a class that was generic in multiple type tuple variables, the current proposal in the PEP is:
class C(Generic[Ts1, Ts2]): ... c: C[Tuple[int, str], Tuple[float]] = C() # Great! c: C[int, str, float] # Not allowed
OK, so the example that Pradeep suggested...
def partial(f: Callable[[*Ts, *Rs], T], *some_args: *Ts) -> Callable[[*Rs], T]: ...
...is similar to Example 2: the ``Callable`` is ambiguous on its own, but there's extra context in the rest of the signature which disambiguates it.
So overall, the three options I see are:
- Option 1: Disallow multiple expanded type variables tuples everywhere,
for consistency and ease-of-understanding
- Option 2: Only allow multiple expanded type variable tuples in
contexts where it's *always* unambiguous - i.e. only in return types.
- Option 3: Allow multiple expanded type variable tuples in general, but
have the type checker produce an error when the types cannot be solved for.
On Wed, 23 Dec 2020 at 11:02, Matthew Rahtz firstname.lastname@example.org wrote:
Thank you for sponsoring this, Guido, and for the thorough review!
I wonder why the proposal left out `Union[*Ts]`.
Ah, yes, great point. I'll add a section on that.
I'm not sure that `Tensor[T1, T2, ...]` is the be-all and end-all of
tensor types (e.g. where would you put the data type of numpy arrays?) but maybe that can be handled by just adding one non-variadic type variable (a complete example would be nice though).
This has been on the back of my mind too. Adding a single additional non-variadic type variable is how I was imagining it would work too, though there are still some details to work out (e.g. ideally it should be optional so that people can choose what level of type verbosity they want to go with). I'll add a section trying to figure this out.
The other thing that's still unresolved is how we handle access to individual types - needed so that we can provide overloads of shape-manipulating operations. (I'm assuming that overloads are the way to go here, at least for the time being. In an ideal world we would be able to express the resulting shapes directly as a function of the arguments, but I don't think that'll be possible without fully dependent typing). My initial idea was to do this using "class overloads":
class Tensor(Generic[*Shape]): ... @overload class Tensor(Generic[Axis1, Axis2]): def transpose(self) -> Tensor[Axis2, Axis1]: ... @overload class Tensor(Generic[Axis1, Axis2, Axis3]): def transpose(self) -> Tensor[Axis3, Axis2, Axis1]: ...
But you're right in calling this out in the draft doc as non-trivial. It's also very verbose, requiring a whole separate class for each possible instantiation.
Instead, perhaps the following would suffice?
class Tensor(Generic[*Shape]): @overload def transpose(self: Tensor[Axis1, Axis2]) -> Tensor[Axis2, Axis1]: ... @overload def transpose(self: Tensor[Axis1, Axis2, Axis3]) -> Tensor[Axis3, Axis2, Axis1]: ...
This is similar to the following example, which already seems to type-check properly in mypy:
class C(Generic[T]): @overload def f(self: C[int], x) -> int: return x @overload def f(self: C[str], x) -> str: return x
I'd welcome other suggestions, though!
In any case, I'll continue cleaning up the doc as suggested, moving discussion of meatier issues to this thread for posterity, and post here once I think the doc is done.
On Tue, 22 Dec 2020 at 23:46, Guido van Rossum email@example.com wrote:
I have read the proposed PEP about variadic generics (PEP 646) and I like it enough that I want to sponsor it and want to help getting it over the finish line (we have to get the Steering Council to understand enough of it that they'll delegate approval to me :-).
For reference, here's the PR that proposes to add PEP 646: https://github.com/python/peps/pull/1740 And here's the original Google Doc: https://docs.google.com/document/d/1oXWyAtnv0-pbyJud8H5wkpIk8aajbkX-leJ8JXsE...
A good review starts by briefly summarizing the proposal being reviewed, so here's the proposal in my own words.
**Motivation A:** We want to create generic types that take an arbitrary number of type parameters, like Tuple. For example, Tensors where each dimension is a "type". There is a demonstration of this without variadics, but it requires defining types `Tensor1[T1]`, `Tensor2[T1, T2]`, etc.: https://github.com/deepmind/tensor_annotations. We want just `Tensor[T1]`, `Tensor[T1, T2]`, etc., for any number of parameters.
**Motivation B:** The type of functions like map() and zip() cannot be expressed using the existing type system. The simplest example would be the type of
def foo(*args): return args a = foo(42, "abc") # Should have type Tuple[int, str]
**Proposal:** Introduce a new kind of type variable that can be instantiated with an arbitrary number of types, some new syntax, and a new type operator:
Ts = TypeVarTuple("Ts") # NEW T = TypeVar("T") def f(*args: *Ts) -> Tuple[*Ts]: ... class C(Generic[*Ts]): ... Callable[[*Ts], T] Tuple[*Ts] Map[SomeType, Ts] # SomeType is a generic of one parameter
In most cases the form `*Ts` may be preceded and/or followed by any number of non-variadic types, e.g., `Tuple[int, int, *Ts, str]`. In cases where it's unambiguous, multiple variadic type variables are also allowed, e.g., `Tuple[*Ts1, *Ts2]`. For older Python versions, `Expand[Ts]` would mean the same as `*Ts`.
So now let me go on with my (generally favorable) review. (I left many detailed editorial comments in the Google Doc -- I will not repeat those here.)
I like the proposal a lot, and I am glad that we now have (apparently) a working prototype in Pyre. This has been on our wish list since at least 2016 -- much early discussion happened in https://github.com/python/typing/issues/193 and at various meetings at PyCon and at the Bay Area Typing Meetups (links in the PEP). The proposed syntax has cycled through endless variations, and I am fine with the current proposal, even though it is still slightly clunky. There's also https://github.com/python/typing/issues/513, which is specifically about array types.
There are probably other motivating applications that the PEP doesn't mention, for example certain decorator types (I doubt that all of these are taken care by PEP 612, ParamSpec).
I wonder why the proposal left out `Union[*Ts]`. This would seem useful, e.g. to type this function:
def f(*args): return random.choice(args)
which could be typed naturally as follows:
def f(*args: *Ts) -> Union[*Ts]: return random.choice(args)
I'm not sure that `Tensor[T1, T2, ...]` is the be-all and end-all of tensor types (e.g. where would you put the data type of numpy arrays?) but maybe that can be handled by just adding one non-variadic type variable (a complete example would be nice though). There are also proposals for integer generics, which deserve their own PEP (presumably aiming at Python 3.11).
Eric Traut proposed an extension that would allow defining variadic subtypes of Sequence which behave similar to Tuple (where `Tuple[int, int]` is a subtype of `Tuple[int, ...]` which is a subtype of `Sequence[int]`), but I'm not sure we would need that a lot -- we could always add that later.
The introduction of a prefix `*` operator requires new syntax in a few cases. While `Callable[[*Ts], T]` is already valid (the parser interprets this as sequence unpacking), `Tuple[*Ts]` is not, and neither is `def f(*a: *Ts)`. For `Tuple[*Ts]` we can piggy-back on PEP 637 (keyword indexing, which adds this as well), but for the `def` example we'll need to add something new specifically for this PEP. I think that's fine -- we can give it runtime semantics that turns `*Ts` into `(*Ts,)`, which is similar to the other places: at runtime it iterates over the argument, producing a tuple. In all cases we need to support `Expand[Ts]` as well for backwards compatibility with Python 3.9 and before.
The `Map` operator is, as I said, fairly clunky. In the past various other syntaxes have been proposed. In particular, @sixolet p https://github.com/python/typing/issues/513roposed a syntax that would allow defining `zip()` as follows:
def zip(*args: Iterable[Ts]) -> Iterator[Tuple[Ts, ...]]: ...
Compare this to what it would look using the current proposal:
def zip(*args: *Map[Iterable, Ts]) -> Iterator[Ts]): ... # Note that Iterator[Ts] is the same as Iterator[Tuple[*Ts]]
Sixolet's syntax made the iteration over the elements of Ts implicit, which is slightly shorter, and doesn't require "higher-order type functions" (is there an official name for that?), but also slightly more cryptic, and created yet another use for the ellipsis: `Tuple[Ts, ...]` is not quite analogous to `Tuple[T, ...]`, since the latter is *homogeneous* while the former is still heterogeneous. The new notation uses an explicit `Map` operator, which is similar to the choice we made in PEP 612 for `Concatenate`. (Speaking of this choice, we could drop the `*` prefix and rely purely on `Expand`, but that feels unnecessarily verbose, and we'll get most of the needed syntax for free with PEP 637, assuming it's accepted.)
All in all my recommendation for this PEP is: clean up the text based on the GDoc feedback, add `Union[*Ts]`, and submit to the Steering Council.
-- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/ _______________________________________________ Typing-sig mailing list -- firstname.lastname@example.org To unsubscribe send an email to email@example.com https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: firstname.lastname@example.org