[Python-ideas] Match statement brainstorm

229 views
Skip to first unread message

Guido van Rossum

unread,
May 19, 2016, 12:16:11 AM5/19/16
to Nick Coghlan, python-ideas
On Wed, May 18, 2016 at 8:17 PM, Nick Coghlan <ncog...@gmail.com> wrote:
> On 19 May 2016 at 08:53, Guido van Rossum <gu...@python.org> wrote:
>> The one thing that Python doesn't have (and mypy doesn't add) would be
>> a match statement. The design of a Pythonic match statement would be
>> an interesting exercise; perhaps we should see how far we can get with
>> that for Python 3.7.
>
> If it's syntactic sugar for a particular variety of if/elif/else
> statement, I think that may be feasible, but you'd presumably want to
> avoid the "Can we precompute a lookup table?" quagmire that doomed PEP
> 3103's switch statement.
>
> That said, for the pre-computed lookup table case, whatever signature
> deconstruction design you came up with for a match statement might
> also be usable as the basis for a functools.multidispatch() decorator
> design.

Let's give up on the pre-computed lookup table. Neither PEP 3103 nor
PEP 275 (to which it compares itself) even gets into the unpacking
part, which would be the interesting thing from the perspective of
learning from Sum types and matching in other languages. Agreed on the
idea of trying to reuse this for multidispatch!

A few things that might be interesting to explore:

- match by value or set of values (like those PEPs)
- match by type (isinstance() checks)
- match on tuple structure, including nesting and * unpacking
(essentially, try a series of destructuring assignments until one
works)
- match on dict structure? (extension of destructuring to dicts)
- match on instance variables or attributes by name?
- match on generalized condition (predicate)?

The idea is that many of these by themselves are better off using a
classic if/elif/else structure, but a powerful matching should be
allowed to alternate between e.g. destructuring matches and value or
predicate matches. IIUC Haskell allows pattern matches as well as
conditions, which they seem to call guards or where-clauses (see
https://www.haskell.org/tutorial/patterns.html, or
http://learnyouahaskell.com/syntax-in-functions if you like your pages
more colorful). Or maybe we should be able to combine structure
matches with guards.

I guess the tuple structure matching idea is fairly easy to grasp.

The attribute idea would be similar to a duck-type check, though more
emphasizing data attributes. It would be nice if we could write a
match that said "if it has attributes x and y, assign those to local
variables p and q, and ignore other attributes". Strawman syntax could
be like this:

def demo(arg):
switch arg:
case (x=p, y=q): print('x=', p, 'y=', q)
case (a, b, *_): print('a=', a, 'b=', b)
else: print('Too bad')

Now suppose we had a Point defined like this:

Point = namedtuple('Point', 'x y z')

and some variables like this:

a = Point(x=1, y=2, z=3)
b = (1, 2, 3, 4)
c = 'hola'
d = 42

then we could call demo with these variables:

>>> demo(a)
x= 1 y= 2
>>> demo(b)
a= 1 b= 2
>>> demo(c)
a= h b= o
>>> demo(d)
Too bad
>>>

(Note the slightly unfortunate outcome for 'hola', but that's what a
destructuring assignment would do. Water under the bridge.)

Someone else can try to fit simple value equality, set membership,
isinstance, and guards into that same syntax.

--
--Guido van Rossum (python.org/~guido)
_______________________________________________
Python-ideas mailing list
Python...@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/

Greg Ewing

unread,
May 19, 2016, 4:20:53 AM5/19/16
to python-ideas
Guido van Rossum wrote:
> def demo(arg):
> switch arg:
> case (x=p, y=q): print('x=', p, 'y=', q)
> case (a, b, *_): print('a=', a, 'b=', b)
> else: print('Too bad')

I would also add

case Point(x=p, y=q): print('Point: x=', p, 'y=', q)

Then if you also had

Vector = namedtuple('x,y,z')

then this case would match a Point but not a Vector.

However, there's a problem with all this if you want to
allow matching on specific values as well as structure.
Haskell allows you to say things like

myfunc [17, 42, x] = ...

which will only match a 3-element list whose first 2
elements are 17 and 42.

This would be fine in Python as long as the constants are
literals. But if you want to name the constants,

Seventeen = 17
FortyTwo = 42

case [Seventeen, FortyTwo, x]:

it becomes ambiguous. Are the identifiers values to be
matched or names to be bound?

If we want this feature, it seems like we will need to
explicitly mark either names to be bound or expressions to
be evaluated and matched against.

At first I thought maybe a name could be enclosed in parens
to force it to be treated as an expression:

case [(Seventeen), (FortyTwo), x]:

but that wouldn't quite be consistent with assignment
syntax, because (x) is actually valid on the left of an
assignment and is equivalent to just x.

>>>>demo(c)
>
> a= h b= o
>
> (Note the slightly unfortunate outcome for 'hola',

If you wanted the second case to only match the tuple,
you could write

case tuple(a, b, *_):

--
Greg

Ian Kelly

unread,
May 19, 2016, 10:48:29 AM5/19/16
to python-ideas
On Wed, May 18, 2016 at 10:15 PM, Guido van Rossum <gu...@python.org> wrote:
> I guess the tuple structure matching idea is fairly easy to grasp.
>
> The attribute idea would be similar to a duck-type check, though more
> emphasizing data attributes. It would be nice if we could write a
> match that said "if it has attributes x and y, assign those to local
> variables p and q, and ignore other attributes". Strawman syntax could
> be like this:
>
> def demo(arg):
> switch arg:
> case (x=p, y=q): print('x=', p, 'y=', q)
> case (a, b, *_): print('a=', a, 'b=', b)
> else: print('Too bad')

That the first case would match attributes and not named items seems
surprising to me. That is, just by looking at it I would have expected
it to match {'x': 1, 'y': 2}. This feels more in line with the way
that keyword arguments work, and more consistent with the second case
in that both would be matching contents of collections.

Sven R. Kunze

unread,
May 19, 2016, 6:35:23 PM5/19/16
to python...@python.org
On 19.05.2016 16:46, Ian Kelly wrote:
> On Wed, May 18, 2016 at 10:15 PM, Guido van Rossum <gu...@python.org> wrote:
>> I guess the tuple structure matching idea is fairly easy to grasp.

That's true. So, I would love to have more capabilities baked into tuple
structure matching.

>> The attribute idea would be similar to a duck-type check, though more
>> emphasizing data attributes. It would be nice if we could write a
>> match that said "if it has attributes x and y, assign those to local
>> variables p and q, and ignore other attributes". Strawman syntax could
>> be like this:
>>
>> def demo(arg):
>> switch arg:
>> case (x=p, y=q): print('x=', p, 'y=', q)
>> case (a, b, *_): print('a=', a, 'b=', b)
>> else: print('Too bad')

However, combining this (the tuple structure matching) with a
switch-case statement is quite some change to my taste especially
regarding Python's history with the switch-case statement.

Maybe, that is not necessary and we can introduce more features to tuple
structure matching like Erlang has without introducing switch-case. I
think it's more powerful and flexible on its own.

> That the first case would match attributes and not named items seems
> surprising to me. That is, just by looking at it I would have expected
> it to match {'x': 1, 'y': 2}. This feels more in line with the way
> that keyword arguments work, and more consistent with the second case
> in that both would be matching contents of collections.

Maybe, here the .x and .y syntax, which has been proposed previously
(another with-statement enhancement proposal), could come in handy. This
would definitely refer to attribute matching. Matching via quotation
marks (" or ') would rather hint at dictionary keys.


Best,
Sven

Nick Coghlan

unread,
May 19, 2016, 10:38:25 PM5/19/16
to Guido van Rossum, python-ideas
On 19 May 2016 at 14:15, Guido van Rossum <gu...@python.org> wrote:
> The attribute idea would be similar to a duck-type check, though more
> emphasizing data attributes. It would be nice if we could write a
> match that said "if it has attributes x and y, assign those to local
> variables p and q, and ignore other attributes". Strawman syntax could
> be like this:
>
> def demo(arg):
> switch arg:
> case (x=p, y=q): print('x=', p, 'y=', q)
> case (a, b, *_): print('a=', a, 'b=', b)
> else: print('Too bad')

For the destructuring assignment by attribute, I'd suggest the "value
as name" idiom, since it's not quite a normal assignment, as well as a
leading "." to more readily differentiate it from iterable unpacking:

def demo(arg):
switch arg:
case (.x as p, .y as q): print('x=', p, 'y=', q)
case (a, b, *_): print('a=', a, 'b=', b)
else: print('Too bad')

Whether to allow ".x" as a shorthand for ".x as x" would be an open question.

> Someone else can try to fit simple value equality, set membership,
> isinstance, and guards into that same syntax.

For these, I'd guess the most flexible option would be to allow the
switch expression to be bound to a name:

switch expr as arg:
case arg == value: ...
case lower_bound <= arg <= upper_bound: ...
case arg in container: ...

Similar to with statement and for loops, this wouldn't create a new
scope, it would just bind the name in the current scope (and hence the
value would remain available after the switch statement ends)

If we went down that path, then the "assign if you can, execute this
case if you succeed" options would presumably need an explicit prefix
to indicate they're not normal expressions, perhaps something like
"?=":

switch expr as arg:
case ?= (.x as p, .y as q): print('x=', p, 'y=', q)
case ?= (a, b, *_): print('a=', a, 'b=', b)
case arg == value: ...
case lower_bound <= arg <= upper_bound: ...
case arg in container: ...
else: print('Too bad')

Which would then have the further implication that it might also make
sense to support attribute unpacking as the LHS of normal assignment
statements:

(.x as p, .y as q) = expr

In a similar vein, item unpacking might look like:

(["x"] as p, ["y"] as q) = expr

And unpacking different structures might look like:

switch expr:
case ?= (.x as x, .y as y): ... # x/y as attributes
case ?= (["x"] as x, ["y"] as y): ... # x/y as mapping
case ?= (x, y): ... # 2-tuple (or other iterable)

Cheers,
Nick.

--
Nick Coghlan | ncog...@gmail.com | Brisbane, Australia

Fabrizio Messina

unread,
May 20, 2016, 4:03:30 AM5/20/16
to Nick Coghlan, python-ideas
why the unpacking has to happen automatically?

I would prefer something like:
switch *args:
case:...

Also I'm not a big fan of adding two new keywords to the syntax, I would think something like:

switch *args:
           &(a==1, ...) as a, *b:
# code
&(a,b) as a, b:
# code
&(a,b, ...) as a, b, *c:
# code
&(...) as a:
# code

This would reduce the numbers of new keywords needed to 1, it would make sense to use the & operator because all the conditions have to be TRUE and this use at the moment raises SyntaxError.
For how I see it could also make sense to be able to pass the arguments to a callable.

switch *args:
           &(a==1, ...): (lambda a, *b: ...)
&(a,b): (lambda a, b: [a, b])
&(a,b, ...): (lambda a, b, *c: [ a+1, b+1, *c])
&(...) as a: (lambda *a: [*a])

Franklin? Lee

unread,
May 20, 2016, 6:56:44 AM5/20/16
to Greg Ewing, python-ideas
I think there should be different syntaxes for matching equality and
binding patterns, and definitely different syntax for singular and
plural cases.

Let's try this:
- Equality:
`case 5:`
- Conditional:
`case if predicate(obj):`
- Pattern-matching:
`case as [a, b, *_]:`
`case as Point(x, y):`

Slightly-more-explicit checks, instead of simply `case 5:`.
- `case == 5:`
- `case is _sentinel:`
- `case is None:`
- `case < 6:`
- `case in (1,2,3):`
- `case in range(2, 5):`
- `case in int:`

The above uses an implicit object and is very different from how
Python usually works. Though I think it's mentally consistent (as in,
no contradictions), it's not an extrapolation of current syntax, and
might go against "explicit self". It isn't necessary, though: require
`case 5` and `case if obj == 5:`. I prefer `case == 5` and `case is
5`, though, even if that's not how other languages do it.

The last one treats a type as a collection of its instances, which I
just like. (I also like `SubClass < BaseClass`.)


On Thu, May 19, 2016 at 4:20 AM, Greg Ewing <greg....@canterbury.ac.nz> wrote:
> If we want this feature, it seems like we will need to
> explicitly mark either names to be bound or expressions to
> be evaluated and matched against.

Possible pre-fix markers:
case as Point(zero, =y):
case as Point(zero, !y):
case as Point(zero, $y):
case as Point(zero, &y):
case as Point(zero, ^y):
case as Point(zero, :y):

Possible post-fix markers:
case as Point(zero, y=):
case as Point(zero, y!):
case as Point(zero, y:):

I think the colon won't be ambiguous in dict displays (it's only a
marker if one side of the colon is missing), but it might complicate
the parser. It'd be ambiguous in slicing, but I don't see
slice-matching being a thing.

(My favorite is probably `&y`, for a bad reason: it's very C-like.
That'd confuse C programmers when `&` doesn't work anywhere else and
`*` won't work at all.)


> If you wanted the second case to only match the tuple,
> you could write
>
> case tuple(a, b, *_):

Slightly naughty. The tuple constructor only takes one argument.


On Thu, May 19, 2016 at 12:15 AM, Guido van Rossum <gu...@python.org> wrote:
> A few things that might be interesting to explore:

I'll try out my syntax ideas on these.

> - match by value or set of values (like those PEPs)

`case == 1:`
`case in (1, 2, 3):`

> - match by type (isinstance() checks)

`case in int:`
`case if isinstance(obj, int):`
`case if type(obj) == tuple:`

> - match on tuple structure, including nesting and * unpacking
> (essentially, try a series of destructuring assignments until one
> works)

`case in tuple as (first, *middle, last):`
`case if isinstance(obj, tuple) as (first, *middle, last):`
`case if type(obj) == tuple as (first, *middle, last):`

> - match on dict structure? (extension of destructuring to dicts)

I think it makes no sense to bind the keys to names, because
they'd just be chosen arbitrarily (unless it's an OrderedDict), so
let's say keys are evaluated and values are (by default) names to
bind.

Let `**_` mean "other items".

`case as {'foo': x, 'bar': y, **_}:`
`case as {key0: val0, key1: val1}: # Binds val0 and val1.`
`case as {'foo': foo_val, var_holding_bar: bar_val, **_}:`
^ Ew. I'd like names-to-bind to require special syntax.
`case as dict(foo=foo_val, **{var_holding_bar: bar_val}, **_):`

If we had an OrderedDict syntax, binding keys makes more sense.
case as [k0: v0, **_, klast: vlast]:

P.S.: If `**_` is allowed in matching, it should be allowed in
unpacking assignment.
{'foo': x, 'bar': y, **_} = d


> - match on instance variables or attributes by name?

One of the following:
`case as object(foo=x, bar=y):`
`case as Object(foo=x, bar=y):`
`case as SpecialAttrMatchingThing(foo=x, bar=y):`

`SpecialAttrMatchingThing` objects would be special in the match system.

> - match on generalized condition (predicate)?

`case <= 4:`
`case if is_something(the_obj):`
`case as Point(x, y) if x == y:`
`case if len(the_obj) < 5 as [first, second, *_] if isinstance(first, int):`


== Matching user classes ==

What about using pickle dunders for this? In particular,
`MyClass.__getnewargs__` and `MyClass.__getnewargs_ex__`. Pickling is
kind of related to pattern matching. Note that the classes don't have
to be immutable to be matchable.

When matching `p` to `Point(x, y)`, the match system calls
`Point.__getnewargs__(p)` (NOT `p.__getnewargs__`, thus allowing for
some subclass matching). The result is matched against a two-tuple,
and finally bound.

# def match_constructor(obj, cls):
if not isinstance(obj, cls):
raise FailedMatch
try:
m = cls.__getnewargs_ex__
except AttributeError:
pass
else:
args, kwargs = m(obj) # Even for subclasses.
return args, kwargs
try:
m = cls.__getnewargs__
except AttributeError:
raise FailedMatch
args = m(obj)
return args, {}

# Back in the case:
try:
(x, y), [] = match_constructor(p, Point)
except ValueError:
raise FailedMatch
run_block()


The problem is that these functions return (args, kwargs), and
optional args and pos_or_kw_params mess things up.

Point(x, y)
Point(x, y=y)
# How can match system know that all of the following are
valid patterns?
Something(x)
Something(x, y)
Something(*args, **kwargs)

Solution 1: Additional args to the pickle methods (or make a new
method which could sometimes be used for pickling):
- nargs: Number of args. An int, or a integer range (for `*args`).
A `range` doesn't allow unbounded, so use `slice(min_nargs, None)`,
`(min_nargs, ...)`, or `(min_nargs, None)`.
- kws: Keywords. To represent **kwargs, either add another arg or
pass `...` or `None` as a keyword.

Solution 2: After receiving (args, kwargs) from the class, inspect the
signature of the class constructor and just figure out how it works.


== Additional Resources ==

* MacroPy implements case classes, which are similar to what Haskell
uses for pattern matching on constructors. I'm not sure that it lends
insight to the `Point(x, y)` case, because MacroPy doesn't have
pattern matching, but maybe someone else would.
https://github.com/lihaoyi/macropy#case-classes

* "Pattern matching in Python" (2009)
An attempt at making a matching thing.
https://monkey.org/~marius/pattern-matching-in-python.html

* PEP 275 -- Switching on Multiple Values
https://www.python.org/dev/peps/pep-0275/

* PEP 3103 -- A Switch/Case Statement
https://www.python.org/dev/peps/pep-3103/

M.-A. Lemburg

unread,
May 20, 2016, 7:50:32 AM5/20/16
to Franklin? Lee, Greg Ewing, python-ideas
Please see these PEPs...

https://www.python.org/dev/peps/pep-3103/
https://www.python.org/dev/peps/pep-0275/

...before starting yet another long thread on the same
topic.
--
Marc-Andre Lemburg
eGenix.com

Professional Python Services directly from the Experts (#1, May 20 2016)
>>> Python Projects, Coaching and Consulting ... http://www.egenix.com/
>>> Python Database Interfaces ... http://products.egenix.com/
>>> Plone/Zope Database Interfaces ... http://zope.egenix.com/
________________________________________________________________________

::: We implement business ideas - efficiently in both time and costs :::

eGenix.com Software, Skills and Services GmbH Pastor-Loeh-Str.48
D-40764 Langenfeld, Germany. CEO Dipl.-Math. Marc-Andre Lemburg
Registered at Amtsgericht Duesseldorf: HRB 46611
http://www.egenix.com/company/contact/
http://www.malemburg.com/

Michael Selik

unread,
May 20, 2016, 12:09:21 PM5/20/16
to Nick Coghlan, Guido van Rossum, python-ideas
On Thu, May 19, 2016 at 10:38 PM Nick Coghlan <ncog...@gmail.com> wrote:
On 19 May 2016 at 14:15, Guido van Rossum <gu...@python.org> wrote:
> The attribute idea would be similar to a duck-type check, though more
> emphasizing data attributes. It would be nice if we could write a
> match that said "if it has attributes x and y, assign those to local
> variables p and q, and ignore other attributes".
 
If we went down that path, then the "assign if you can, execute this

case if you succeed" options would presumably need an explicit prefix
to indicate they're not normal expressions, perhaps something like
"?=":

    switch expr as arg:
        case ?= (.x as p, .y as q): print('x=', p, 'y=', q)

If you don't mind adding a new operator, then an easier way to handle several of these situations would be to make ``?=`` an assignment expression that evaluates to True/False whether the assignment succeeded:


    def foo(obj):
        return a ?= obj.a

Could be equivalent to:

    def foo(obj):
        try:
            a = obj.a
        except Exception:
            return False
        else:
            return True


The use-cases of are somewhat overlapping with the idea of an inline try/except as in PEP 463 (https://www.python.org/dev/peps/pep-0463/).


Which would then have the further implication that it might also make
sense to support attribute unpacking as the LHS of normal assignment
statements:

    (.x as p, .y as q) = expr

The ``as`` syntax flips the familiar variable-on-the-left and makes this one tough for me to read. I'd rather force a little repetition:

    o = expr
    p, q = o.x, o.y

Using a temporary variable like ``o`` makes it even fewer characters than the proposed parens and ``as`` keyword.

Ryan Gonzalez

unread,
May 20, 2016, 12:48:55 PM5/20/16
to Franklin? Lee, python-ideas
On Fri, May 20, 2016 at 5:48 AM, Franklin? Lee <leewangzh...@gmail.com> wrote:
I think there should be different syntaxes for matching equality and
binding patterns, and definitely different syntax for singular and
plural cases.

Let's try this:
- Equality:
    `case 5:`
- Conditional:
    `case if predicate(obj):`
- Pattern-matching:
    `case as [a, b, *_]:`
    `case as Point(x, y):`

Slightly-more-explicit checks, instead of simply `case 5:`.
- `case == 5:`
- `case is _sentinel:`
- `case is None:`
- `case < 6:`
- `case in (1,2,3):`
- `case in range(2, 5):`
- `case in int:`


I personally really like the equality, conditional, and pattern-matching syntax you showed; it looks the most Pythonic IMO.

However, the more explicit checks are a little overkill and feel like too much special-casing. I don't know of any languages that go so far as to allow stuff like `case < 6`; usually, there would instead be some syntax to assign the initial object to an intermediate variable, that way you can just use it with an `if` predicate.



--
Ryan
[ERROR]: Your autotools build scripts are 200 lines longer than your program. Something’s wrong.

Paul Moore

unread,
May 20, 2016, 1:46:11 PM5/20/16
to Ryan Gonzalez, python-ideas
On 20 May 2016 at 17:45, Ryan Gonzalez <rym...@gmail.com> wrote:
> On Fri, May 20, 2016 at 5:48 AM, Franklin? Lee
> <leewangzh...@gmail.com> wrote:
>>
>> I think there should be different syntaxes for matching equality and
>> binding patterns, and definitely different syntax for singular and
>> plural cases.
>>
>> Let's try this:
>> - Equality:
>> `case 5:`
>> - Conditional:
>> `case if predicate(obj):`
>> - Pattern-matching:
>> `case as [a, b, *_]:`
>> `case as Point(x, y):`
>>
>> Slightly-more-explicit checks, instead of simply `case 5:`.
>> - `case == 5:`
>> - `case is _sentinel:`
>> - `case is None:`
>> - `case < 6:`
>> - `case in (1,2,3):`
>> - `case in range(2, 5):`
>> - `case in int:`
>>
>
> I personally really like the equality, conditional, and pattern-matching
> syntax you showed; it looks the most Pythonic IMO.

Agreed.

> However, the more explicit checks are a little overkill and feel like too
> much special-casing. I don't know of any languages that go so far as to
> allow stuff like `case < 6`; usually, there would instead be some syntax to
> assign the initial object to an intermediate variable, that way you can just
> use it with an `if` predicate.

Also agreed. It seems that

switch expr as name
case if name is _sentinel: do_stuff()

would be a reasonable syntax for naming the switch expression -
although just giving it a name before using it in the switch is
probably just as reasonable:

name = expr
switch name:
case if name is _sentinel: do_stuff()

BTW, there's also a semantic question - if multiple cases match, would
only one (presumably the first) or all of them be executed? I'm sure
this was discussed to death during the discussions on PEPs 3103 and
275, but I don't have the time or inclination to re-read those now, so
I'll just leave the question open here for someone else to do the
research...

Paul

Guido van Rossum

unread,
May 20, 2016, 1:48:24 PM5/20/16
to Paul Moore, python-ideas
It's definitely going to be "try cases in order until one matches".
--
--Guido van Rossum (python.org/~guido)

Jim Baker

unread,
May 20, 2016, 3:27:02 PM5/20/16
to Guido van Rossum, python-ideas
Agreed, if we want to support the structural pattern matching as seen in Haskell, Rust, or Scala, we should do the following:
  • Match in sequence against the provided cases. Cases should support optional guards (`if` clause)
  • Follow only one case - no fall through, no need to `break`
  • Raise a match error if the match is not exhaustive; an `else` clause is fine for exhaustion
Using the typical switch pattern seen in C or Java is already idiomatically addressed in Python through dispatching through a dict. This type of matching is different.

Paul Moore

unread,
May 20, 2016, 3:46:12 PM5/20/16
to Guido van Rossum, python-ideas
On 20 May 2016 at 18:47, Guido van Rossum <gu...@python.org> wrote:
> It's definitely going to be "try cases in order until one matches".

Cool, thanks.

Life's so much simpler with a BDFL pronouncement :-)

Random832

unread,
May 22, 2016, 1:51:32 PM5/22/16
to python...@python.org
On Fri, May 20, 2016, at 15:25, Jim Baker wrote:
> Agreed, if we want to support the structural pattern matching as seen in
> Haskell, Rust, or Scala, we should do the following:
>
> - Raise a match error if the match is not exhaustive; an `else` clause
> is fine for exhaustion

This is required because the analogous constructs in those languages are
expressions that return a value, and there's no reason to require it for
a python construct which is a statement.

Joao S. O. Bueno

unread,
May 23, 2016, 2:46:05 PM5/23/16
to Random832, Python-Ideas
I still fail to see what justifies violating The One Obvious Way to Do It which
uses an if/elif sequence so blatantly beyond the
feeble "Yet, some kind of switch statement is found in many languages
and it is not unreasonable to expect that its addition to Python will
allow us to write up certain code more cleanly and efficiently than
before." in the rejected PEP.

It is not like omitting the object reference name on the conditional
expression (and therefore,
a condition expression that is tied to one object) is more readable at all.

But them, it is the BDLF cming with the proposal - I am just this
Python develoepr teacher, that just last Wednesday was saying
something along on a crash course:
"We don need switch/case because it is just a special case of an
if/elif sequence,
that was meaningful when C compilers did not had resources to optimize
that themselves".

I argue no further, if people still see this as desirable.Ths is just my plain
Python user "-1" on python-ideas.

js
-><-

Guido van Rossum

unread,
May 23, 2016, 3:59:07 PM5/23/16
to Joao S. O. Bueno, Python-Ideas
On Mon, May 23, 2016 at 11:45 AM, Joao S. O. Bueno
<jsb...@python.org.br> wrote:
> I still fail to see what justifies violating The One Obvious Way to Do It which
> uses an if/elif sequence so blatantly beyond the
> feeble "Yet, some kind of switch statement is found in many languages
> and it is not unreasonable to expect that its addition to Python will
> allow us to write up certain code more cleanly and efficiently than
> before." in the rejected PEP.
>
> It is not like omitting the object reference name on the conditional
> expression (and therefore,
> a condition expression that is tied to one object) is more readable at all.
>
> But them, it is the BDLF cming with the proposal - I am just this
> Python develoepr teacher, that just last Wednesday was saying
> something along on a crash course:
> "We don need switch/case because it is just a special case of an
> if/elif sequence,
> that was meaningful when C compilers did not had resources to optimize
> that themselves".
>
> I argue no further, if people still see this as desirable.Ths is just my plain
> Python user "-1" on python-ideas.

Honestly I'm not at all convinced either! If it was easy to do all
this with a sequence of if/elif clauses we wouldn't need it. The
problem is that there are some types of matches that aren't so easy to
write that way, e.g. combining an attempted tuple unpack with a guard,
or "instance unpack" (check whether specific attributes exist)
possibly combined with a guard. (The tricky thing is that the guard
expression often needs to reference to the result of the unpacking.)

It might become yet more interesting when combining the unpack syntax
with type annotations, so you could check e.g. whether there's an x
attribute that's a float and a y attribute that's a string.

But it's about the most speculative piece of language design I've
contemplated in a long time...

--
--Guido van Rossum (python.org/~guido)

Michael Selik

unread,
May 23, 2016, 6:50:16 PM5/23/16
to gu...@python.org, Joao S. O. Bueno, Python-Ideas
On Mon, May 23, 2016 at 3:58 PM Guido van Rossum <gu...@python.org> wrote:
On Mon, May 23, 2016 at 11:45 AM, Joao S. O. Bueno
<jsb...@python.org.br> wrote:
> I still fail to see what justifies violating The One Obvious Way to Do It which uses an if/elif sequence

Honestly I'm not at all convinced either! If it was easy to do all
this with a sequence of if/elif clauses we wouldn't need it. The
problem is that there are some types of matches that aren't so easy to
write that way, e.g. combining an attempted tuple unpack with a guard,
or "instance unpack" (check whether specific attributes exist)
possibly combined with a guard. (The tricky thing is that the guard
expression often needs to reference to the result of the unpacking.)

Wouldn't an exception-catching expression (PEP 463) enable most of what you want with the existing if/elif/else chain?

    def demo(arg):
        if ((arg.x, arg.y) except AttributeError: False):
            print('x=', arg.x, 'y=', arg.y)
        elif ((arg[0], arg[1]) except IndexError: False):
            a, b, *_ = arg
            print('a=', a, 'b=', b)
        else:
            print('Too bad')


To get everything you want, we could have an exception-catching assignment expression. While a plain ``=`` assignment would be problematic as an expression due to the common ``==`` typographical error, a less error-prone operator might be OK. I'd suggested ``?=`` earlier in the thread, but perhaps I didn't write out a good example at that time. Here's one that's closer to the original demo example.

    def demo(arg):
        if p, q ?= arg.x, arg.y: print('x=', p, 'y=', q)
        elif a, b, *_ ?= arg: print('a=', a, 'b=', b)
        else: print('Too bad')


I figure it's better to solve the category of problems -- exception-catching expressions -- rather than the single problem of catching exceptions in an if/elif/else chain. Or do you think this is too much temptation for unreadable code golf?

Guido van Rossum

unread,
May 23, 2016, 7:14:27 PM5/23/16
to Michael Selik, Python-Ideas
People are likely either going to put in exceptions that don't catch
enough (e.g. IndexError isn't the only exception that example can
throw) or, overreacting to that problm, that catch everything ("except
Exception:" is an anti-pattern that's hard to fight).

Plus there's the redundancy that you showed.

Michael Selik

unread,
May 23, 2016, 7:53:33 PM5/23/16
to gu...@python.org, Python-Ideas
On Mon, May 23, 2016 at 7:13 PM Guido van Rossum <gu...@python.org> wrote:
On Mon, May 23, 2016 at 3:49 PM, Michael Selik <michae...@gmail.com> wrote:
>
>
> On Mon, May 23, 2016 at 3:58 PM Guido van Rossum <gu...@python.org> wrote:
>>
>> On Mon, May 23, 2016 at 11:45 AM, Joao S. O. Bueno
>> <jsb...@python.org.br> wrote:
>> > I still fail to see what justifies violating The One Obvious Way to Do
>> > It which uses an if/elif sequence
>>
>> Honestly I'm not at all convinced either! If it was easy to do all
>> this with a sequence of if/elif clauses we wouldn't need it. The
>> problem is that there are some types of matches that aren't so easy to
>> write that way, e.g. combining an attempted tuple unpack with a guard,
>> or "instance unpack" (check whether specific attributes exist)
>> possibly combined with a guard. (The tricky thing is that the guard
>> expression often needs to reference to the result of the unpacking.)
>
> I figure it's better to solve the category of problems -- exception-catching
> expressions -- rather than the single problem of catching exceptions in an if/elif/else chain.

People are likely either going to put in exceptions that don't catch
enough (e.g. IndexError isn't the only exception that example can
throw) or, overreacting to that problm, that catch everything ("except
Exception:" is an anti-pattern that's hard to fight).

It sounds like the justification for a switch/match syntax is to provide a special situation where generic Exception-catching assignment expressions are acceptable, because they're useful in a long elif chain but too dangerous for widespread use.

Clearly it's beneficial enough to have appeared in other languages. However, languages like Haskell have already accepted the danger of assignment (and more) being an expression. Is there a language that makes the expression/statement distinction that has as powerful a matching syntax?

Guido van Rossum

unread,
May 23, 2016, 8:23:59 PM5/23/16
to Michael Selik, Python-Ideas
No, what I'm saying is that seeing it as sugar for exception handing
is the wrong way to look at it.

Michael Selik

unread,
May 23, 2016, 10:58:33 PM5/23/16
to gu...@python.org, Python-Ideas
On Mon, May 23, 2016 at 8:23 PM Guido van Rossum <gu...@python.org> wrote:
No, what I'm saying is that seeing it as sugar for exception handing
is the wrong way to look at it.

Ok, let me refocus on the idea of assign-if-you-can during matching, not just sugar for try/except.

The matching features of equality, inequality, predicates, and subtype are already available via if/elif. The matching features of destructuring, assignment, and post-assignment guards need new syntax. The other proposals in this thread suggest a new keyword or keyword pair like switch/case. Instead, why not extend if/elif with a new operator?

def demo(arg):
    if p, q ?= arg.x, arg.y:                        # dict structure
    elif x ?= arg.x and isinstance(x, int)          # assignment + guard
    elif a, b, *_ ?= arg:                           # tuple structure
    elif isinstance(arg, Mapping):                  # nothing new here


A major advantage to merging these new matching features with the existing if/elif is that refactoring gets much easier. One could insert a single elif block near the top of the chain using the new assign-if-you-can operator/keyword, without needing to change any of the existing elif statements.

Otherwise, I could imagine someone waffling back and forth between switch/case and if/elif as they add and remove cases. What would be the style recommendation if you have 5 boring cases that are all easy to read via if/elif?

The disadvantage would be that the new syntax wouldn't stand out as much as it would with, say, switch/case, and someone might not notice its usage. I think a good choice of operator/keyword mitigates this. A keyword like ``as`` would stand out due to syntax highlighting. An operator like ``?=`` looks different enough and many similar-looking operators, like ``/=`` would be illegal.

Operator precedence would affect the beauty of the code. Giving ``?=`` the same precedence as ``==`` seems best for post-assignment guards.

Guido van Rossum

unread,
May 23, 2016, 11:44:26 PM5/23/16
to Michael Selik, Python-Ideas
This idea has some clear advantages -- it's unambiguous about the
order of matching, and combines clearly with existing conditions. It
also seems like it would support "recursive" matching nicely, by
allowing you to chain additional unpacking operators. ("Recursive"
because IIUC that's what Haskell calls matches inside matches --
similar to nested tuple unpackings like (a, (b, c)) = in Python.)

The trick is to find a good syntax for the conditional assignment;
using ? has been rejected by this group in the past for other
conditionalisms.

Nick Coghlan

unread,
May 24, 2016, 12:36:36 AM5/24/16
to Guido van Rossum, Python-Ideas
I don't think we've ever considered it specifically in the context of
a "maybe-assign" expression, though.

While such a conditional assignment syntax does sound appealing as a
way to do runtime pattern matching, one key concern I'd have with it
would be how it's defined in the case of a "can't fail" assignment to
a single variable:

if x ?= re.match(pattern, text):
# Can x be None here?

while x ?= expr():
# Can x be any non-truthy expression here?

Assuming binding to a name was a "can't fail" operation that always
returned either "True" or the LHS as a tuple (so assigning to a single
name resulted in a 1-tuple, while only a failed assignment resulted in
an empty tuple), then the correct spelling for those operations would
be:

if x ?= re.match(pattern, text) and x is not None:
# x is definitely not None here

while x ?= expr() and x:
# x is always truthy

The other tricky problem would be defining precisely how exception
handling worked on the RHS. For iterable unpacking, clearly the
TypeError from the implicit iter() call would be suppressed (if
thrown), as would errors related to mismatches between the number of
items and the number of unpacking targets, but catching arbitrary
AttributeErrors from the RHS to support conditional attribute based
destructuring assignment would be problematic. That suggests to me
that some kind of explicit syntax for attribute based destructuring
may still be needed to avoid overbroad suppression of exceptions.

Cheers,
Nick.


--
Nick Coghlan | ncog...@gmail.com | Brisbane, Australia

Greg Ewing

unread,
May 24, 2016, 2:09:06 AM5/24/16
to Python-Ideas
> On Mon, May 23, 2016 at 7:57 PM, Michael Selik <michae...@gmail.com> wrote:
>
>>def demo(arg):
>> if p, q ?= arg.x, arg.y: # dict structure
>> elif x ?= arg.x and isinstance(x, int) # assignment + guard
>> elif a, b, *_ ?= arg: # tuple structure
>> elif isinstance(arg, Mapping): # nothing new here

I'm unenthusiastic about this -- the above looks like
an unreadable mess to me.

Some other worries:

* It appears that this would have to work by means of
exception-catching in the rhs, so it's really a
disguised form of the except: expression proposal;
is that intentional?

* It could also be abused to get an assigment-in-
expression anywhere you wanted. Such things have
been rejected before; are we changing our mind
on that?

* THere would be no hope of checking for coverage
of all cases in a static checker.

I feel like we've wandered a long way from the idea
we started with, which is something to facilitate
a Haskell-like case-analysis style of programming,
and ended up with something much looser that tries
to be all things to everyone.

--
Greg

Paul Moore

unread,
May 24, 2016, 4:25:46 AM5/24/16
to Greg Ewing, Python-Ideas
On 24 May 2016 at 07:08, Greg Ewing <greg....@canterbury.ac.nz> wrote:
>> On Mon, May 23, 2016 at 7:57 PM, Michael Selik <michae...@gmail.com>
>> wrote:
>>
>>> def demo(arg):
>>> if p, q ?= arg.x, arg.y: # dict structure
>>> elif x ?= arg.x and isinstance(x, int) # assignment + guard
>>> elif a, b, *_ ?= arg: # tuple structure
>>> elif isinstance(arg, Mapping): # nothing new here
>
>
> I'm unenthusiastic about this -- the above looks like
> an unreadable mess to me.

Agreed - the ?= operator looks like noise to me. One of the advantages
of a switch/case statement is that it's keyword based, which IMO is
inherently more readable.

If we keep getting sucked into comparisons with "chain of if"
constructs, maybe the problem is that the "switch" keyword is too
strongly associated with that in people's minds? Maybe we could focus
on the fact that it's matching that we're doing and make it a "match"
statement? So

match expr:
argspec: xxx
argspec:
yyy
else:
default

Whether we want to simply make "argspec" do standard tuple unpacking,
or something a little more complex like attribute checking, can be up
for debate. I'm inclined to start small and see if there's any value
in a simple "try various unpackings" construct first.

One big problem with this is that making "match" a keyword would be a
real pain - I bet there's lots of code that uses "match" as a
variable...

Paul

M.-A. Lemburg

unread,
May 24, 2016, 4:27:52 AM5/24/16
to Greg Ewing, Python-Ideas
On 24.05.2016 08:08, Greg Ewing wrote:
>> On Mon, May 23, 2016 at 7:57 PM, Michael Selik
>> <michae...@gmail.com> wrote:
>>
>>> def demo(arg):
>>> if p, q ?= arg.x, arg.y: # dict structure
>>> elif x ?= arg.x and isinstance(x, int) # assignment + guard
>>> elif a, b, *_ ?= arg: # tuple structure
>>> elif isinstance(arg, Mapping): # nothing new here
>
> I'm unenthusiastic about this -- the above looks like
> an unreadable mess to me.

Seconded. It would be more readable to make the assignment
explicit:

if (p, q = arg.x, arg.y):
...

but even then I find this error prone. Just think of the
common typo of writing "if x = 1: ..." instead of
"if x == 1: ...".

This version

p, q = arg.x, arg.y
if (p, q):
...

is just a bit more verbose, but comes without all the
problems of having to allow implicit assignment-in-expression
everywhere.

--
Marc-Andre Lemburg
eGenix.com

Professional Python Services directly from the Experts (#1, May 24 2016)
>>> Python Projects, Coaching and Consulting ... http://www.egenix.com/
>>> Python Database Interfaces ... http://products.egenix.com/
>>> Plone/Zope Database Interfaces ... http://zope.egenix.com/
________________________________________________________________________

::: We implement business ideas - efficiently in both time and costs :::

eGenix.com Software, Skills and Services GmbH Pastor-Loeh-Str.48
D-40764 Langenfeld, Germany. CEO Dipl.-Math. Marc-Andre Lemburg
Registered at Amtsgericht Duesseldorf: HRB 46611
http://www.egenix.com/company/contact/
http://www.malemburg.com/

SW

unread,
May 24, 2016, 5:33:18 AM5/24/16
to python...@python.org
On 24/05/16 07:08, Greg Ewing wrote:
>> On Mon, May 23, 2016 at 7:57 PM, Michael Selik
>> <michae...@gmail.com> wrote:
>>
>>> def demo(arg):
>>> if p, q ?= arg.x, arg.y: # dict structure
>>> elif x ?= arg.x and isinstance(x, int) # assignment + guard
>>> elif a, b, *_ ?= arg: # tuple structure
>>> elif isinstance(arg, Mapping): # nothing new here
>
> I'm unenthusiastic about this -- the above looks like
> an unreadable mess to me.
Agreed.

S

Koos Zevenhoven

unread,
May 24, 2016, 8:04:20 AM5/24/16
to Guido van Rossum, Python-Ideas
On Tue, May 24, 2016 at 6:43 AM, Guido van Rossum <gu...@python.org> wrote:
> On Mon, May 23, 2016 at 7:57 PM, Michael Selik <michae...@gmail.com> wrote:
>> Ok, let me refocus on the idea of assign-if-you-can during matching, not
>> just sugar for try/except.
>>
>> The matching features of equality, inequality, predicates, and subtype are
>> already available via if/elif. The matching features of destructuring,
>> assignment, and post-assignment guards need new syntax. The other proposals
>> in this thread suggest a new keyword or keyword pair like switch/case.
>> Instead, why not extend if/elif with a new operator?
>>
>> def demo(arg):
>>     if p, q ?= arg.x, arg.y:                        # dict structure
>>     elif x ?= arg.x and isinstance(x, int)          # assignment + guard
>>     elif a, b, *_ ?= arg:                           # tuple structure
>>     elif isinstance(arg, Mapping):                  # nothing new here
>>
>>
[...]

> This idea has some clear advantages -- it's unambiguous about the
> order of matching, and combines clearly with existing conditions. It
> also seems like it would support "recursive" matching nicely, by
> allowing you to chain additional unpacking operators. ("Recursive"
> because IIUC that's what Haskell calls matches inside matches --
> similar to nested tuple unpackings like (a, (b, c)) = in Python.)
>
> The trick is to find a good syntax for the conditional assignment;
> using ? has been rejected by this group in the past for other
> conditionalisms.

Yeah, and indeed it's not obvious how to draw the line between whether the conditional assignment should return a truth value or raise an exception, as I think Greg was already implying. For instance, what would `a ?= data[0]` do, if data[0] raises, say, a TypeError. Should it be caught or be raised? Anyway, maybe it is possible to draw that line in a reasonable way.

So here's a
​nother syntax
:

Introduce a
​new ​
keyword `given`, which
​could be used as
 a prefix operator with roughly the following properties

given a    
​   ​       
# True if the name a is bound to something
given b.c    
​ ​      
# roughly equivalent to hasattr(b, 'c')
given a, b.c  
​      ​
# same as (given a and given b.c)
given d, e = a, b.c
​ ​
# like `given a, b.c` but with added assignment

Then one could do (
​expanding on
 the above demo example)

def demo(arg):
    if given p, q = arg.x, arg.y:
        # do something
    elif given x = arg.x and isinstance(x, int):
        # do somet​hing
    elif given a, b, *_ = arg:
        # do somet​h​ing
    elif isinstance(arg, Mapping):
        # do something

​# Or something like this:
def demo2(arg):​
​    global importan​t_value
​    if not given important_value:
        important_value = compute_important_value()

​-- Koos​

​PS. I like seeing the word "brainstorm" used here. I'm not sure if I've seen that here before. I think that means that we try to see the good parts of the presented ideas and send further ideas and thoughts to see if we can find something useful -- even if the presented ideas are not yet optimal. (Of course that does not mean that the ideas should not be presented clearly!) Anyway, in the end, if nothing is found, it is easy to just drop the whole concept.

Nick Coghlan

unread,
May 24, 2016, 8:11:18 AM5/24/16
to Paul Moore, Python-Ideas
New keywords that collide with standard library APIs (re.match in this
case) are pretty much right out :)

I suspect you're right that switch/case have too much baggage to be
viable candidates for this use case, though.

Cheers,
Nick.

--
Nick Coghlan | ncog...@gmail.com | Brisbane, Australia

Michael Selik

unread,
May 24, 2016, 9:32:35 AM5/24/16
to Greg Ewing, Python-Ideas
On Tue, May 24, 2016 at 2:08 AM Greg Ewing <greg....@canterbury.ac.nz> wrote:
> On Mon, May 23, 2016 at 7:57 PM, Michael Selik <michae...@gmail.com> wrote:
>
>>def demo(arg):
>>    if p, q ?= arg.x, arg.y:                        # dict structure
>>    elif x ?= arg.x and isinstance(x, int)          # assignment + guard
>>    elif a, b, *_ ?= arg:                           # tuple structure
>>    elif isinstance(arg, Mapping):                  # nothing new here

I'm unenthusiastic about this -- the above looks like
an unreadable mess to me.

Which is unreadable: ``if/elif`` keywords or ``?=`` operator? If it's the latter, please don't get hung up on that, as I intended that as a placeholder for whatever operator or keyword is best. My main point is that switch/case/matching is semantically identical to if/elif, so why use something different?
 
Some other worries:

* It appears that this would have to work by means of
exception-catching in the rhs, so it's really a
disguised form of the except: expression proposal;
is that intentional?

No. The exception-catching expression is not meant to be available in any other context. However, exception-catching is an essential feature to matching via destructuring. I believe it's unavoidable, though *which* exceptions are suppressed could be made more clear.

* It could also be abused to get an assigment-in-
expression anywhere you wanted. Such things have
been rejected before; are we changing our mind
on that?

No, using the assign-if-can operator would be syntax error outside of an if/elif, the reverse of how an assign operator is a syntax error inside an if/elif. 

* THere would be no hope of checking for coverage
of all cases in a static checker.

I believe you, though the logic isn't clear to me. Could you explain why?

 
I feel like we've wandered a long way from the idea
we started with, which is something to facilitate
a Haskell-like case-analysis style of programming,
and ended up with something much looser that tries
to be all things to everyone.

That was not my intention. If the assign-if-can operation is only available in an if/elif statement, I think it's exactly Haskell-like case-analysis and nothing more. Did I miss something? 

Koos Zevenhoven

unread,
May 24, 2016, 10:04:51 AM5/24/16
to Michael Selik, Python-Ideas
On Tue, May 24, 2016 at 4:31 PM, Michael Selik <michae...@gmail.com> wrote:
>
> On Tue, May 24, 2016 at 2:08 AM Greg Ewing <greg....@canterbury.ac.nz> wrote:
[...]
>> * It could also be abused to get an assigment-in-
>> expression anywhere you wanted. Such things have
>> been rejected before; are we changing our mind
>> on that?
>
> No, using the assign-if-can operator would be syntax error outside of an
> if/elif, the reverse of how an assign operator is a syntax error inside an if/elif.

I might be stating the obvious, but the `given` expression syntax
could of course also be allowed only inside if/elif, even if I didn't
include that.

>> * THere would be no hope of checking for coverage
>> of all cases in a static checker.
>
> I believe you, though the logic isn't clear to me. Could you explain why?
>

It's not clear to me either. Maybe there is hope, with a sophisticated
enough checker at some point in the future?

-- Koos

Paul Moore

unread,
May 24, 2016, 10:35:48 AM5/24/16
to Michael Selik, Python-Ideas
On 24 May 2016 at 14:31, Michael Selik <michae...@gmail.com> wrote:
> On Tue, May 24, 2016 at 2:08 AM Greg Ewing <greg....@canterbury.ac.nz>
> wrote:
>>
>> > On Mon, May 23, 2016 at 7:57 PM, Michael Selik <michae...@gmail.com>
>> > wrote:
>> >
>> >>def demo(arg):
>> >> if p, q ?= arg.x, arg.y: # dict structure
>> >> elif x ?= arg.x and isinstance(x, int) # assignment + guard
>> >> elif a, b, *_ ?= arg: # tuple structure
>> >> elif isinstance(arg, Mapping): # nothing new here
>>
>> I'm unenthusiastic about this -- the above looks like
>> an unreadable mess to me.
>
>
> Which is unreadable: ``if/elif`` keywords or ``?=`` operator? If it's the
> latter, please don't get hung up on that, as I intended that as a
> placeholder for whatever operator or keyword is best. My main point is that
> switch/case/matching is semantically identical to if/elif, so why use
> something different?

For me, it's the use of operators (punctuation) plus the fact that the
overall layout doesn't in any way imply to me "I'm looking at *this*
expression. Let's try a sequence of matches..."

The key distinguishing feature for me of a switch/match statement is
that it organises the "flow" of the statement differently from
if/elif. The if/elif chain says "try this, then that, now something
else". There's no implication that the tests are all looking at the
same subject - to spot that, you need to be able to see (from the
layout of the tests) the actual subject item on each line, and the ?=
operator syntax obscures that because "arg" is used differently in
each test.

With a switch statement, however, the subject is stated once, at the
top of the statement. The checks are then listed one after the other,
and they are all by definition checks against the subject expression.
And in fact, for me that's the distinguishing feature of a switch
statement - that it's a series of tests against a single implied
subject. That's also (I suspect) why it's hard to come up with a good
syntax for tests - Python doesn't really do "implied subjects"
anywhere else (explicit is better than implicit and all that).

IMO, "Series of tests against a single expression" is a common enough
pattern to be worth exploring in spite of the fact that it runs
counter to EIBTI. But you may need to be Dutch to understand why :-)

Paul

Michel Desmoulin

unread,
May 24, 2016, 11:00:45 AM5/24/16
to python...@python.org
What about making it a function ?

Pattern matching is complex, it can be like a mini language inside the
language, just like regex.

To match text we do:

import re
re.match('pattern', 'string')

We could do the same for matching structures:

from inspect import match

def do(stuff):
m = match(stuff): # m implements __call__
if m('whatever mini language you like'):
return foo
if m('again'):
return m.thing_your_extracted

Pros:

- no need for a new syntax
- very explicit
- use well know constructs
- we can easily start experimenting with the matching language in a lib
in Pypy

Cons:

- much more verbose;
- debugging your mini language and handling error is not as good;

Michel Desmoulin

unread,
May 24, 2016, 11:01:43 AM5/24/16
to python...@python.org
What about making it a function ?

Pattern matching is complex, it can be like a mini language inside the
language, just like regex.

To match text we do:

import re
re.match('pattern', 'string')

We could do the same for matching structures:

from inspect import match

def do(stuff):
m = match(stuff): # m implements __call__
if m('whatever mini language you like'):
return foo
if m('again'):
return m.thing_your_extracted

Pros:

- no need for a new syntax
- very explicit
- use well know constructs
- we can easily start experimenting with the matching language in a lib
in Pypy

Cons:

- much more verbose;
- debugging your mini language and handling error is not as good;

Le 24/05/2016 16:35, Paul Moore a écrit :

Koos Zevenhoven

unread,
May 24, 2016, 11:43:05 AM5/24/16
to Michel Desmoulin, python-ideas
On Tue, May 24, 2016 at 5:59 PM, Michel Desmoulin
<desmoul...@gmail.com> wrote:
> What about making it a function ?
>
> Pattern matching is complex, it can be like a mini language inside the
> language, just like regex.
>
> To match text we do:
>
> import re
> re.match('pattern', 'string')
>
> We could do the same for matching structures:
>
> from inspect import match
>
> def do(stuff):
> m = match(stuff): # m implements __call__
> if m('whatever mini language you like'):
> return foo
> if m('again'):
> return m.thing_your_extracted
>

Or with methods:

m = match(stuff)
if m.by_type(SomeType):
# handle SomeType
elif m.by_attrs('x', 'y'):
# do things with stuff.x and stuff.y
elif m.by_len(3):
x,y,z = stuff
# do things with x, y, z

> Pros:
>
> - no need for a new syntax
> - very explicit
> - use well know constructs
> - we can easily start experimenting with the matching language in a lib
> in Pypy
+ can be extended easily

>
> Cons:
>
> - much more verbose;
> - debugging your mini language and handling error is not as good;
+ probably slower
+ need to separately get the desired attributes/elements after matching.

-- Koos

Jim Baker

unread,
May 24, 2016, 3:22:08 PM5/24/16
to Koos Zevenhoven, python-ideas
Structural pattern matching should address the following, somewhat similar to regex pattern matching:

1. Patterns are quoted. This can be implicitly done, because of built-in syntactic support; or one is supplying the pattern to a library using regular Python quoting.

2. Patterns should support bind variables for the unpacking. In languages like Scala, this is very natural to do, with bop, v1, v2 lexically scoped to the RHS of =>.

```scala
case Binary(bop @ (Lt|Le|Gt|Ge), v1, v2) if isValue(v1) && isValue(v2) =>
   doreturn(B(inequalityVal(bop, v1, v2)) ) 
```

This is an example from an interpreter used in a lab exercise in a class I teach, with patterns against ASTs. The ASTs are themselves defined in terms of Scala's case classes, which are more or less equivalent to namedtuples in Python.

Clearly the scoping of bind variables can be emulated by the match object, much as we do in regexes, at the cost of some minor overhead.

3. Some protocol to connect together objects like tuples, namedtuples, and arbitrary classes with the matching. In Scala, this is called unapply, and given that Scala has similar object-oriented aspects that are similar to Python in making life more difficult ;), when compared to Haskell's comparatively simple rules, it's probably close to what we need to do for __match__ or something like that.

The challenge is making this work together, especially distinguishing patterns from bind variables. Haskell makes it easy by a requirement that algebraic types have constructors which start with an upper case letter. Scala can do simple scope analysis of names to determine if it's a case class or a bind variable. For Python, presumably more syntax is necessary in the pattern specifier. Maybe something like the following, which seems unambiguous, possibly not so ugly:

```python
case Binary(bop @ (Lt|Le|Gt|Ge), v1@, v2@) if isValue(v1) and isValue(v2):
   # bop, v1, v2 are lexically scoped here
```

Am I missing anything? It seems to me that one can do structural pattern matching as a library (do obvious changes to above); although having it supported with specific syntax might make it much nicer.

- Jim


Greg Ewing

unread,
May 24, 2016, 7:04:01 PM5/24/16
to Python-Ideas
Michael Selik wrote:
> Which is unreadable: ``if/elif`` keywords or ``?=`` operator?

The whole thing.

> If it's
> the latter, please don't get hung up on that, as I intended that as a
> placeholder for whatever operator or keyword is best.

Syntax is an important part of readability, though.
Any alternative syntax for the same semantics would have
to be evaluated for readability on its own merits.

> My main point is
> that switch/case/matching is semantically identical to if/elif, so why
> use something different?

But then you say

> No, using the assign-if-can operator would be syntax error outside of an
> if/elif, the reverse of how an assign operator is a syntax error inside
> an if/elif.

So what you're proposing is *not* semantically equivalent
to composing the existing if/elif with a new pattern matching
assignment, which means re-using the existing keywords for
it is misleading.

> The exception-catching expression is not meant to be available in
> any other context. However, exception-catching is an essential feature
> to matching via destructuring. I believe it's unavoidable, though
> *which* exceptions are suppressed could be made more clear.

Perhaps I can elaborate on that a bit. In Guido's original
proposal, you would say something like

case Foo(a=x, b=y):

to match something of class Foo having attributes a and b.
Implementing that would involve using hasattr() on the
object being matched, or something equivalent. That catches
AttributeError, but only on the particular operation of
testing for the attribute.

Now with your

x, y ?= arg.a, arg.b

my assumption was that there was nothing special about
the right hand side, so the implementation would have to
just evaluate all of it and catch any AttribteErrors,
KeyErrors, IndexErrors, etc. emanating from it, which is
much looser and prone to catching too much.

But if the right hand side is special, all bets are off
and you'll have to explain exactly what would be allowed
and how to interpret it.

Stephen J. Turnbull

unread,
May 25, 2016, 1:04:34 AM5/25/16
to Paul Moore, Python-Ideas
Paul Moore writes:

> With a switch statement, however, the subject is stated once, at the
> top of the statement. The checks are then listed one after the other,
> and they are all by definition checks against the subject expression.
> And in fact, for me that's the distinguishing feature of a switch
> statement

+1

That may also make some optimizations easier to spot.

I'm also -1 on the ?= syntax, which reads "maybe assign" to me, but
that covers way too much ground (and in particular has been proposed
for "null-propagation" in the recent past).

I admit I'm not in love with the "switch/case" syntax for the kind of
matching intended here, as C's switch is way too burned into my
thinking. I think it's gone right through the EEPROM silicon into the
plastic case[sic, just can't get away from those preexisting "case"es!]

How about

for <thing>:
try <matcher>:
pass
try <matcher>:
pass

Look Ma! No new keywords! Yeah, I know, "for" and "try" both have
very strong connotations in Python already, so this may be a "not even
the Dutch could like it" automatic-parser-only syntax.

Steve

Chris Angelico

unread,
May 25, 2016, 1:12:00 AM5/25/16
to Python-Ideas
On Wed, May 25, 2016 at 3:03 PM, Stephen J. Turnbull <ste...@xemacs.org> wrote:
> How about
>
> for <thing>:
> try <matcher>:
> pass
> try <matcher>:
> pass
>
> Look Ma! No new keywords! Yeah, I know, "for" and "try" both have
> very strong connotations in Python already, so this may be a "not even
> the Dutch could like it" automatic-parser-only syntax.

I'd much prefer a different keyword instead of 'for'. If 'with' hadn't
been used, that would be a better choice. Maybe 'using'... or
'switch'. But without iteration, 'for' is going to be very confusing.

ChrisA

Michael Selik

unread,
May 25, 2016, 1:34:52 AM5/25/16
to Greg Ewing, Python-Ideas
On Tue, May 24, 2016 at 7:03 PM Greg Ewing <greg....@canterbury.ac.nz> wrote:
Michael Selik wrote:
> My main point is
> that switch/case/matching is semantically identical to if/elif, so why
> use something different?

But then you say

 > No, using the assign-if-can operator would be syntax error outside of an
 > if/elif, the reverse of how an assign operator is a syntax error inside
 > an if/elif.

So what you're proposing is *not* semantically equivalent
to composing the existing if/elif with a new pattern matching
assignment, which means re-using the existing keywords for
it is misleading.

Perhaps I didn't explain my proposal correctly. I spent about 3 hours this evening writing out a more thorough proof of the semantic equivalence. As I did so, toying with different syntaxes, I realized that it might be better to break this problem apart and solve a few subproblems first.

The first problem to solve is how to write a destructuring bind for more than just sequences. Once we're happy with that, it'll be easier to discuss pattern matching.

# Iterable-destructuring bind (a.k.a unpacking)
(a, b, *rest) = sequence

# Subscriptable-destructuring bind
{'x': p, 'y': q} = mapping
{0: p, 42: q, **rest} = indexable

# Attribute-destructuring bind (a few awkward ideas)
(.x: p, .y: q) = obj
(x=p, y=q) = obj

Or would you call that "object-destructuring"? No clue what the best name is here. I think Clojure side-steps attributes and only provides destructuring sequences and mappings. If you want to do pattern matching on a more complex type, you must provide essentially a conversion from that type to a mapping.

> The exception-catching expression is not meant to be available in
> any other context. However, exception-catching is an essential feature
> to matching via destructuring. I believe it's unavoidable, though
> *which* exceptions are suppressed could be made more clear.

my assumption was that there was nothing special about
the right hand side, so the implementation would have to
just evaluate all of it and catch any AttribteErrors,
KeyErrors, IndexErrors, etc. emanating from it, which is
much looser and prone to catching too much.

But if the right hand side is special, all bets are off
and you'll have to explain exactly what would be allowed
and how to interpret it.

I agree that catching all exceptions during the assign-if-can (via if/elif or switch/case) might be too error-prone. Whether it's the LHS of ``as`` or the RHS of ``?=``, it would help to restrict the kind of exceptions handled/suppressed as a failed match. However, I fail to see why ``case ... as ...`` would be restrictive and ``if ... ?= ...`` would not. They could have the same semantics, catching either a specific set of exceptions or all/most exceptions.

Michael Selik

unread,
May 25, 2016, 1:39:51 AM5/25/16
to Michel Desmoulin, python...@python.org
On Tue, May 24, 2016 at 11:00 AM Michel Desmoulin <desmoul...@gmail.com> wrote:
What about making it a function ?
Pattern matching is complex, it can be like a mini language inside the
language, just like regex.

What was that aphorism I heard -- "Any sufficiently complicated Python program contains an ad hoc implementation of Haskell."?

Just kidding. Mostly.

Nick Coghlan

unread,
May 25, 2016, 1:53:41 AM5/25/16
to Chris Angelico, Python-Ideas
On 25 May 2016 at 15:11, Chris Angelico <ros...@gmail.com> wrote:
> On Wed, May 25, 2016 at 3:03 PM, Stephen J. Turnbull <ste...@xemacs.org> wrote:
>> How about
>>
>> for <thing>:
>> try <matcher>:
>> pass
>> try <matcher>:
>> pass
>>
>> Look Ma! No new keywords! Yeah, I know, "for" and "try" both have
>> very strong connotations in Python already, so this may be a "not even
>> the Dutch could like it" automatic-parser-only syntax.
>
> I'd much prefer a different keyword instead of 'for'. If 'with' hadn't
> been used, that would be a better choice. Maybe 'using'... or
> 'switch'. But without iteration, 'for' is going to be very confusing.

As a variant on Guido's original switch/case idea:

given EXPR [as TARGET]:
case MATCH_PATTERN [as TARGET] [and CONDITION]:
...
case MATCH_PATTERN [as TARGET] [and CONDITION]:
...
case if CONDITION:
...
case MATCH_PATTERN [as TARGET]:
...
else:
...

Using the running demo:

def demo(arg):
given arg:
case x, y, *_: # Tuple matching (implicit name binding)
...
case (.x, .y) as p, q: # Attribute matching
...
case (["x"], ["y"]) as p, q: # Item matching
...
case (.x) as p and isinstance(p, int): # Match + condition
...
case if isinstance(arg, int): # Condition only
...
else: # Default
...

The other key change there is introducing "as" to the individual cases
in order to be able to separate the match pattern definition from the
local name binding.

Cheers,
Nick.

--
Nick Coghlan | ncog...@gmail.com | Brisbane, Australia

Greg Ewing

unread,
May 25, 2016, 1:59:04 AM5/25/16
to Python-Ideas
Michael Selik wrote:
> I fail to see
> why ``case ... as ...`` would be restrictive and ``if ... ?= ...`` would
> not. They could have the same semantics, catching either a specific set
> of exceptions or all/most exceptions.

The difference is not which exceptions get caught,
it's the size of the region of code around which
the catching occurs.

When I see

if x, y ?= arg.a, arg.b:
do_stuff()

it suggests that something like this is going on:

try:
temp = arg.a, arg.b
except AttributeError:
pass
else:
x, y = temp
do_stuff()

Whereas the implementation I had in mind for

switch arg:
case (a = x, b = y):
do_stuff()

would be more like

if hasattr(arg, "a") and hasattr(arg, "b"):
x = arg.a
y = arg.b
do_stuff()

They're equivalent if the only things you have on
the RHS are attribute accesses, but not if the RHS
is something more complicated.

If you're intending to restrict the RHS so that
you're not allowed anything more complicated, I
think that would be weird and suprising.

Another weird and surprising thing, that applies in
either case, is that

if x, y ?= arg.a, arg.b:
...

would *not* be equivalent to

z = arg.a, arg.b
if x, y ?= z:
...

With the switch/case syntax or something like it,
that issue doesn't arise.

Nick Coghlan

unread,
May 25, 2016, 2:00:49 AM5/25/16
to Michael Selik, Python-Ideas
Having the RHS of an assignment operation be syntactically restricted
isn't something Python has ever done before, while the LHS is already
heavily restricted (since it describes name binding targets rather
than normal expressions for evaluation).

The syntactic restrictions then mean evaluation order can be
controlled to ensure any exception handles only cover the desired step
in the process (e.g. a particular key lookup), rather than the
evaluation of arbitrary subexpressions.

Cheers,
Nick.

--
Nick Coghlan | ncog...@gmail.com | Brisbane, Australia

Greg Ewing

unread,
May 25, 2016, 2:26:55 AM5/25/16
to Python-Ideas
Nick Coghlan wrote:

> case (.x, .y) as p, q: # Attribute matching

I don't think I like the "as". Like the "?=",
it separates the pattern from the names being bound
too much.

> case (["x"], ["y"]) as p, q: # Item matching

That's even more confusing. The part before the "as"
looks like it should match a tuple of two one-element
lists containing the values "x" and "y".

My feeling is that the patterns should look like
constructors. The archetypal constructor for a mapping
object is the dict display, so a pattern that matches
a mapping having particular keys would be

case {"x": p, "y": q}:
...

> case MATCH_PATTERN [as TARGET] [and CONDITION]:

That way of handling guards wouldn't allow for multiple
guards on the same case. I would suggest

case PATTERN:
when CONDITION:
...
when CONDITION:
...

Note that this would be different from

case PATTERN:
if CONDITION:
...
elif CONDITION:
...

because failure of all the "when" clauses should cause
the next case to be tried.

--
Greg

Michael Selik

unread,
May 25, 2016, 2:36:10 AM5/25/16
to Greg Ewing, Python-Ideas
On Wed, May 25, 2016 at 2:26 AM Greg Ewing <greg....@canterbury.ac.nz> wrote:

My feeling is that the patterns should look like
constructors. The archetypal constructor for a mapping
object is the dict display, so a pattern that matches
a mapping having particular keys would be

    case {"x": p, "y": q}:

This is starting to look really good to me. But if that's valid in a case statement, why not in a regular assignment?

    (a, b, *rest) = sequence
    {'x': p, 'y': q, **rest} = mapping

Ethan Furman

unread,
May 25, 2016, 3:38:52 AM5/25/16
to python...@python.org
On 05/24/2016 10:52 PM, Nick Coghlan wrote:

> Using the running demo:
>
> def demo(arg):
> given arg:
> case x, y, *_: # Tuple matching (implicit name binding)
> ...
> case (.x, .y) as p, q: # Attribute matching
> ...
> case (["x"], ["y"]) as p, q: # Item matching
> ...
> case (.x) as p and isinstance(p, int): # Match + condition
> ...
> case if isinstance(arg, int): # Condition only
> ...
> else: # Default
> ...
>
> The other key change there is introducing "as" to the individual cases
> in order to be able to separate the match pattern definition from the
> local name binding.

With this one I have a clue as to what's going on.

--
~Ethan~

Nick Coghlan

unread,
May 25, 2016, 4:42:10 AM5/25/16
to Michael Selik, Python-Ideas
Aye, I'm warming to that as a syntax for item unpacking (although I'm
not sold on the "**rest" notation, since that entails doing something
like "{k: v for k, v in obj.items() if k not in explicit_keys}" to
populate it, further elevating "items()" towards the status of being a
protocol method without double-underscores)

I also agree with the principle that any prospective structural
pattern matching statement should align with assignment target
notation.

Unfortunately, we don't have anything like dictionary displays to
inform possible structures for an implied getattribute as part of
assignment to a particular target. The option I dislike least so far
is :

(p=.x, q=.y) = obj # Attribute unpacking

Which is clearly distinct from:

(p, q) = obj # Iterable unpacking
{0: p, 1: q} = obj # Item unpacking
{'x': p, 'y': q} = obj # Item unpacking

However, at this point we've strayed a *long* way from the ideal of
"executable pseudocode" :(

It would be slightly more readable if the new unpacking options used
"from" as their assignment keyword rather than the traditional "=":

(p=.x, q=.y) from obj # Attribute unpacking
{0: p, 1: q} from obj # Item unpacking
{'x': p, 'y': q} from obj # Item unpacking

Cheers,
Nick.

--
Nick Coghlan | ncog...@gmail.com | Brisbane, Australia

Franklin? Lee

unread,
May 25, 2016, 5:05:40 AM5/25/16
to Nick Coghlan, Python-Ideas
On Wed, May 25, 2016 at 1:52 AM, Nick Coghlan <ncog...@gmail.com> wrote:
> Using the running demo:
>
> def demo(arg):
> given arg:
> case x, y, *_: # Tuple matching (implicit name binding)
> ...
> case (.x, .y) as p, q: # Attribute matching
> ...
> case (["x"], ["y"]) as p, q: # Item matching
> ...
> case (.x) as p and isinstance(p, int): # Match + condition
> ...
> case if isinstance(arg, int): # Condition only
> ...
> else: # Default
> ...
>
> The other key change there is introducing "as" to the individual cases
> in order to be able to separate the match pattern definition from the
> local name binding.

I still don't like that `case THING` is a pattern, rather than a value
to test against. Here's my modifications with "as", attributes, and

def demo(arg):
given arg:
case as x, y, *_: # Tuple matching (implicit name binding)
...
case as object(x=p, y=q, **_): # Attribute matching
...
case as {'x': p, 'y', q, **_}: # Item matching
...
case as object(x=p, **_) and isinstance(p, int): # Match + condition
...
case if isinstance(arg, int): # Condition only
...
else: # Default
...


Here, the "as" pattern is a shape to fit the object's parts into, and
it should be possible to get back the original (or something
isomorphic to it) by evaluating the pattern expression (so `case as
{'x': p, 'y', q, **_}": assert isomorphic({'x': p, 'y', q, **_},
arg)`).

For attribute-matching, I think it's possible to make user types also
play well with this syntax for attribute matching, and I made a
proposal earlier for an API. (Section 'Matching user classes' here:
https://mail.python.org/pipermail/python-ideas/2016-May/040343.html)

(Proposal to make the `object` case more reasonable:
`object(**kwargs)` constructor creates an object with the given
attributes. It won't affect subclasses, because `object.__new__` can
test whether `cls is object`.)

Perhaps literal dicts could match against sequences, too.

{0: x, 42: y, **rest} = some_list

And this could be how you enforce types:

dict({0: x, 42: y, **rest}) = some_list # Fails.
tuple((x, y, z)) = some_list # Fails.

though I haven't figured out how these constructor shapes would be
implemented for user types.
Big conceptual obstacle: iterating over a dict gives its keys (which
has always bothered me), so as a value, `list({0: x, 42: y, **rest})`
would just be a list of keys. (I'd give up the typecheck syntax,
personally, and have you move the check into a guard. I'm not too
attached to the indexing syntax, either.)


Note that I force an explicit `**_, so that `object(x=p)` will fail if
`arg` has (non-dunder) attributes other than `x` (which I think is a
good thing). It's kinda wasteful to pack unused things into a
variable, so `...` could specify ignored args (which has come up
before on this list), and the matcher engine can tell the type's
matchmaker that it doesn't care about the other args.

Problem: `Point(x, y, ...)` is a legitimate function call, so if
`Point(x, 0)` is a legal pattern (i.e. no distinguishing syntax
between values and bindnames), you'd need the syntax to be `Point(x,
y, *...)`. Personally, I'd require that everything is a bindname
(unless it looks like a constructor call), and require checks to be in
guards.

Ethan Furman

unread,
May 25, 2016, 5:18:03 AM5/25/16
to python...@python.org
'case' is the signal word, 'as' is the seperator word -- but it should
be separating user stuff, not keyword and all the user stuff clumped
together.

`case blah blah as blah blah reads much better to me, and clearly says
"the stuff between 'case' and 'as' will now be known as the stuff
between 'as' and ':'".

--
~Ethan~

Paul Moore

unread,
May 25, 2016, 5:26:58 AM5/25/16
to Ethan Furman, Python-Ideas
On 25 May 2016 at 08:38, Ethan Furman <et...@stoneleaf.us> wrote:
> On 05/24/2016 10:52 PM, Nick Coghlan wrote:
>
>> Using the running demo:
>>
>> def demo(arg):
>> given arg:
>> case x, y, *_: # Tuple matching (implicit name binding)
>> ...
>> case (.x, .y) as p, q: # Attribute matching
>> ...
>> case (["x"], ["y"]) as p, q: # Item matching
>> ...
>> case (.x) as p and isinstance(p, int): # Match + condition
>> ...
>> case if isinstance(arg, int): # Condition only
>> ...
>> else: # Default
>> ...
>>
>> The other key change there is introducing "as" to the individual cases
>> in order to be able to separate the match pattern definition from the
>> local name binding.
>
>
> With this one I have a clue as to what's going on.

On first reading, I felt the same way. But rereading, I find that a
number of odd cases start bothering me: (["x"], ["y"]) doesn't look
like an item match the more I think about it. And to match "a 2-tuple
with ["x"] as the first item would be ["x"], y which is very close to
the item matching case.

As an *example* it works, but I don't see any way to describe the
detailed *semantics* without it being a mess of special cases.

One thought that this *does* prompt, though - the details of the
statement syntax are more or less bikeshed material in all this. The
*real* meat of the debate is around how we express matches.

So maybe we should focus solely on pattern matching as a primitive
construct. If we have an agreed syntax for "a match" then working out
how we incorporate that into a switch statement (or a "does this
match" maybe-assignment expression, or whatever) would likely be a lot
easier.

So looking solely at a "match" as a concept, let's see where that takes us:

- Unpacking syntax (NAME, NAME, *REST) handles matching sequences.
- Matches should probably nest, so NAME in the above could be a (sub-)match.
- For matching constants, if NAME is a constant rather than a name,
then that element must equal the literal for the match to succeed.
We'd probably need to restrict this to literals
(https://docs.python.org/3/reference/expressions.html#grammar-token-literal)
to avoid ambiguity.
- Matching mappings could be handled using dict syntax {literal: NAME,
literal: NAME, **REST}. Again allow recursive submatches and literal
matches?
- There's no "obvious" syntax for object mappings - maybe use type
constructor syntax TYPE(attr=NAME, attr=NAME). In the case where you
don't care about the type, we could use "object" (in theory, I don't
think it's ambiguous to omit the type, but that may be difficult for
the reader to understand). Also, TYPE() would then be an isinstance
check - do we want that?
- Top-level matches can have "and CONDITION" to do further tests on
the matched values (we don't want to allow this for nested matches,
though!)

Translating (and extending a bit) Nick's example:

def demo(arg):
given arg:
case (x, y, *_): # Tuple matching (implicit name binding)
...
case object(x=p, y=q): # Attribute matching
...
case {"x": p, "y": q): # Item matching
...
case object(x=p) and isinstance(p, int): # Match + condition
...
case int(): # Type match
...
case (1, p, {"key": 0, "match": q}):
# Match a sequence of length 3, first item must be 1, last
# must be a mapping with a key "key" with value 0 and
a key "match"
...
else: # Default

The worst one to my mind is the object match (not just in this style,
but basically everywhere) - that's because there's no existing display
or unpacking syntax for objects, so whatever we come up with is
unfamiliar.

I'm still a bit meh on this, though. Every proposal I've seen now
(including the above!) looks natural for simple examples - and would
probably look natural for 99% of real-world uses, which are typically
simple! - but gets awfully messy in the corner cases. It feels like
"If the implementation is hard to explain, it's a bad idea." may apply
here (although it's less the "implementation" and more the "detailed
semantics" that's hard to explain).

On 25 May 2016 at 10:04, Franklin? Lee <leewangzh...@gmail.com> wrote:
> Problem: `Point(x, y, ...)` is a legitimate function call, so if
> `Point(x, 0)` is a legal pattern (i.e. no distinguishing syntax
> between values and bindnames), you'd need the syntax to be `Point(x,
> y, *...)`. Personally, I'd require that everything is a bindname
> (unless it looks like a constructor call), and require checks to be in
> guards.

With the above syntax, "bare" values aren't a valid match, so in a
match, Point(x, y) can never be a function call, it must be a match
"Object of type Point, with x and y attributes (which we check for but
don't bind)".

Paul

Franklin? Lee

unread,
May 25, 2016, 1:10:36 PM5/25/16
to Paul Moore, Python-Ideas
On Wed, May 25, 2016 at 5:26 AM, Paul Moore <p.f....@gmail.com> wrote:
> On 25 May 2016 at 10:04, Franklin? Lee <leewangzh...@gmail.com> wrote:
>> Problem: `Point(x, y, ...)` is a legitimate function call, so if
>> `Point(x, 0)` is a legal pattern (i.e. no distinguishing syntax
>> between values and bindnames), you'd need the syntax to be `Point(x,
>> y, *...)`. Personally, I'd require that everything is a bindname
>> (unless it looks like a constructor call), and require checks to be in
>> guards.
>
> With the above syntax, "bare" values aren't a valid match, so in a
> match, Point(x, y) can never be a function call, it must be a match
> "Object of type Point, with x and y attributes (which we check for but
> don't bind)".

I mean a function call, in particular a constructor call.

Take these examples, written as constructor destructure assignment:
Point(x, y) = obj
Point(x=_, y=_) = obj

I'd say that Point(x, y) binds two positional constructor arguments,
and Point(x=_, y=_) binds attributes .x and .y to throwaway variables.
(`object` takes no positional args, and I want it to start allowing
keyword args.)

Here's my proposed implementation again: Call Point.__asmatch__(obj,
nargs, keys, args, kwargs) (where "asmatch" might be an enhanced
"__getnewargs__"), with these params:
- obj: The object to destructure, which is an instance of Point
(possibly typechecked by the match engine before calling). (Note that
we're not calling obj.__asmatch__(nargs, ...), to give superclasses a
chance to destructure a subclass instance.)
- nargs: Number of positional args.
- keys: Keyword args specified.
- args: Whether or not *rest was used. (Extension: Whether or not
splat was used, and whether to discard them.)
- kwargs: Whether or not **rest was used. (Extension: and whether
to discard them.)

It will return (args: tuple, kwargs: dict) if the match is possible.
It will check whether there are enough positional args, and whether
all keys were valid, and perform the logic about the optional params
(giving up as few args and kwargs as possible).

Contract: The returned tuple and dict _should_ be valid for
Point(*args, **kwargs). This constraint is not necessary if "discard"
was specified for anything.

My problem is, this would then be ambiguous:
Point(0, ...) = obj # Match if the first arg is 0 and the
second is `...`?

while this would not, but might be ugly.
Point(0, *...) = obj