Skip to content
This repository has been archived by the owner on Nov 21, 2022. It is now read-only.

Consider 'PATTERN as name' instead of 'name := PATTERN' #140

Open
gvanrossum opened this issue Aug 17, 2020 · 22 comments
Open

Consider 'PATTERN as name' instead of 'name := PATTERN' #140

gvanrossum opened this issue Aug 17, 2020 · 22 comments
Labels
accepted Discussion leading to a final decision to include in the PEP fully pepped Issues that have been fully documented in the PEP sprint

Comments

@gvanrossum
Copy link
Owner

Towards the end of this python-dev message, Jean Abou Samra lists some reasons why PATTERN as name would be more readable than name := PATTERN. I don't recall that we considered this before, but right now the two look about equal (i.e., I can't think of a good reason why as wouldn't work), so we should at least think about it.

@gvanrossum gvanrossum added needs more pep An issue which needs to be documented in the PEP open Still under discussion labels Aug 17, 2020
@Tobias-Kohn
Copy link
Collaborator

I agree that this is sensible enough to warrant consideration and a brief discussion. Moreover, the as variant also looks more or less equal to me as well, so I have no string feeling for or against such a change.

Concerning the reasons that Jean Abou Samra mentions:

The walrus := looks quite similar to the equality sign =, which, however, has completely different meaning. I find this quite a compelling and good argument in favour of as. It really might help flatten the learning curve a bit.

Pattern matching is close to try statements. This is a very common misconception and just another "pattern matching is extended switch" in disguise. However, that the walrus operator := is by design an expression is indeed an interesting thought.

There is another aspect, though, that I like about this: using as means that all values flow from left to right. If you consider something like case C(a=pat1, b=pat2):, then the attributes a and b go into the patterns pat1 and pat2, respectively. With pat2 as x, this flow from left to right is kept up, which seems more uniform to me.

@stereobutter
Copy link

stereobutter commented Aug 17, 2020

Especially with multiple walrus operators in a single line one easily gets cross-eyed, compare:

match value:
    case a:= Number(), b:= Number(): ...

vs

match value:
    case Number() as a, Number() as b: ...

Would that mean though that we'd either get the walrus operator or pattern as foo? For binding a sub pattern inside a nested pattern I can imagine that using foo:=SubPattern() might look nicer.

@gvanrossum
Copy link
Owner Author

Another point to consider: The := operator in normal expressions assigns exactly the value of what's on its RHS. The as form in general does some processing of its LHS, e.g. import foo as bar does an import, except E as err: doesn't assign E to err but an exception matching E (typically an instance of E), and with cmgr() as var: is quite different from with (var := cmgr()):.

So is the walrus pattern more like := in expressions, or does it do additional processing on its argument? I would say there is processing; e.g. case a := Number(): tries to match the target to the pattern Number() and then assigns the target to to a.

Would that mean though that we'd either get the walrus operator or pattern as foo? For binding a sub pattern inside a nested pattern I can imagine that using foo:=SubPattern() might look nicer.

We should have one or the other, not both. (To see for yourself, try a more complex, complete example both ways.)

@Tobias-Kohn
Copy link
Collaborator

Would that mean though that we'd either get the walrus operator or pattern as foo?

Yes, at least as I understand it. The rationale for using as is based on the notion that := can be misleading and might not fit the overall syntax of patterns well, and should therefore not be used. It would not make sense to have both, then (not to mention the "one and only one way" 😉).

@stereobutter
Copy link

@Tobias-Kohn

There should be one-- and preferably only one --obvious way to do it.

but I agree that PATTERN as NAME really does spell nice because, as you said, the variable binding flows from left to right as one reads.

@Tobias-Kohn
Copy link
Collaborator

So is the walrus pattern more like := in expressions, or does it do additional processing on its argument? I would say there is processing [...]

This is an interesting point (highlights from me) and I fully agree.

@stereobutter
Copy link

stereobutter commented Aug 17, 2020

Using PATTERN as NAME also kills two birds with one stone in that people apparently really really want to have as used somewhere in the syntax for match (evident by the piles of dried bike-shedding paint in the mailing list) and that it strengthens the case for case (pun very much intended) since

match value:
    with PATTERN as NAME: ...

would just read too much like a context manager to be easily teachable (in my personal opinion ).

@stereobutter
Copy link

stereobutter commented Aug 17, 2020

Since PATTER as NAME seems to have garnered some some positive attention I'd like to put forward (without a strong opinion on this) an extension, namely PATTER as NAME if GUARD. A minimal example:

match value:
    case Number() as a if a > 0, Number() as b if b > 0: ...

instead of

match value:
    case Number() as a, Number() as b if a > 0 and b > 0: ...

The advantage is again much like with PATTERN as NAME that the when reading the case the logic flows left to right and the guard for a variable is placed right next to it instead of at the end. Also since there is no kind of delimiter between last pattern and the guard

Number() as b if a>0 and b>0`
#            ^
#            | 
#   no delimiter there

the current syntax looks a bit like the last pattern is somehow special because it can have a guard.


This is probably somewhat related to #130 about non-backtracking behavior of guards that suprised me because my first language (Mathematica) does backtrack guards.

@gvanrossum
Copy link
Owner Author

I don't want to open up the floor right now for case PAT1 if GUARD1, PAT2 if GUARD2:.

However, I would be happy to rearrange the grammar rules so you can write the following:

case PAT if GUARD: ...
case (PAT1, PAT2, ...) if GUARD: ...

but the following would be a syntax error:

case PAT1, PAT2 if GUARD: ...

That way we can change our mind in the future.

@stereobutter
Copy link

stereobutter commented Aug 18, 2020

Since case PAT1 if GUARD, case PAT2 if GUARD: ... already is invalid syntax and raises SyntaxError I assumed individual guards for sub patterns could be added later without introducing parentheses like in case (PAT1, PAT2, ...) if GUARD now?


Yet another benefit of allowing PATTER as NAME instead of NAME := PATTERN is when one wants to match dicts/mappings and sequences, compare:

match value:
    case dict(): {foo(k): bar(v) for k, v in value.items()} 
match value:
    case d:= dict(): {foo(k): bar(v) for k, v in d.items()} 
match value:
    case dict() as d: {foo(k): bar(v) for k, v in d.items()} 
match value:
    case {**kwargs}: {foo(k): bar(v) for k, v in kwargs.items()} 

I dislike 1. because when there are lots of cases the definition of value is visually far removed from the case, 2. and 3. are both okay but I'd much prefer 3. and 4. is just too magical for this simply use case of matching the whole mapping.

@gvanrossum
Copy link
Owner Author

Since case PAT1 if GUARD, case PAT2 if GUARD: ... already is invalid syntax and raises SyntaxError I assumed individual guards for sub patterns could be added later without introducing parentheses like in case (PAT1, PAT2, ...) if GUARD now?

Yes, that’s the idea. Disallow it now (syntactically) so we can add it in the future.

——-

Yet another benefit of allowing PATTER as NAME instead of NAME := PATTERN is when one wants to match dicts/mappings and sequences, compare:
[…]

Note that case (4) is different from the others, it accepts any Mapping, vs. just dict and subclasses for the others.

Regarding (1) vs. the rest, that’s going to be different in different cases, so it’s good to have options here.

And (2) vs. (3) is pretty much esthetics, it’s hard to argue about. Also we already had that discussion above and came out in favor of “as”, so no need to repeat this.

@gvanrossum
Copy link
Owner Author

Note to self: Add Jean Abou Samra to Acks in PEP.

@jeanas
Copy link

jeanas commented Aug 20, 2020

Thanks.

@gvanrossum I guess @SaschaSchlemmer meant that, in a future where guards would be allowed for subpatterns (for what could be called "guarded patterns"), we could have "PAT1, PAT2 if COND" mean "(PAT1, PAT2) if COND" so the current behavior would be kept without a need for syntax restrictions today. In other words, the comma (as well as "as" and "|") would bind tighter than "if". I think it makes sense as global guards are going to be much more frequent (and they are the only ones allowed by the current proposal).

Also, it'll be easier to detect your mistake in case you understand it the wrong way. Take PAT1 | PAT2 if COND. Let's assume it means PAT1 | (PAT2 if COND). You could write

case int(x) | Fraction() as x if x > 0:

and because you wrongly think it means "(int(x) | Fraction() as x) if x > 0", you get negative integers matched, leading to weird failures. Now assume "PAT1 | PAT2 if COND" means "(PAT1 | PAT2) if COND" but you think of it the other way. You're going to write, for example,

case int(x) | Fraction() as x if x.denominator == 1:

and the problem will soon become obvious because int objects have no attribute 'denominator'.

However, I'm not sure that guarded patterns are terrible with 'if' as a keyword. With the current draft, you can read 'case ... if ...' which is natural, but as long as 'PAT if COND' is parenthesized, it very much looks like the start of a ternary operator A if C else B, whereas it has quite a different meaning.

To summarize:

  • I think that guarded subpatterns are independent from this issue.
  • They could be introduced later without a need for syntactical restrictions now.
  • Yet, it'd be beneficial to discuss alternative spellings for 'if' now, like 'where', 'when' or 'with', because that will be set in stone by the PEP at hand.

@brandtbucher
Copy link
Collaborator

I don't think that's workable, though. It would mean that:

match ...:
    case P1 if C1, P2 if C2: ...

...is equivalent to...

match ...:
    case ((P1 if C1), P2) if C2: ...

...which I think everyone can agree is unintuitive. Either way, this idea is probably off-topic for this issue.

@jeanas
Copy link

jeanas commented Aug 20, 2020

Oh! Indeed, now I understand why Guido was proposing to restrict the syntax. I'm sorry for having cluttered this issue with pointless text.

@gvanrossum
Copy link
Owner Author

Note that all comments since this one are irrelevant to the topic at hand (the discussion wandered to nested guards).

@brandtbucher
Copy link
Collaborator

brandtbucher commented Oct 13, 2020

Can we make a decision on this? I now favor as for its left-to-right semantics and readability.

Unlike | and _, there's no real visual reason to prefer := over as. The best logic I can think of is that walrus patterns function similarly to assignment expressions, but I fear that the analogy is shaky at best and potentially controversial. It's not an obvious choice to me, and I think we also have some minor gotchas in our grammar where unparenthesized tuples and walruses don't interact the way they do elsewhere in the language.

I believe that going left-to-right also clears up readability in these unparenthesized cases: it's much clearer to me what Point(), as x or Point() as x, do than x := Point(),. Is x a Point, or a 1-tuple containing a Point? I honestly forget!

"as-patterns"? I've gotten used to calling | patterns "or-patterns", but I know our terminology has varied somewhat.

@Tobias-Kohn
Copy link
Collaborator

Agreed. I am also in favour of as rather than the walrus :=. I think the current version of the PEP says something like that the SC might want to weigh in on this, though. And although I favour the as, I could live just as fine with := if the SC thinks that one better.

@gvanrossum
Copy link
Owner Author

+1

If we're in agreement here I see no need to ask the SC (they can always ask that we change it back if they don't like it).

Could one of you submit a separate PR that changes this in the PEPs? (Or one PR per PEP, at your choosing -- coordinate here without me.)

@gvanrossum
Copy link
Owner Author

(Also, this comment provides useful motivation for the rationale: #140 (comment))

@Tobias-Kohn
Copy link
Collaborator

I am updating PEP 635.

@gvanrossum gvanrossum added accepted Discussion leading to a final decision to include in the PEP sprint and removed open Still under discussion labels Oct 19, 2020
@gvanrossum
Copy link
Owner Author

This has been addressed for PEP 634 and PEP 635 on master; and for PEP 636 in python/peps#1658, so labeling as "fully pepped".

@gvanrossum gvanrossum removed the needs more pep An issue which needs to be documented in the PEP label Oct 20, 2020
@gvanrossum gvanrossum added the fully pepped Issues that have been fully documented in the PEP label Oct 20, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
accepted Discussion leading to a final decision to include in the PEP fully pepped Issues that have been fully documented in the PEP sprint
Projects
None yet
Development

No branches or pull requests

5 participants