On Thu, 16 Jul 2020 at 02:09, Guido van Rossum email@example.com wrote:
On Wed, Jul 15, 2020 at 4:41 PM Oscar Benjamin firstname.lastname@example.org wrote:
I've taken a look through PEP 622 and I've been thinking about how it could be used with sympy.
In principle case/match and destructuring should be useful for sympy because sympy has a class Basic which defines a common structure for ~1000 subclasses. There are a lot of places where it is necessary to dispatch on the type of some object including in places that are performance sensitive so those would seem like good candidates for case/match. However the PEP doesn't quite seem as I hoped because it only handles positional arguments indirectly and it does not seem to directly handle types with variadic positional args.
From a first glimpse of the proposal I thought I could do matches like this:
match obj: case Add(Mul(x, y), Mul(z, t)) if y == t: case Add(*terms): case Mul(coeff, *factors): case And(Or(A, B), Or(C, D)) if B == D: case Union(Interval(x1, y1), Interval(x2, y2)) if y1 == x2: case Union(Interval(x, y), FiniteSet(*p)) | Union(FiniteSet(*p), Interval(x, y)): case Union(*sets):
Knowing the sympy codebase each of those patterns would look quite natural because they resemble the constructors for the corresponding objects (as intended in the PEP). It seems instead that many of these constructors would need to have args= so it becomes:
match obj: case Add(args=(Mul(args=(x, y)), Mul(args=(z, t)))) if y == t: case Add(args=terms): case Mul(args=(coeff, *factors)): case And(args=(Or(args=(A, B)), Or(args=(C, D)))) if C == D: case Union(args=(Interval(x1, y1), Interval(x2, y2))) if y1 == x2: case Union(args=(Interval(x, y), FiniteSet(args=p))) | Union(args=(FiniteSet(args=p), Interval(x, y))): case Union(args=sets):
Each of these looks less natural as they don't match the constructors and the syntax gets messier with nesting.
That's a really interesting new use case you're bringing up.
You may have noticed that between v1 and v2 of the PEP we withdrew the `__match__` protocol; we've been brainstorming about different forms a future `__match__` protocol could take, once we have more practical experience. One possible variant we've been looking at would be something that would *only* be used for positional arguments -- `__match__` would just return a tuple of values extracted from the object that can then be matched by the interpreter's match machinery. Your use case could then (almost, see below) be handled by having `__match__` just return `self.args`.
That would work but something else just occurred to me which is that as I understand it the intention of __match_args__ is that it is supposed to correspond to the parameter list for __init__/__new__ like
class Thing2: __match_args__ = ('first', 'second')
def __init__(self, first, second): self.first = first self.second = second
That is deliberate so that matching can have a similar logic to the way that arguments are handled when calling __init__:
match obj: case Thing2(1, 2): case Thing2(1, second=2): case Thing2(first=1, second=2): ...
Maybe __match_args__ could have a way to specify the variadic part of a parameter list as well:
class ThingN: __match_args__ = ('first', 'second', '*rest')
def __init__(self, first, second, *rest): self.first = first self.second = second self.rest = rest
Then you can match with
match obj: case ThingN(1, 2): case ThingN(1, 2, 3): case ThingN(first, second, *rest): case ThingN(first, *second_and_rest): case ThingN(*allargs): ...
The normal restrictions for combinations of keyword and positional arguments could apply to the patterns as if __match_args__ was the parameter list for a function.
Perhaps the * in '*rest' isn't as clear between quotes and some more noticeable syntax could be used like
__match_args__ = ('first', 'second', ('rest',))