More natural type hints for built-in containers

Currently type hints for built-in containers are, in my opinion, less succint than required. I suspect it is probably not very difficult for a type checker to interpret something like this for example: var1: {str: (int, int)} = {"label": (1, 2)} var2: {str} = {"other_label"} def function(param1: {int: str} = {1: "foo"}, param2: (int, ...) = (1, 2, 3)) -> (int, (str, ...)): return 3, ("foo", "bar") as equivalent to: var1: dict[str, tuple[int, int]] = {"label": (1, 2)} var2: set[str] = {"other_label"} def function(param1: dict[int, str] = (1, "foo"), param2: tuple[int, ...] = (1, 2, 3)) -> tuple[int, tuple[str, ...]]: return 3, ("foo", "bar") I thought of posting something like this as a mypy feature suggestion, but I suspect the language would need to modify the way type annotations are interpreted for something like it to work (or maybe not?). Basically, inside a type annotation, the objects created by (a, b), [a], {a: b}, {a} would be reinterpreted as the same thing as constructing tuple[a, b], dict[a, b], list[a], set[a]. I have found myself wanting to write things like this a couple of times, so I think this overloaded usage of builtin containters is natural. I actually feel it is so natural it must either have been proposed before (in which case I would love to give the feature my +1) or there is some more or less obvious flaw.

I suggested this before in some typing meetup, but there are a few problems with it. One is that annotating arguments as "list" or "dict" is often the wrong thing to do: instead, people should use broader, immutable types like Iterable, Sequence, or Mapping, to avoid variance problems ( https://mypy.readthedocs.io/en/stable/common_issues.html#invariance-vs-covar... ). El mar, 27 jul 2021 a las 20:13, Ignacio Pickering (<ign.pickering@gmail.com>) escribió:

Haskell does something similar to this.. ls :: [String] ls = [“hello”, “world”] t :: (String, Int) t = (“hello”, 5) While at it, why don’t we pattern match function parameters like Haskel. Very elegant way for function overload. Something like this … case def func(int(x), [float(y), *_], int(z)) if z != 0 -> int: return int((x + y) / z) case def func(int(x), [float(y), *_], int(z)): -> int: return 0 case def func(_): raise # func(1, [1.2, 3, 5], 3) -> (1 + 1.2) / 3 # func(1, [1.2], 0) -> 0 # func("dummy", [1.2, 3, 5]) -> raise error No need to precede it with the “match” statement because we are pattern matching function parameters here in those cases. Also, annotating the parameters will be redundant. Abdulla

On Sun, Aug 01, 2021 at 07:42:07PM +0400, Abdulla Al Kathiri wrote:
While at it, why don’t we pattern match function parameters like Haskel. Very elegant way for function overload.
I don't think your example is very elegant:
That's a clear violation of DRY. The indentation is odd. And where does the docstring go? What will `func.__annotations__` be? It is especially awkward if you have a realistic function body rather than a single line statement, and the syntax is just begging to be mis-written as: case def func(...): body case def func(...): body def func(...): body especially if the bodies are realistically large.
What is so special about the case where you are matching on *exactly* the parameters as written? That may be common, but I don't think that it is so common that we need special syntax for it. A more general function would pattern match on only some of the parameters, with the other parameters used elsewhere in the function. Of course we can always just match the other parameters with the wild-card, but that's not very elegent. It also promotes function annotations from an optional type hint with no runtime semantics (other than setting `__annotations__`) to syntactic sugar for runtime function multiple dispatch. I think that would be far better written as a single function with the case inside the body. # I may have the syntax of the case statement wrong... def func(x, alist, z): # Type-hints remain optional. """Docstring goes here""" case (x, alist, z): match (int(x), [float(y), _], 0): return 0 match (int(x), [float(y), _], int(z)): return int((x+y)/z) match _: raise SomeException -- Steve

Yeah just switch between case and match. def func(x, alist, z): # Type-hints remain optional. """Docstring goes here""" match (x, alist, z): case (int(x), [float(y), _], 0): return 0 case (int(x), [float(y), _], int(z)): return int((x+y)/z) case _: raise SomeException Why don’t we just remove the match inside a function body if we are matching the function parameters? This way, we don’t repeat the parameters again and remove one level of indentation. So the rule like this, if the cases are inside the function, it’s implied we are pattern matching the parameters if we don’t specify the “match” statement. Of course if you opt to use the match statement, you can pattern match anything including the parameters themselves like the example above.

28.07.21 02:42, Ignacio Pickering пише:
The idea of having different syntax in annotations was rejected multiple times. You should have ability to refactor your code and introduce simple aliases for cumbersome constructions like Mapping[Tuple[int, str], Callable[[str, Tuple[int, str], Iterable[Mapping[str, Tuple[int, str]]]], List[Callable[[str], bool]]] (it is not too far from real world examples). And it includes parameterized types. MyMultiDict = dict[str, list[T]] MyStrMultiDict = MyMultiDict[str] MyIntMultiDict = MyMultiDict[int] Now, with your syntax: MyMultiDict = {str: [T]} MyStrMultiDict = MyMultiDict[str] # returns not what you want MyIntMultiDict = MyMultiDict[int] # KeyError So you would need principally different syntax in annotations and outside of annotations. It will cause issues in refactoring, and equal definitions would look completely unrelated.

I suggested this before in some typing meetup, but there are a few problems with it. One is that annotating arguments as "list" or "dict" is often the wrong thing to do: instead, people should use broader, immutable types like Iterable, Sequence, or Mapping, to avoid variance problems ( https://mypy.readthedocs.io/en/stable/common_issues.html#invariance-vs-covar... ). El mar, 27 jul 2021 a las 20:13, Ignacio Pickering (<ign.pickering@gmail.com>) escribió:

Haskell does something similar to this.. ls :: [String] ls = [“hello”, “world”] t :: (String, Int) t = (“hello”, 5) While at it, why don’t we pattern match function parameters like Haskel. Very elegant way for function overload. Something like this … case def func(int(x), [float(y), *_], int(z)) if z != 0 -> int: return int((x + y) / z) case def func(int(x), [float(y), *_], int(z)): -> int: return 0 case def func(_): raise # func(1, [1.2, 3, 5], 3) -> (1 + 1.2) / 3 # func(1, [1.2], 0) -> 0 # func("dummy", [1.2, 3, 5]) -> raise error No need to precede it with the “match” statement because we are pattern matching function parameters here in those cases. Also, annotating the parameters will be redundant. Abdulla

On Sun, Aug 01, 2021 at 07:42:07PM +0400, Abdulla Al Kathiri wrote:
While at it, why don’t we pattern match function parameters like Haskel. Very elegant way for function overload.
I don't think your example is very elegant:
That's a clear violation of DRY. The indentation is odd. And where does the docstring go? What will `func.__annotations__` be? It is especially awkward if you have a realistic function body rather than a single line statement, and the syntax is just begging to be mis-written as: case def func(...): body case def func(...): body def func(...): body especially if the bodies are realistically large.
What is so special about the case where you are matching on *exactly* the parameters as written? That may be common, but I don't think that it is so common that we need special syntax for it. A more general function would pattern match on only some of the parameters, with the other parameters used elsewhere in the function. Of course we can always just match the other parameters with the wild-card, but that's not very elegent. It also promotes function annotations from an optional type hint with no runtime semantics (other than setting `__annotations__`) to syntactic sugar for runtime function multiple dispatch. I think that would be far better written as a single function with the case inside the body. # I may have the syntax of the case statement wrong... def func(x, alist, z): # Type-hints remain optional. """Docstring goes here""" case (x, alist, z): match (int(x), [float(y), _], 0): return 0 match (int(x), [float(y), _], int(z)): return int((x+y)/z) match _: raise SomeException -- Steve

Yeah just switch between case and match. def func(x, alist, z): # Type-hints remain optional. """Docstring goes here""" match (x, alist, z): case (int(x), [float(y), _], 0): return 0 case (int(x), [float(y), _], int(z)): return int((x+y)/z) case _: raise SomeException Why don’t we just remove the match inside a function body if we are matching the function parameters? This way, we don’t repeat the parameters again and remove one level of indentation. So the rule like this, if the cases are inside the function, it’s implied we are pattern matching the parameters if we don’t specify the “match” statement. Of course if you opt to use the match statement, you can pattern match anything including the parameters themselves like the example above.

28.07.21 02:42, Ignacio Pickering пише:
The idea of having different syntax in annotations was rejected multiple times. You should have ability to refactor your code and introduce simple aliases for cumbersome constructions like Mapping[Tuple[int, str], Callable[[str, Tuple[int, str], Iterable[Mapping[str, Tuple[int, str]]]], List[Callable[[str], bool]]] (it is not too far from real world examples). And it includes parameterized types. MyMultiDict = dict[str, list[T]] MyStrMultiDict = MyMultiDict[str] MyIntMultiDict = MyMultiDict[int] Now, with your syntax: MyMultiDict = {str: [T]} MyStrMultiDict = MyMultiDict[str] # returns not what you want MyIntMultiDict = MyMultiDict[int] # KeyError So you would need principally different syntax in annotations and outside of annotations. It will cause issues in refactoring, and equal definitions would look completely unrelated.
participants (5)
-
Abdulla Al Kathiri
-
Ignacio Pickering
-
Jelle Zijlstra
-
Serhiy Storchaka
-
Steven D'Aprano