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.