[Python-ideas] Yet Another Switch-Case Syntax Proposal

Andrew Barnert abarnert at yahoo.com
Wed Apr 23 01:20:15 CEST 2014


On Apr 22, 2014, at 15:06, Devin Jeanpierre <jeanpierreda at gmail.com> wrote:

> On Fri, Apr 18, 2014 at 9:54 PM, Andrew Barnert
> <abarnert at yahoo.com.dmarc.invalid> wrote:
>> Meanwhile, ML and friends let you go farther, matching by partial values:
>> 
>>    of (True, y):
>>        process(y)
>>    of Eggs(z):
>>        fry(z)
>> 
>> The first matches only if it's a tuple of exactly two values and the first
>> is equal to True, binding y to the second; the second matches only if it's
>> equal to an Eggs instance constructed with exactly one argument, and binds z
>> to that argument.
>> 
>> Clearly not all of this fits into Python. (The last example isn't even
>> conceptually possible, given the way object initialization works.) But it's
>> worth considering what does and what doesn't fit rather than trying to
>> design a feature without even knowing what the options are.
> 
> I'm really glad you brought this up, and it isn't conceptually
> impossible. Eggs(x) inside a pattern probably won't actually
> initialize an Eggs object; it probably would create a pattern object
> instead, and the value being matched can decide whether or not it
> obeys that pattern, and (if so) what object should be assigned to z.

That's a promising idea that I'd love to see fleshed out. Forgive me for dumping a whole lot of questions on you as I think this through...

First, does this really need to be restricted to case statements, or can constructor patterns just become another kind of target, which can be used anywhere (most importantly, assignment statements), and then cases just use the same targets? (Notice the potential parallels--and also differences--with __setattr__ and __setitem__ type assignments, not just tuple-style assignments.)

I can see how this could work for complete matching. Let's say I assign spam = Spam(2, 3), and I later try to match that against Spam(x, y) where x and y are not bound names. So, Python calls spam.__match__(Spam, 'x', 'y'), which returns {'x': 2, 'y': 3}, so the case matches and x and y get locally bound to 2 and 3. 

But what about partial matching? What if I want to match against Spam(2, y)? If you want that to work with literals, it might be possible. (Note that "2, y = eggs" is caught by the early stages of the compiler today, because the grammar doesn't allow literals in assignment targets.) Maybe something like spam.__match__(Spam, (2, True), ('y', False))? That's not conceptually too hard, but it would get really ugly inside the __match__ method of every class, unless you had some good types and helper functions similar to what you have in the opposite direction with inspect.Signature.bind.

If you want partial matching with dynamic values, however, it's a lot harder. You can't put expressions in target lists today, for obvious reasons (they have a partially parallel but simpler grammar), and targets are never treated as values directly. In particular, "x = 2" doesn't raise a NoItIsntError if x is already bound to 1, it just rebinds it. This means that if we do have partial patterns, they may still not be the same as (or as powerful as) you'd like. For example, if matching Spam(x, y) always matches and binds x as a name, rather than partially matching on its value (unless you can think of some syntax for that?) it's a lot harder to write dynamic patterns. (You can probably do something with partial, but that just makes matching even more complicated and leaves the syntax for using it in these cases unfriendly.)

Also, keep in mind that tuple-like targets always expand an iterable into a list, so "x, *y = range(3)" matches 0 and [1, 2], and "x, *y = itertools.count()" raises a MemoryError, so you're never going to have the full power of Haskell patterns here.

Beyond that, is there anything else to add to target lists besides call-like forms to match constructor patterns, or is that sufficient for everything you need from pattern matching?

> Why is it that every time someone brings up switch/case, they're being
> inspired by C and Java instead of ML or Haskell? People are looking
> backwards instead of forwards.

I agree, but it's certainly more complex, and harder to fit into Python. Thanks for showing me that it's not actually intractable, though...

> Python doesn't have a nice way of
> pattern matching on tagged unions in general, not just C-style enums
> -- and it really ought to. I think pattern matching could fit in
> Python, where switch doesn't really. Switch replaces if/elif, pattern
> matching replaces the if/elif plus any work unpacking contents. The
> format of a pattern match encourages code to be written in a way that
> disjointly separates groups of disparate values, in a way that isn't
> difficult and doesn't involve weird type checks or whatever.
> 
> Looking at the difference in e.g. AST manipulation code with/without
> is a nice example of how code can be made more readable. And looking
> at the problems Python code has today with this kind of thing is also
> illustrative -- for example, how do you tell the difference between no
> result, and a result whose value is None? In some APIs, you can't (!),
> in some, you need to specify a special sentinel object and check
> identity (ew), and in some, no result is represented by an exception
> -- which is fine, but in some cases very error prone (an uncaught
> StopIteration, for example, will terminate any generator that calls
> you, which can be difficult to notice.)   Pattern matching offers a
> real alternative option here that wasn't realistic before.

I think coming up with just enough of a proposal to then show how you could rewrite some existing AST (or DOM?) manipulation code, might make the case a lot better than trying to explain it.

If you're not up for doing that, I might try to tackle it, but it's your idea and you're the one with use cases in his head.

> But, maybe this is one of those things that won't happen because
> Haskell licked the cookie and now everyone thinks it's gross and
> unreadable, and can't be convinced otherwise. I'm more worried that
> people don't even know this exists...

The main objection to bringing in declarative features from functional languages, especially Haskell, is that it often forces people to read things at a higher level of abstraction. That isn't true for all such features (list comprehensions, anyone?), but it's a default assumption you have to overcome.

I think you can overcome it here; reading the actual __match__ methods is one thing, but the (presumably much more common, especially in novice code) uses of case statements or other matching targets can be just as explicit and concrete as if-based switching code, and a lot more readable. We just need to make that case for the specific feature strongly enough to overcome the general presumption. I think your AST example would go a long way toward doing that.



More information about the Python-ideas mailing list