
Matthew Rahtz wrote:
## Function arguments ... Conclusion: sometimes alright, sometimes not. ## Function returns ... Conclusion: always fine. ## Classes ... Conclusion: never alright.
While I agree with most of the analysis here, I think some of the conclusions can be further amended. With higher-order functions, one can easily move function returns into parameter position and function parameters into return position. So whether the concatenation is fine or not is not really tied to whether it occurs at parameter or return position. For example: ``` def higher_order() -> Callable[[Tuple[*Ts1, *Ts2]], None]: ... ``` This function put the concatenation at the return type, but it runs into the same problem as your example 1: we have no way to decide which variables are bound to Ts1 and which are bound to Ts2 inside the function. Similar trick can be played to flip the parameters/returns in your example 2, 3, and 4. As a result, I think it would be really hard to implement your Option 2 since there is no easy way of telling when it's always unambiguous by only looking at the parameter/return positions. Regarding concatenation on classes, I do think there's a way to add extra contexts via method calls: ``` class C(Generic[*Ts1, *Ts2]): def __init__(self, t0: Tuple[*Ts1], t1: Tuple[*Ts2]) -> None: ... def test() -> None: reveal_type(C((1, "a"), ("b", 2))) # should be C[int, str, str, int] ``` It is true that for class `C` you will never be able to spell out any explicit type annotations (e.g. one can't just write `c: C[int, str, int]`since there's no way to disambiguate Ts1 and Ts2), but other than that things shouldn't work too differently from variadic functions. That said, I suspect allowing this is going to make type inference on the caller side a bit trickier, so no strong opinions on banning it as well. Another possibility is to make this an optional feature so each type checker can choose to implement it or not. Overall I would champion either your Option 1 and Option 3. Slightly in favor of Option 3 for more flexibility.
If, for some reason, we did want a class that was generic in multiple type tuple variables, the current proposal in the PEP is: Example 6: 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.
Thoughts?