gvanrossum / patma

Pattern Matching
1.03k stars 65 forks source link

Consider object.__match__() #22

Open ilevkivskyi opened 4 years ago

ilevkivskyi commented 4 years ago

From my understanding of #8 the current proposal is that there is no object.__match__() so that by default trying to match against Foo() raises an error. In my draft notes I have some spec for object.__match__() because I think it is important to provide some "out of the box" experience, so that we don't force owners of existing codebases to add dummy @matchable class decorators to dozens of their classes.

I agree that in its current version my spec is an overkill (like it uses __slots__ as __match_args__ etc). But I think there is a way to provide a minimal safe object.__match__() implementation if we make one tweak to the spec: missing attribute should raise an exception rather than fail a match.

The main argument is that failing a match would hide hard to debug typos. Essentially, my idea is that matching x against Foo(bar=12) should be equivalent to isinstance(x, Foo) and x.bar == 12, and the latter would raise an exception in case of a typo in the attribute name. Also, it is rare for classes to have optional attributes unset, they are set to some default value (like None) much often. Finally, it is easy to override this behaviour by explicitly adding an attribute to __match_args__ indicating we shouldn't raise if such attribute was requested.

Note: this would be different from matching against a mapping, where not having a key is just a failed match. But I think this is normal, classes can behave stricter than just syntax sugar for dictionaries, more like TypedDicts in typing terminology for which the set of allowed keys is fixed.

If we agree with the stricter semantics then IIUC object.__match__() would just perform an isinstance() check and return self. So that match by name will work, but positional match will raise an exception. Btw I am not sure the latter is a conclusion of #8, but I think it probably should be. If the proxy object doesn't specify __match_args__, the positional match should raise (with an error message like <class 'Foo'> doesn't support match by position) rather than fail the match.

ilevkivskyi commented 4 years ago

Another minor thing, one can obviously opt-out of pattern matching using __match__ = None, similar to __hash__ = None.

gvanrossum commented 4 years ago

I am in full agreement here.

gvanrossum commented 4 years ago

Oh, object.__match__ must be a class method, else the isinstance() check has nothing to check for. (I think in your PEP, __match__ is implicitly a class method; I'm not sure I like that (I can imagine a static method being useful in some cases, or even an instance method).

ilevkivskyi commented 4 years ago

Just to clarify, what are the cases where static/instance methods will be useful? I am a bit uncomfortable with using instance method for this because if one writes:

class Foo:
    def __match__(self):
        ...

then in this code

x = 1
match x:
    as Foo():
        ...

we will call Foo.__match__(1), i.e. the self argument will be and int, not a Foo. So I imagine people who will read the code may be surprised by a check like if isinstance(self, Foo).

Also there are precedents where methods that are called on classes by the interpreter are implicitly class methods, like __new__(), __init_subclass__(), etc., and __match__() is very similar to __new__().

ilevkivskyi commented 4 years ago

I think I see a use case for instance methods: defining a __match__() in metaclass. Potentially some ORMs might want to do this.

gvanrossum commented 4 years ago

Also, EIBTI here, I think.