Open radix opened 5 years ago
I guess if you implement a global stack to enable case
know in which match
statement it is located, you can also keep track of met cases inside the match
contextmanager and throw on exit if not all possibilities were exhausted.
looking back on this, this could also work, without any global stack:
with match(myobj) as M:
with M.case(MyClass.MyConstructor) as x:
return x.y
with M.case(MyClass.MyOtherConstructor) as x:
return x.z
Actually, I don't think this is possible, because I don't think context managers have any way to stop their code block from being run.
unless you commit crimes like what this PR for adt
does: https://github.com/jspahrsummers/adt/pull/38 -- basically using exceptions and python's ability to mutate the calling frame to jump around in the code.
Hey hey hey, I'm pround of the atrocities I've commited in that PR, but I wouldn't recomend it though. My use of the sys.settrace
function is questionable at best, will probably not do it's job in some rare cases and is guaranteed to mess up some debugger at one point. Unfortunately I've not been able to find some other way to block context managers from running their code, but I'll let you know if I find a better way.
I've been thinking of some other ideas though. If we're assuming python 3.8 or higher than something like this could be an option.
with object:
if case(case_a_value := object.case_a):
...
if case(case_b_value := object.case_b):
...
if case(x := object.case_c):
case_c_1, case_c_2 = x
...
You'll still need to unpack multiple values inside the if because the walrus operator does not support unpacking (case c).
I don't think you would need a global stack, the context manager would put the object in a state where it would track access to the cases by using the descriptor protocol.
I think your solution with the decorator is the nicest looking, reliable and backwards compatible option I've seen so far. Do you mind if I experiment with it at adt
? I'll give credit for the idea.
Edit: Removed the elif
's, the object won't be able to check whether all cases where covered if the value isn't requested from every case.
Maybe important to add. The case(...)
function is needed because the object.case
may contain a falsy value (eg, False
, None
, 0, etc). So my idea is to let the object.case
return either the value or a Mismatch
singleton object. The case function would then return True if it got a value or False if it got the Mismatch singleton. The value itself would already be in the variable thanks to the walrus operator.
@aarondewindt Yes, you can of course feel free to take any ideas (or code) you want from sumtypes. I would honestly be happy to just deprecate sumtypes if adt can already do everything that sumtypes does.
One of the most annoying things I see with the decorator approach is that it's not really a match expression, it's a way of defining functions whose bodies are entirely match statements. So it's not that great for inline expressions, and also not great for one-off matches in the context of a larger function. Granted, the with
syntax I discussed above also isn't an expression, but at least it's immediately executed.
The other most annoying thing about it is that using class
is just ugly/confusing in general.
I guess it could also be possible to define a decorator which returns the result of immediately evaluating the cases.... but that would read very wierdly:
value = Enum.CaseA(1)
@match_immediate(value)
class foo_result:
def CaseA(x): return x
def CaseB(x, y): return y
assert foo_result == 1
yuck.
How about:
I think I could implement this, but I'm not sure how I would implement exhaustiveness checking.