If we wanted match-case
for def
, I’d think of it in terms of “how to define match-def
or case-def
”, in which the cases are different suites for the function body. This is aligned with the functional languages which have destructuring matches baked into function definition syntax.
Although I’m not sure how much I like the results yet, playing with this looks interesting to me.
My first thought is to inline match def
to mean “a function whose body starts with a match
on its parameters”, e.g.,
match def simple_euclidean(
p0: tuple[float, float],
p1: tuple[float, float],
) -> float:
case ((x0, y0), (x1, y1)):
return math.sqrt(
(x1 - x0) ** 2 + (y1 - y0) ** 2
)
case _:
raise TypeError(f"Expected two 2-tuples, got: {p0!r}, {p1!r}")
That is practically the same as we can already write today, so it doesn’t seem to me like anyone would be satisfied with it. But perhaps the match
itself should be dropped, so that the def
line is the pattern match?
case def simple_euclidean(
(x0: float, y0: float), (x1: float, y1: float)
) -> float:
return math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
case def simple_euclidean(
(x0: float, y0: float, z0: float), (x1: float, y1: float, z1: float)
) -> float:
return math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2 + (z1 - z0) ** 2)
That reads very nicely to me, but I don’t like the repetition of def simple_euclidean
and the return type.
And I wonder about what case _
should mean (or if it should even be supported).
If we go back to match def
, but use case
on the parameters, some interesting possibilities emerge:
match def simple_euclidean -> float:
case (
(x0: float, y0: float), (x1: float, y1: float)
):
return math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
case (
(x0: float, y0: float, z0: float), (x1: float, y1: float, z1: float)
):
return math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2 + (z1 - z0) ** 2)
What I like about this option is that it allows you to express things unrelated to the unpacking case which currently require typing.overload
with a little tweak to that return type annotation.
class StringHolder:
match def get_value:
case (self, key: str) -> str:
return self.data[key]
case (self, key: str, *, converter: Callable[[str], T]) -> T:
return converter(self.data[key])
I like this last one. It’s the sort of code that I’ve felt subtle pressure to stop writing as type annotations have become part of my day-to-day, since the overload syntax makes it “too costly” to be worth combining APIs where it’s easily avoidable. (Instead I’ll write two methods, get_value
and get_and_convert_value
, or whatever is appropriate to the problem space.)
As for case _
, I omit it because the natural default here would be a TypeError
. Arguably, the fallthrough case here would be *args, **kwargs
anyway, so we could write things like…
class StringHolder:
match def get_value:
case (self, key: str) -> str:
return self.data[key]
case (self, key: str, *, converter: Callable[[str], T]) -> T:
return converter(self.data[key])
case (self, key: str, *args: Any, converter: Callable[[str], T], **kwargs: Any) -> T:
warnings.warn(
f"unrecognized parameters detected: {args!r}, {kwargs!r}"
)
return converter(self.data[key])
case (self, key: str, *args: Any, **kwargs: Any) -> str:
warnings.warn(
f"unrecognized parameters detected: {args!r}, {kwargs!r}"
)
return self.data[key]
Existing match-case
unpacking covers parts of this, but I think the dict unpacking requirements for keyword arguments would be largely new.