Skip to content

Alternative notations for covariance and contravariance #211

Closed
@ilevkivskyi

Description

@ilevkivskyi

NOTE: The Idea described here was discussed in the context of #2 , but somehow was forgotten.
Here I would like to revive the discussion of this particular idea.

I was writing a text explaining variance of generic types and I have found something that is quite inconvenient and confusing form me in the current notations for variance. The key point is that covariance, contravariance, or invariance are the properties of generics, not of type variables. As currently agreed, only the declaration-site variance is supported. Yet, something like this is currently not prohibited by the PEP:

def fun(lst: List[T_contra]) -> T_contra:
    ...

It is quite unclear what that would mean. There is another problem, if one sees a type variable, then unless it has a self-explanatory name, it is not clear whether it is covariant, invariant, or contravariant, one needs to scroll up to its definition.

I would like to propose to change slightly the notation. Namely, remove covariant and contravariant keyword arguments from TypeVar constructor. Instead, implement __pos__ and __neg__ methods for unary plus and minus. Pluses and minuses should be allowed only in the definition/declaration of a generic type. So that one could write

class MyClass(Generic[+T, U, -S], Mapping[T, int], Set[S]):

    def my_method(self, lst: List[T]) -> S:
        ...

and this would mean that:

  1. MyClass[t, u, s] is a subtype of Mapping[t, int] and a subtype of Set[s], i.e. could be used in place of function parameters with such declared types.
  2. MyClass[t1, u, s] is a subtype of MyClass[t2, u, s] if t1 is a subtype of t2 (covariance).
  3. MyClass[t, u, s2] is a subtype of MyClass[t, u, s1] if s1 is a subtype of s2 (contravariance).
  4. MyClass is invariant in second type argument.

Also, as discussed in #115 a shorter notation (ommiting Generic) could be used in simple cases:

class MyClass(Mapping[+T, int]):

    def my_method(self, lst: List[T]) -> int:
        ...

At the same time, pluses and minuses in type annotations of functions and variables should be prohibited by the PEP:

def fun(lst: List[-T]) -> -T: # Error
    ...

I think that the new notation would be especially clearer in situation with complex indexed generic types. One could be easily confused by

it = [] # type: Iterable[Tuple[T_contra, T_co]]

how could it be contravariant if Iterator is defined as covariant? Such confusions will be eliminated with the new notations. Also, it would be simpler to understand for people familiar with other languages like C#, Scala or OCaml.

EDITED: there was a typo in point 1, and a mistake in the short example

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions