radix / sumtypes

Sum Types, aka Tagged Unions, for Python
MIT License
42 stars 1 forks source link

Can we have a nicer `match` syntax? #4

Open radix opened 5 years ago

radix commented 5 years ago

How about:

with match(myobj):
    with case(MyClass.MyConstructor) as x:
        x.y
    with case(MyClass.MyOtherConstructor) as x:
        x.z

I think I could implement this, but I'm not sure how I would implement exhaustiveness checking.

bergus commented 4 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.

radix commented 4 years ago

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
radix commented 4 years ago

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.

radix commented 4 years ago

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.

aarondewindt commented 3 years ago

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.

aarondewindt commented 3 years ago

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.

radix commented 3 years ago

@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.