[Python-ideas] Yet Another Switch-Case Syntax Proposal
Devin Jeanpierre
jeanpierreda at gmail.com
Wed Apr 23 05:58:46 CEST 2014
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).
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