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

Devin Jeanpierre jeanpierreda at gmail.com
Wed Apr 23 08:13:44 CEST 2014


On Tue, Apr 22, 2014 at 8:58 PM, Devin Jeanpierre
<jeanpierreda at gmail.com> wrote:
> On Tue, Apr 22, 2014 at 4:20 PM, Andrew Barnert <abarnert at yahoo.com> wrote:
>> On Apr 22, 2014, at 15:06, Devin Jeanpierre <jeanpierreda at gmail.com> wrote:
>>> 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.)
>
> Making assignment statements do a lot of work would slow down Python
> way too much. The restricted sort of destructuring bind python already
> does is sufficiently limited that the compiler can do all the heavy
> lifting.
>
>> 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.
>
> That's a bit complicated, and another example of when a simpleish API
> is made hard without nice union types. Why not use pattern matching?
> :)
>
> spam.__match__(ConstructorPattern(Spam, Var('x'), Var('y'))) and
> spam.__match__(ConstructorPattern(Spam, Literal(2), Var('y')))
>
> in __match__ you can check "match pat: case ConstructorPattern(t,
> Var(a), Var(b)): if t == Spam: ...", etc. (This is not cyclic; the
> base cases are the __match__ for ConstructorPattern, Var, and Literal,
> which can do some difficult manual work instead).

Sorry, this is unsound / doesn't specify where the recursion is
allowed to happen. Instead, have __match__ accept a constructor and a
list of identifiers and return a dict mapping identifiers to values --
it never sees anything other than variables in the constructor
parameters. (No literals.)

If you try to match foo with Spam(2, y), then __match__ gets Spam,
generated_variable_12345, and y. When it fills in
generated_variable_12345, the thing it assigns to that variable is
subsequently pattern matched against 2 (probably using __eq__ instead
of an overloaded __match__).

Stupid mistake I make a lot when I'm not thinking about recursive
algorithms... :(

-- Devin

> In most cases people would not be writing __match__ by hand, so it
> doesn't need to be easy to write. You'd probably be defining matchable
> cases using something like what Haoyi Li wrote, except with a
> namedtuple-analogue instead of AST macros:
>
> Nil = constructor('Nil', ())
> Cons = constructor('Cons', ('x', 'xs'))
> value = Cons('hello world', Nil())
> match value:
>     case Cons(a, Nil()):
>         print(a) # a == value.x == 'hello world'
>
>
>> 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.)
>
> I am not sure what you're getting at here, exactly. x should never be
> asked if it's OK to do x = 2. If you want to use variables as values
> to compare with, rather than targets to assign to, usually that's done
> by adding a conditional. Although, Racket has a match extension for
> using a variable to check for equality in a pattern match, like so:
>
>> (define (check_eq x y)
>     (match x
>         [ (== y) (displayln "x == y")]
>         [ _ (displayln "x != y")]))
>> (check_eq 1 1)
> x == y
>> (check_eq 1 2)
> x != y
>
> One could add such a thing (maybe $x instead of (== x) ? ;O), or use
> guard clauses, or just use a nested if.
>
>> 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?
>
> You haven't presented an organized list :(
>
> I think a good set of features is:
>
> - variables (e.g. matching against x)
> - constructor/term patterns (e.g. matching against foo(a, b))
> - literals (e.g. matching against (a, b) or even (a, 2))
> - repetition operator (e.g. matching against *a) # nonessential, but powerful
> - combining patterns (e.g. match against foo(a) or bar(a, 2)) # convenient
> - binding subpatterns to names (e.g. match against foo(bar(z) as x);
> now x equals the thing that matched bar(z)) # convenient
>
> And take the transitive closure of combining those operations. That's
> a pretty big wishlist, with implications that maybe would weird people
> out, so I won't explain the details. They aren't important.
>
>> 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.
>
> I doubt such a proposal would be accepted, but I am OK writing what I
> did above and showing an example.
>
> compare http://hg.python.org/cpython/file/v2.7.3/Lib/ast.py#l52 with:
>
> def _convert(node):
>     match node:
>         case Str(v) or Num(v):
>             return v
>         case Tuple(vs):
>             return tuple(map(_convert, vs))
>         case List(vs):
>             return list(map(_convert, vs))
>         case Dict(_):
>             return dict((_convert(k), _convert(v)) for k, v
>                         in zip(node.keys, node.values))
>         case Name(id):
>             if id in _safe_names:
>                 return _safe_names[id]
>         case BinOp(Num(left), op, Num(right)):
>             if isinstance(left, (int,long,float)) and
> isinstance(right, (int,long,float))):
>                 match op:
>                     case Add():
>                         return left + right
>                     case Sub():
>                         return left - right
>         case _: pass # presumably failure to match is an error? idk
>
>     raise ValueError('malformed string')
>
> This is definitely more readable, in particular in the BinOp case. And
> you'll note that a mere switch statement really isn't very helpful. ;)
>
> -- Devin


More information about the Python-ideas mailing list