Skip to content

Allow type aliases such X = List[T] if T is a type variable #606

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
JukkaL opened this issue Mar 21, 2015 · 21 comments
Closed

Allow type aliases such X = List[T] if T is a type variable #606

JukkaL opened this issue Mar 21, 2015 · 21 comments

Comments

@JukkaL
Copy link
Collaborator

JukkaL commented Mar 21, 2015

This code should be okay:

from typing import TypeVar, List
T = TypeVar('T')
X = List[T]
def f(x: X) -> None: ...

It should be equivalent to this:

from typing import TypeVar, List
T = TypeVar('T')
def f(x: List[T]) -> None: ...

This was suggested by Guido, "based on the general equivalence in Python of

X = <stuff>
f(X)

to

f(<stuff>)

".

@JukkaL JukkaL added the feature label Mar 21, 2015
@JukkaL
Copy link
Collaborator Author

JukkaL commented May 17, 2015

This is not included in PEP 484, so closing this.

@JukkaL JukkaL closed this as completed May 17, 2015
@gvanrossum
Copy link
Member

Whoa, maybe this should be added to PEP 484? I still am in favor of this.

@gvanrossum gvanrossum reopened this May 17, 2015
@gvanrossum
Copy link
Member

In particular, PEP 484 (https://github.com/ambv/typehinting/blob/master/pep-0484.txt) contains this example:

Vector = Iterable[Tuple[T, T]]

@JukkaL
Copy link
Collaborator Author

JukkaL commented May 19, 2015

What about something like this:

Table = Dict[str, T]

def f(t: Table[int]) -> ...: ...

@gvanrossum
Copy link
Member

The implementation in typing.py requires that you write Table[str, int] in the argument attribute. I agree this is a bit arbitrary, but the alternative felt pretty confusing. In an earlier version I was trying to support this to the extreme, allowing e.g.

# T, U, V are TypeVars
A = Tuple[T, U, V]
X = Dict[int, Union[Tuple[U, U], A]]

and then X would have 3 parameters (U, T, V). That felt too complicated, so I retreated on the rule that you can't change the number of parameters at all, and substitutions must be either completely concrete or single type variables, and the resulting type still has the original number of parameters. Maybe we need to find a middle ground, where your example is allowed and does in fact reduce the number of parameters. But you could still have only substitutions that are either completely concrete or a single type variable. An edge case would be X = Dict[T, T] which gives X a single parameter, T.

@berdario
Copy link

berdario commented May 5, 2016

Just stumbled upon this, the confusing thing is that the error is reported not at the line of the type alias, but at the line at which the TypeVar (T in this case) has been defined:

 error: Invalid type "module.T"

@vitawasalreadytaken
Copy link
Contributor

With mypy 0.4.4 I'm getting the error at the line of the type alias:

from typing import TypeVar, Tuple

T = TypeVar('T')
U = TypeVar('U')

X = Tuple[T, U]
$ mypy test.py
test.py:6: error: Invalid type "test.T"
test.py:6: error: Invalid type "test.U"

@roganov
Copy link

roganov commented Oct 10, 2016

+1 for this

I'm developing a dependency injection library where types are first-class citizens (users can request instance provided a type), that's why I need either generic aliases or ability to define generic types at runtime.

@ddfisher
Copy link
Collaborator

I think we shouldn't allow this, but should instead allow definition of parameterized type aliases. Having the type variable be explicit makes a big difference in understandability and power.
Here's an example. Let's say we have Vec defined in some module like so:

# in module vector
T = TypeVar('T')
Vec = List[Tuple[T, T]]

Let's say we want to define append and map. Here's how we'd do that with this proposal:

# in module __main__
from vector import Vec, T

def append(vec: Vec, elem: T) -> Vec: ...

U = TypeVar('U')
def map(vec: Vec, f: Callable[[T], U]) -> ???: ...

Note that we have to use vector.T to properly define append. If we define our own T locally, that will cause a confusing error (because it will be considered a different type variable). (Of course, for all expressions not involving a Vec, T will behave just like any other type variable, so I think it's likely to be seen in other contexts too, which would be additionally confusing.) Also note that the fact that elem is the right element type for vec is implicit rather than explicit (as is the fact that vec and the return type have the same type). Finally, note that we cannot define map using the type alias -- uses of the type alias are confined to those which do not change the parameterized type.

Here's how it would look with parameterized type aliases:

# in module __main__ with parameterized type aliases
from vector import Vec

T = TypeVar('T')
U = TypeVar('U')
def append(vec: Vec[T], elem: T) -> Vec[T]: ...

def map(vec: Vec[T], f: Callable[[T], U]) -> Vec[U]: ...

With parameterized type aliases, all the relationships between parameters have been made explicit, and you can express concepts like map using the type alias.

Another problem with the proposal as it stands is that type variables only really make sense in context. For example, you can't have a top-level variable with type T. T has to be captured in either class or function scope. The way this happens is already a bit more implicit than I'd like, but this proposal would make it very non-obvious where type variables are hiding.

To sum up, parameterized type aliases would be both more powerful and more understandable than non-parameterized type aliases, and I think we should implement them instead.

@gvanrossum
Copy link
Member

gvanrossum commented Oct 13, 2016 via email

@rwbarton
Copy link
Contributor

I agree with @ddfisher that the original X = List[T] with the result that X is "linked" to the variable T is confusing and insufficiently expressive. Of course, in this particular case you could simply write X = List instead.

The problem with Vector = Iterable[Tuple[T, T]] with the idea that Vector[int] then means Iterable[Tuple[int, int]] is that you need a way to disambiguate the order of multiple type parameters. Plus it just looks wrong and doesn't fit in with the idea of replacing things with their definitions: (Iterable[Tuple[T, T]])[int] isn't legal. The syntax should be more like

Vector[T] = Iterable[Tuple[T, T]]

though that exact syntax isn't possible without a previous definition of Vector.

@JukkaL
Copy link
Collaborator Author

JukkaL commented Oct 16, 2016

I also agree that the original formulation is not a good idea.

Making generic types indexable if they have type variables could likely be made to work with some heuristics and I believe that it would be at least be marginally useful, but I don't have a strong opinion. I'd suggest finding several real-world use cases where this could be used before making a decision so that we can validate whether this actually looks worth implementing. If the benefit is minor, it might not justify the extra rules and implementation complexity.

If we want to support it, we'd perhaps want to make List[T][int] valid syntactically, for consistency, even though it seems questionable stylistically. However, somebody might want to write something like Tuple[T, T][Dict[int, List[int]] as a shorter form for Tuple[Dict[int, List[int]], Dict[int, List[int]]].

For cases like X[T, T, S][int] where the substitution is ambiguous we can order the type variables based on the order they first appear in the type textually. This would mean that X[T, T, S][int] is equivalent to X[int, int, S], and X[T, T, S][int, str] is equivalent to X[int, int, str].

@gvanrossum
Copy link
Member

gvanrossum commented Oct 16, 2016 via email

@ilevkivskyi
Copy link
Member

@rwbarton Maybe I misunderstand you, but it looks like you a talking about an outdated version of typing.py. Namely, (Iterable[Tuple[T, T]])[int] is perfectly legal since "Revamped Generics" (python/typing#195). The problem of disambiguating the order of multiple type parameters was also solved in that PR by Guido. Few simple examples (assuming that python/typing#296 for nicer repr is merged soon):

>>> Vector = Iterable[Tuple[T, T]]
>>> Vector[int]
typing.Iterable[typing.Tuple[int, int]]
>>> Iterable[Tuple[T, U, T]][int]
Traceback (most recent call last):
...
Too few parameters for typing.Iterable[typing.Tuple[~T, ~U, ~T]]; actual 1, expected 2
>>> Iterable[Tuple[T, U, T]][int, str]
typing.Iterable[typing.Tuple[int, str, int]]

@JukkaL
Copy link
Collaborator Author

JukkaL commented Oct 16, 2016

I had forgotten about this change as well... Did it go in without much discussion?

@ilevkivskyi
Copy link
Member

@JukkaL Most of discussion about this happened in python/typing#115
I thought the "specification" was well discussed there. The actual implementation maybe was not well discussed, although I like it and it seems quite robust.

@JukkaL
Copy link
Collaborator Author

JukkaL commented Oct 17, 2016

Oops, I think that I wasn't paying much attention to the original discussion then!

Yeah, the implementation sounds reasonable -- my biggest reservation is how useful this will be in practice, and it's unclear when we are going to have the bandwidth to add support for this to mypy.

@ilevkivskyi
Copy link
Member

@JukkaL
I am interested in this not directly, but mostly because of python/typing#299 (allow subclassing Tuple and Callable). The point is that both of these trigger similar code in typing.py:

C = List[Tuple[T, T]]
C[int]
# and
class C(List[Tuple[T, T]]): ...
C[int]

Probably you are right that the first form might be of less priority for mypy. Both above forms are already allowed in typing.py, but the following forms are not supported:

class C(Tuple[T, T]): ...
class C(Tuple[T, ...]): ...
class C(Callable[[T], T]): ...

I would like to submit a PR to typing.py to allow these forms and using C[int] for them. As I understand your response on the mentioned issue, you are in favor of this. My idea is just to extend the GenericMeta infrastructure (__origin__, __args__, etc) on Tuple and Callable (these two could be seen as special variadic generics). As a side effect of this Tuple[T, T][int] will also work.

I think this is rather positive side effect. If people will ask for this and we decide to support this in mypy, then we would not need to change typing.py. Also IMHO generic aliases improve readability:

VecT = Iterable[Tuple[T, T]]
def dilate(vec: VecT, scale: T) -> VecT: ...
def rotate(vec: VecT, angle: T) -> VecT: ...
v: VecT[float] = []

@JukkaL
Copy link
Collaborator Author

JukkaL commented Oct 17, 2016

@ilevkivskyi Yeah, I'm not opposed to this.

By the way, shouldn't your example be written like this:

Vec = Iterable[Tuple[T, T]]
def dilate(vec: Vec[T], scale: T) -> Vec[T]: ...
def rotate(vec: Vec[T], angle: T) -> Vec[T]: ...
v: Vec[float] = []

(Based on what @ddfisher wrote above.)

@ilevkivskyi
Copy link
Member

@JukkaL

By the way, shouldn't your example be written like this:

Good point! Indeed, it looks much cleaner and Vec[T] is also supported in typing.py already.

@gvanrossum
Copy link
Member

gvanrossum commented Oct 17, 2016 via email

gvanrossum pushed a commit that referenced this issue Nov 3, 2016
Fixes #606 as per PEP 484.

Now type aliases may be generic, so that the example in PEP works. Generic type aliases are allowed for generic classes, unions, callables, and tuples. For example:

Vec = Iterable[Tuple[T, T]]
TInt = Tuple[T, int]
UInt = Union[T, int]
CBack = Callable[..., T]

The aliases could be used as specified in PEP 484, e.g. either one specifies all free type variables, or if unspecified they are all substituted by Any, for example:

def fun(v: Vec[T]) -> Vec[T]: # Same as Iterable[Tuple[T, T]]
    ...
v1: Vec[int] = []      # Same as Iterable[Tuple[int, int]]
v2: Vec = []           # Same as Iterable[Tuple[Any, Any]]
v3: Vec[int, int] = [] # Error: Invalid alias, too many type arguments!

Generic aliases may be used everywhere where a normal generic type could be used (in annotations, generic base classes etc, and as of recently also in runtime expressions). Nested and chained aliases are allowed, but excessive use of those is discouraged in the docs. Like ordinary (non-generic) aliases, generic ones may be imported from other modules.

NOTE: Many examples in the tests and in docs require a recent version of typing.py from python/typing to work at runtime (see #2382). This feature may be used with older versions of typing.py by using type comments or "forward references".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants