|
|
Log in / Subscribe / Register

"Structural pattern matching" for Python, part 1

By Jake Edge
August 5, 2020

We last looked at the idea of a Python "match" or "switch" statement back in 2016, but it is something that has been circulating in the Python community both before and since that coverage. In June it was raised again, with a Python Enhancement Proposal (PEP) supporting it: PEP 622 ("Structural Pattern Matching"). As that title would imply, the match statement proposed in the PEP is actually a pattern-matching construct with many uses. While it may superficially resemble the C switch statement, a Python match would do far more than simply choose a chunk of code to execute based on the value of an expression.

Proposal

Guido van Rossum introduced PEP 622 to the python-dev mailing list on June 23; he was one of five co-authors of the PEP along with Brandt Bucher, Tobias Kohn, Ivan Levkivskyi, and Talin. Van Rossum's introduction did not include the PEP itself (original version), which is somewhat unusual, but instead consisted of the contents of a README file, "which is shorter and gives a gentler introduction than the PEP itself". It notes that the python-ideas mailing list had "several extensive discussions" about some kind of match statement over the years, which were summarized by Kohn in a blog post in 2018. The introduction starts with a fairly simple example:

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 401:
            return "Unauthorized"
        case 403:
            return "Forbidden"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something else"

That is pretty self-explanatory except perhaps for "_", which is a wildcard that will match anything. The idea is that each case will be tried in the order specified, the first to match will be executed, and the match statement terminates; only zero or one of the case entries will be executed and there is no falling through as with C. But this example is also deceptively similar to the C switch statement. Multiple case values can be combined in a single line, however, (e.g. case 401|403:) which is somewhat more compact than the C equivalent.

The next example starts to show where the proposed match statement takes a different path:

# The target is an (x, y) tuple
match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"Y={y}")
    case (x, 0):
        print(f"X={x}")
    case (x, y):
        print(f"X={x}, Y={y}")
    case _:
        raise ValueError("Not a point")

If point is not a tuple of two values, it will match the wildcard case and raise the exception, otherwise it will match one of the four cases given. For the three cases that are not the origin (i.e. (0, 0)), the variables in the case get replaced with their corresponding values from point, thus they can be printed out. These variable "extractions" are new, different behavior than what may be expected in Python; a point that is (0, 1) will not only match the second case, it will bind y to the value 1. Here is some sample output:

    # point = (23, 0)
    X=23
    # point = (0, 'foo')
    Y=foo
    # point = ('0', '0')
    X=0, Y=0

There is lots more to the proposal, as the introductory message and the voluminous PEP describe, including using Python dataclasses, guard clauses, sequence and mapping patterns, extracting sub-patterns with the walrus operator (":=") that came from the contentious PEP 572, and more. Here is an example that (perhaps nonsensically) combines several of those in the introduction:

    from dataclasses import dataclass

    @dataclass
    class Point:
	x: int
	y: int

    def test(item):
	match item:
	    case Point(x, y) if x == y:
		print('A Point on the diagonal')
	    case [ Point(x1, y1), p2 := Point(x2, y2) ]:
		print(f'Two Points, x coord of the first is {x1}, the second Point is {p2}')
	    case { 'bandwidth' : b, 'latency' : l }:
		print(f'not a Point but it has {b} for bandwidth with {l} for latency')
The first case uses a guard clause on the Point data class to restrict it to only match on points with the same value for x and y. The second uses a sequence pattern to match a sequence of two points, extracting the second into p2. The last case is checking for a mapping object that has an entry for the two fields named bandwidth and latency. These patterns can be arbitrarily complex and composed in various ways, as might be expected.

One of the areas that drew complaints was the syntax for constants. Instead of using hard-coded values in match statements, developers will likely want to use symbolic constants, as the following example shows:

RED, GREEN, BLUE = 0, 1, 2

match color:
    case .RED:
        print("I see red!")
    case .GREEN:
        print("Grass is green")
    case .BLUE:
        print("I'm feeling the blues :(")
If the names were used without the dot, they would be interpreted as variable extractions (e.g. case RED: would match anything and assign it to the variable RED), so some syntactic mechanism must be used to disambiguate those cases. The PEP authors chose the dot, but many of those commenting on it were not particularly happy with that choice. There were plenty of other complaints as well.

Reaction

Antoine Pitrou had several concerns, but one of those was the "context switch" required when reading the code:

When reading and understanding a match clause, there's a cognitive overhead because [suddenly] `Point(x, 0)` means something entirely different (it doesn't call Point.__new__, it doesn't lookup `x` in the locals or globals...).

He suggested alternative syntax using with or @ rather than making the case look like the creation of a new object. In a somewhat similar vein, Greg Ewing suggested making it explicit that variables in the case statements were being bound:

[...] I would rather see something explicitly marking names to be bound, rather than making the binding case the default.
E.g.
   case Point(?x, 0):
This would also eliminate the need for the awkward leading-dot workaround for names to be looked up rather than bound.

The PEP says this was rejected on the grounds that binding is more common than matching constants, but code is read more often than it is written, and readability counts.

Van Rossum noted that the PEP authors "did plenty of bikeshedding in private" about how to specify a case condition; he suggested that if there were a groundswell of support for some alternative, it could be adopted instead. Daniel Moisset was strongly in favor of the overall idea, but took some of the concerns even further, suggesting that the use of the dot on constants makes it easy for developers to shoot themselves in the foot:

The "We use a name token to denote a capture variable and special syntax to denote matching the value of that variable" feels a bit like a foot-gun.

He listed two possibilities that he found to be less dangerous: using angle brackets around to-be-captured variable names (e.g. "<x>") or effectively putting the capture variables in their own namespace by matching into a "capture object":

match get_node() into c:
    case Node(value=c.x, color=RED): print(f"a red node with value {c.x}")
    case Node(value=c.x, color=BLACK): print(f"a black node with value {c.x}")
    case c.n: print(f"This is a funny colored node with value {c.n.value}")

Rob Cliffe was generally in favor of the PEP, but was concerned with using "|" for listing alternatives in matches given that it already has a different meaning (i.e. bitwise OR). In addition, he agreed with others who were suggesting that "else" be used instead of the wildcard value as the default case when nothing else matches. Using "_" "is obscure, and [wouldn't] sit well with code that already uses that variable for its own purposes". The single-underscore "variable" is a convention used in Python to denote a value that is not needed (i.e. a throwaway value); for example:

    x, _, z = foo

That will unpack the three-element foo sequence and throw away the middle value. Its connection to wildcards is somewhat tenuous, though. Chris Angelico wondered if using the ellipsis ("...") made sense instead, which Barry Warsaw also favored. But Van Rossum said that he would find ellipsis confusing:

The problem is that ellipsis already has a number of other meanings, *and* is easily confused in examples and documentation with leaving things out that should be obvious or uninteresting. Also, if I saw [a, ..., z] in a pattern I would probably guess that it meant "any sequence of length > 2, and capture the first and last element" rather than "a sequence of length three, and capture the first and third elements". (The first meaning is currently spelled as [a, *_, z].)

He also pointed out that other languages use "_" for wildcards as well. The current version of the PEP puts it this way:

Perhaps the most convincing argument is that _ is used as the wildcard in every other language we've looked at supporting pattern matching: C#, Elixir, Erlang, F#, Haskell, Mathematica, OCaml, Ruby, Rust, Scala, and Swift.

One counterargument is that internationalization (i18n) libraries often use the underscore to indicate a string that needs translation (e.g. _('This string should be localized')), which makes the use as a wildcard somewhat confusing. The two uses would not collide, though, because the pattern for a case is treated differently than other language constructs—which may be part of what's causing some of the negative reaction to the PEP.

Marc-André Lemburg, who wrote the rejected PEP 275 ("Switching on Multiple Values") back in 2001, was happy to see the new PEP, but was surprised, like many others, about the lack of an else for the catch-all case. Van Rossum, who wrote the also-rejected PEP 3103 ("A Switch/Case Statement") in 2006 targeting Python 3, noted that, in the 19 years since PEP 275, "there are now some better ideas to steal from other languages than C's switch. :-)" He said that the authors of the current PEP were split on the question of else to some extent.

The authors don't feel very strongly about whether to use `else:` or `case _:`. The latter would be possible even if we added an explicit `else` clause, and we like TOOWTDI [there's only one way to do it]. But it's clear that a lot of people *expect* to see `else`, and maybe seeing `case _:` is not the best introduction to wildcards for people who haven't seen a match statement before.

A wrinkle with `else` is that some of the authors would prefer to see it aligned with `match` rather than with the list of cases, but for others it feels like a degenerate case and should be aligned with those. (I'm in the latter camp.)

He said that the authors were still discussing it and, once they reach agreement, would update the proposal. Lemburg was concerned that "case _:" is "just too easy to miss when looking at the body of 'match'". But as Van Rossum and others noted, a case with just a wildcard (whatever the syntax for that ends up being) will be valid syntax, so an else is not strictly needed, though it may still be desirable for other reasons.

The PEP also had a lengthy section on a __match__() protocol that could be used to customize how objects are matched, but it turned out to be a confusing idea with a lot of unclear corner cases. It drew lots of complaints and has been dropped in later versions of the PEP.

Gregory P. Smith raised concerns about confusing readers of the code, especially those who are not well-versed in Python. He posited showing a chunk of code using match to someone who does not know the language:

match get_shape():
    case Line(start := Point(x, y), end) if start == end:
        print(f"Zero length line at {x}, {y}")
I expect confusion to be the result. If they don't blindly assume the variables come from somewhere not shown to stop their anguish.

With Python experience, my own reading is:

  • I see start actually being assigned.
  • I see nothing giving values to end, x, or y.
  • Line and Point are things being called, probably class constructions due to being Capitalized.
  • But where did the parameter values come from and why and how can end be referred to in a conditional when it doesn't exist yet? They appear to be magic!

Did get_shape() return these? (i think not). Something magic and implicit rather than explicit happens in later lines. The opposite of what Python is known for.

He suggested that making the match conditions look like a call to a class constructor would simply be too confusing. He presented alternative syntax, but Ewing called that "inscrutable [...] in its own way". Paul Moore disagreed with the idea that those with Python knowledge would be completely confused as portrayed by Smith; it may take a bit of effort to get there, but Moore is confident that developers would come up to speed on match fairly quickly.

Needed?

Mark Shannon questioned whether the PEP truly outlined a serious problem that needed solving in the language. The PEP describes some anecdotal evidence about the frequency of the isinstance() call in large Python code bases as a justification for the match feature, but he found that to be a bit odd; "[...] it would be better to use the standard library, or the top N most popular packages from GitHub". He also wondered why the PEP only contained a single example from the standard library of code that could be improved using match. "The PEP needs to show that this sort of pattern is widespread."

The example cited by the PEP does make it clearer why isinstance() is mentioned. The basic idea is that heterogeneous data is frequently "destructured"—objects of various sorts have their internal data pulled out in different ways—in large Python code bases. The original PEP puts it this way:

This PEP aims at improving the support for destructuring heterogeneous data by adding a dedicated syntactic support for it in the form of pattern matching. On very high level it is similar to regular expressions, but instead of matching strings, it will be possible to match arbitrary Python objects.

We believe this will improve both readability and reliability of relevant code. To illustrate the readability improvement, let us consider an actual example from the Python standard library:

def is_tuple(node):
    if isinstance(node, Node) and node.children == [LParen(), RParen()]:
        return True
    return (isinstance(node, Node)
            and len(node.children) == 3
            and isinstance(node.children[0], Leaf)
            and isinstance(node.children[1], Node)
            and isinstance(node.children[2], Leaf)
            and node.children[0].value == "("
            and node.children[2].value == ")")

With the syntax proposed in this PEP it can be rewritten as below. Note that the proposed code will work without any modifications to the definition of Node and other classes here:

def is_tuple(node: Node) -> bool:
    match node:
        case Node(children=[LParen(), RParen()]):
            return True
        case Node(children=[Leaf(value="("), Node(), Leaf(value=")")]):
            return True
        case _:
            return False

The proposed syntax is far more clear—though there are some conceptual hurdles to surmount—but it is not so obvious that the problem is truly widespread. Shannon would seem to be concerned that match may be a feature that is in search of real use cases.

Moisset said that he had put together some extensive notes on the feature. In them, he argued that the feature was not really about pattern matching and was, instead, about introducing algebraic data types, which come from functional languages, into Python. Beyond that, he also described the proposal rather more clearly than the PEP itself does, which is likely part of what got him invited to join the author group of the PEP for round two.

Just over 24 hours after his initial post, Van Rossum called for something of a pause in all of the comments pouring into the mega-thread. He noted four separate items that the PEP authors now knew were contentious (an alternate spelling for "|", else, a different wildcard token instead of "_", and what to do about the dot notation for constants) and asked that folks wait to add additional comments on those choices until the authors could come to some agreement. The final item was also meant to cover the possibility of changing to marking variables (e.g. "?foo") rather than constants as had been suggested. He asked that any other concerns with the PEP be concisely added to his new thread, which several did—though at a much-relaxed pace compared to the original.

That takes us up near the end of June in this tale, but there is more to come. The authors came back with a second version of the PEP, without the __match__() protocol, and dropping the dot notation for constants, replacing it with a requirement that constants in case entries be referenced from some namespace (thus have a dot in their representation: Color.RED), but making few other substantive changes—beyond a gentler introduction courtesy of Moisset. That set off another mega-thread along with several other threads discussing specific aspects of the PEP. We will pick up where we left off soon; stay tuned.


Index entries for this article
PythonEnhancements
Pythonmatch statement
PythonPython Enhancement Proposals (PEP)/PEP 622


to post comments

"Structural pattern matching" for Python, part 1

Posted Aug 5, 2020 17:16 UTC (Wed) by mb (subscriber, #50428) [Link] (14 responses)

No, please don't!
This syntax just feels "backwards" to me.
match point:
    case (x, 0):
        print(f"X={x}")
I would expect that to match, if point==(x,0) with x being defined before the match. I don't expect the case to assign anything to x.
case 401|403:
I would expect this to match if the value was 403 only. (bitwise OR)
case _:
Horrible. This is Perl-like. Use 'else'. That is Python-like.
case Point(?x, 0):
Special characters which are not intuitive to understand. Perl, Perl, Perl.
match color:
    case .RED:
What is being subscripted/looked up here with the dot? Yes, nothing.

All the example are just horrible to read. They are much harder to read then what they replace.

"Structural pattern matching" for Python, part 1

Posted Aug 5, 2020 21:00 UTC (Wed) by NYKevin (subscriber, #129325) [Link] (1 responses)

Re the bitwise OR thing: I'm confused why they didn't just use logical OR there? In more complicated cases, you're very likely going to want short-circuit pattern matching (i.e. you probably should not be filling any variables on the "wrong" side of the logical operator, especially if they collide with the "right" side), so that seems like a better semantical fit for what they are trying to accomplish. Regardless, bitwise OR is needed for writing some constant values that cannot be expressed in any other way (see https://docs.python.org/3/library/enum.html#flag), so they certainly need to pick a different operator anyway.

I very much doubt there's a serious use case for "match the left pattern, but then if that pattern match succeeds *and* the thing you're matching against is falsey, throw out the whole match and start over with the right pattern instead" (which is the only other sensible interpretation of logical OR in this context). I also doubt anyone would intuitively interpret it that way, since that's just too ridiculous. So logical OR seems to me like the least-worst option here.

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 14:14 UTC (Thu) by kevincox (subscriber, #93938) [Link]

I agree, this seems like a logical or to me so I don't see why they chose `|` over the `or` keyword.

"Structural pattern matching" for Python, part 1

Posted Aug 5, 2020 22:27 UTC (Wed) by logang (subscriber, #127618) [Link] (4 responses)

I agree. One of the things I liked about Python over Perl and Ruby is not having to parse some weird magic stuff like this.

The is_tuple() example is pretty clear for anyone vaguely familiar with programming. (It's also in lib2to3 code that should be pretty dead by now anyway). The match replacement is not clear: on first blush I'd expect it not to be equivalent and only match in cases that are far more specific. It's only saves a couple lines anyway.

Cleaning up the offending code itself would probably be a better idea. It's already very odd that the empty tuple case matches different LParen and RParen while the full tuple case allows for more general Leaf instances.

But something like this would probably be less tedious to read and not require nasty match syntax:

def is_tuple(node):
    if not isinstance(node, Node) or not node.children:
        return False
    if node.children[0] != LParen() or node.children[-1] != RParen():
        return False
    if len(node.children) == 2:
        return True  # Empty Tuple
    elif len(node.children) == 3:
        return isinstance(node.children[1], Node)
    else:
        return False

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 18:51 UTC (Thu) by intgr (subscriber, #39733) [Link] (3 responses)

> But something like this would probably be less tedious to read and not require nasty match syntax: [... example ...]

I agree that's the best way to write it currently. But getting rid of such verbose "inverted logic" checking code is the reason why I'm very excited for Python's match/destructuring syntax.

Every piece of criticism by mb is spot on, however. The syntax needs to be redesigned.

After some dabbling in Rust, I'm now convinced that pattern matching is a feature that every modern language should have. I've also found the destructuring features of TypeScript (JavaScript) very helpful, Python has been lagging behind but pattern matching would catch up with and surpass what they have to offer.

"Structural pattern matching" for Python, part 1

Posted Aug 7, 2020 3:41 UTC (Fri) by NYKevin (subscriber, #129325) [Link] (2 responses)

I think part of the problem with Python is that it doesn't have explicit variable declarations. In Python, you create a local variable by assigning to it, which is great for minimizing typing, but it also means that your destructuring syntax has no obvious way of distinguishing between "please create a new variable to hold this value" and "please compare this value against this variable."

I can think of several possible fixes for this:

1. If the variable exists in any visible scope, then assume we're comparing against it, otherwise capture into it. A local variable exists throughout the scope in which it is created, so that (for example) you can get an UnboundLocalError if you try to read from a local variable that has not yet been assigned to (even if there's a global with the same name, it's shadowed from the moment you enter the function scope). If you allow capturing to create a variable, then the variable exists in the local scope, and so we must instead interpret it as a comparison, but then the variable does not get created by capturing, and does not exist (in that scope or anywhere else), making it ineligible for comparison. This is an infinite loop.
2. Matching creates a scope. This would be extremely bizarre, since nothing in Python creates a scope besides functions and classes (and modules, I suppose). It also means that you need to use a lot of nonlocal foo declarations if the match is going to set any variables in the enclosing function scope, or else you need to make Python's name resolution rules even more complicated than they already are.
3. If the local variable has a type annotation, then assume we're comparing against it, otherwise capture it. This means you can't annotate the results of pattern matching. Also, some people are allergic to type annotations and don't want to be forced into using them.
4. Captures always become attributes of a separate match object, which is created explicitly using the "as" syntax that several people have suggested. This is a bit more verbose, but probably the least-worst option in this list.
5. Either capturing or comparing is the default, and you need to use special syntax to do the other. The current proposal seems to prefer capturing, but preferring comparing instead is probably no better. Either way, there's a special case which developers have to remember, and ugly syntax associated with that case. You probably could make the syntax a bit less ugly using some sort of upfront "match [expr] into [variables...]" syntax, but that would make it a bit too easy to forget to update "[variables...]" when you add a new one.

"Structural pattern matching" for Python, part 1

Posted Aug 7, 2020 8:48 UTC (Fri) by Wol (subscriber, #4433) [Link] (1 responses)

6. Neither is the default? so you have to be explicit?

Or does that collide with something else?

Cheers,
Wol

"Structural pattern matching" for Python, part 1

Posted Aug 7, 2020 21:14 UTC (Fri) by NYKevin (subscriber, #129325) [Link]

That has some "worst of both worlds" vibes for me, although you are correct that it would be more explicit. But I have a hard time imagining a good syntax for that.

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 3:47 UTC (Thu) by cyphar (subscriber, #110703) [Link] (2 responses)

A lot of these seem to be based on Rust's match feature (it even has the same name!). I agree that this seems out-of-place for Python, and while I think Python's staunch refusal to include switch-like statements is a flaw of the language (and one I'd like to see fixed) I wouldn't be happy with this design. Rust's matching design makes sense in the context of the rest of the language -- you can't just transplant it to another language and hope it fits.

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 5:00 UTC (Thu) by Cyberax (✭ supporter ✭, #52523) [Link] (1 responses)

Uhh... Pattern matching has existed since ML. Not "Machine Learning", since the original Standard ML language developed in 1983: https://www.cs.nmsu.edu/~rth/cs/cs471/sml.html#pattern

"Structural pattern matching" for Python, part 1

Posted Aug 7, 2020 13:07 UTC (Fri) by anton (subscriber, #25547) [Link]

ML was developed by Robin Milner and accomplices in the 1973-1978 time frame. The 1978 book on Edinburgh LCF contains a detailed description of ML. It seems to me that the dominant ML dialect in this century has been OCaml.

Prolog was slightly earlier (starting in 1972); there pattern matching is a natural result of unification (unlike pattern matching in ML, which is a special language feature); the syntax, though, is not that far to what we see in ML or in the proposal. And maybe the contentious issue "Is it the value of the name or a new variable to be bound?" is there because there is no difference in Prolog (logic variables work differently from functional/imperative variables). E.g., the first clause of the definition test from the article can be written similar to the example:

test(point(X,Y),Z):- X=Y, Z="A point on the diagonal".
or as:
test(point(X,X), "A point on the diagonal").
Making a query for test(point(1,2),Z) will try to unify point(1,2) with point(X,X), which fails, because X cannot be simultaneously be bound to 1 and 2.

I don't remember how the value-or-unbound issue is solved in ML.

An earlier language with pattern matching is Snobol (started 1962), but I don't know enough about Snobol to compare it.

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 7:15 UTC (Thu) by flussence (guest, #85566) [Link] (3 responses)

This comment's misinformed complaining is amusing because Perl/Raku's equivalent given/when syntax looks nowhere near as terse, unreadable or alien as these cherry-picked examples.

I'll agree that they are horribly shoehorned in and out of place though. That's what happens when your language doesn't evolve for 15 years then tries to steal features from other languages verbatim. Perl 5 fell into the same trap.

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 16:28 UTC (Thu) by mb (subscriber, #50428) [Link] (2 responses)

> This comment's misinformed complaining

These were just quotes from the article. How can that be misinformed?
I'm fully aware that there are alternative spellings in the article for the examples I picked from the acticle, but I just wanted to comment on the worst ones. That doesn't mean I agree with the alternatives, though.

> Perl/Raku's equivalent given/when syntax looks nowhere near as terse, unreadable or alien as these cherry-picked examples.

I did not (want to) compare the exact features from the examples to Perl's equivalents.
I just said that the constuct of using magic variables (_) or operators (?) is Perl-like and not very Pythonic.

> I'll agree that they are horribly shoehorned in and out of place though.

Ok. So you basically agree with me.

> That's what happens when your language doesn't evolve for 15 years

Erm, it'd call _that_ misinformed, indeed.
Literally dozens of major good features have been integrated in the past 15 years.
One of the most recent ones being f-strings, for example.

"Structural pattern matching" for Python, part 1

Posted Aug 7, 2020 11:56 UTC (Fri) by flussence (guest, #85566) [Link] (1 responses)

> Literally dozens of major good features have been integrated in the past 15 years.
> One of the most recent ones being f-strings, for example.

Look up Perl's format() function. You won't find many uses of that in modern code, because the community collectively realised some time in the 20th century that making people carry the cognitive load of a loquacious bespoke sublanguage that duplicates half of printf (and then doesn't even fix strftime) is absolutely bananas.

And f-strings are explicitly runtime-evaluated. You don't even get constant-folding!

"Structural pattern matching" for Python, part 1

Posted Aug 7, 2020 13:41 UTC (Fri) by excors (subscriber, #95769) [Link]

Perl's documentation introduces "format" as being based on FORTRAN, BASIC and nroff, so it's hardly surprising that it doesn't attract many users nowadays. (https://perldoc.perl.org/perlform.html)

f-strings sound a lot more like Perl's interpolated strings (f"Hello {name}" vs "Hello $name") which are widely used, plus they have nicer syntax when embedding expressions (f"HELLO {name.upper()}" vs "HELLO @{[ uc $name ]}", the latter being a horrific but common Perl idiom). They've also got the printf-ish format specifiers which I doubt I'd be able to use without checking the documentation every time, but f-strings seem useful even without that.

"Structural pattern matching" for Python, part 1

Posted Aug 5, 2020 18:10 UTC (Wed) by donacthulhuote (guest, #136577) [Link] (2 responses)

Ignoring the fact I wouldn't be able to use this until wherever I'm working gets to python 3.10, I really like the proposal. The only language I know with similar capabilities is Rust, but once you get used to the syntax (which is admittedly a slight hurdle, and which people far smarter than me are working out in this case), it makes for really clean code in a number of cases (to my eyes). It also feels like a better abstraction for a large number of if statements in the cases where what you really want is, well, to match your object to something.

My only consternation is that a large part of the power in rust is that you're forced to deal with all cases, and compiler will help you along if you're missing anything. Python being dynamic doesn't have that luxury, and I wonder how much utility is lost thereby.

"Structural pattern matching" for Python, part 1

Posted Aug 5, 2020 18:15 UTC (Wed) by re:fi.64 (subscriber, #132628) [Link] (1 responses)

Well, type hint checkers can be updated to handle the new pattern matching syntax.

"Structural pattern matching" for Python, part 1

Posted Aug 5, 2020 21:33 UTC (Wed) by NYKevin (subscriber, #129325) [Link]

I think that will depend to some extent on the ability of the type linter to produce refutations. If the type linter cannot prove that a pattern (or series of patterns) is irrefutable, but it also cannot provide a sample refutation to those patterns, then the developer will be tempted to shove in a case _: pass clause "to make the type linter shut up." Depending on the circumstances, this may or may not be safe, and it certainly won't be future proof (if the patterns are later changed in such a way that they really do become refutable, then the type linter won't see anything wrong with the match).

On the other hand, if the type linter can guarantee that it will always be able to come up with a specific refutation, then it may actually help catch bugs much earlier in development. Given the complexity of Python's dynamic typing system, I think this is still a bit of an open problem, although I welcome corrections on this point.

"Structural pattern matching" for Python, part 1

Posted Aug 5, 2020 18:43 UTC (Wed) by pj (subscriber, #4506) [Link] (2 responses)

Yeah, I'm pretty 'sounds okay, but details need work' on this.

The auto-destructuring that confuses assigning a variable with use of variables and forces the use of a leading dot on existing variables is just bad. I like the proposed 'into' syntax much better, though I'd spell it 'as' since that spelling is used by both except clauses and context managers, so there's precedent and familiarity.

Also, how about 'default:' instead of 'else:' for the fall-through case name? `case _` I think is too tied to wildcards, when what you've got is really a pile of expressions, not patterns, and `else` _does_ seem like it should go on the `match` block instead of in the list of cases - but `default` is clearly a case and not part of the match block.

Sums up to the following, which I think is concise but still legible:

match get_point() as p:
    case Point(p.x, 0):
        print("y is zero")
    case Point(0, p.y):
        print("x is zero")
    case Point(0, 0):
        print("origin")
    default:
        print("No zeroes")
and
match get_shape() as s:
    case Line(s.start, s.end) if s.start == s.end:
        print(f"Zero length line at {s.x}, {s.y}")

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 8:46 UTC (Thu) by Karellen (subscriber, #67644) [Link] (1 responses)

I like it, but couldn't you just use as assignment expression and do:

match p := get_point():

The advantage of case (x, y): is that it will match any tuple and allow you to access the members as x and y, whereas doesn't your syntax only allow you to access structures whose members are already called x and y? Feel like I'm missing something here...

"Structural pattern matching" for Python, part 1

Posted Aug 7, 2020 21:25 UTC (Fri) by NYKevin (subscriber, #129325) [Link]

I interpreted it differently. My understanding of pj's comment was that "p" is not the point you matched against (because then you would just write p = get_point() and skip the entire match statement altogether). Rather, "p" is a generic object whose attributes are populated by the matcher. You could just as easily write "p.foo" and "p.bar" and it would work exactly the same.

Another way to think about it: "p." is a prefix meaning "capture a variable here." If a variable is not prefixed, then it's used as a comparison instead.

"Structural pattern matching" for Python, part 1

Posted Aug 5, 2020 18:47 UTC (Wed) by josh (subscriber, #17465) [Link] (1 responses)

I'd love to have this in Python.

One thought: we have pattern matching in Rust, and it's incredibly helpful. The whole "matching a declared constant" thing is a problem in Rust, too, and even though Rust constants are declared in advance and statically typed, it's *still* a potential source of ambiguity; we've wished several times that there was a dedicated syntax to avoid the ambiguity.

"Structural pattern matching" for Python, part 1

Posted Aug 9, 2020 15:39 UTC (Sun) by kleptog (subscriber, #1183) [Link]

Elixir uses pattern matching extensively and they simply prefix it with a tilde, like so:
iex> x = 1
1
iex> case 10 do
...>   ^x -> "Won't match"
...>   _ -> "Will match"
...> end
"Will match"
Seems like a straightforward approach.

"Structural pattern matching" for Python, part 1

Posted Aug 5, 2020 19:10 UTC (Wed) by ibukanov (subscriber, #3942) [Link] (3 responses)

Python has limited support for destructing assignments so one can write: x, _, y = (1, 2, 3). But Python does not support more advanced forms like JS { x, y: z, ...rest } = { x: 1, y: 2, foo: 3, bar: 4} that assigns 1 to x, 2 to z and { foo: 3, bar: 4} to rest. If Python first implemented that, using the same syntax in the match statement will be only natural.

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 1:23 UTC (Thu) by pj (subscriber, #4506) [Link] (1 responses)

Python 3 has
a, b, *c = 1, 2, 3, 4, 5
that yields (I think)
a=1, b=2, c=(2,3,4,5) 
that is a kind of 'rest of it', but does nothing with dicts at all.

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 17:48 UTC (Thu) by adam820 (subscriber, #101353) [Link]

Python 3 works out to:

a=1, b=2, c=[3, 4, 5]

But yes, you are correct. Neat, TIL.

"Structural pattern matching" for Python, part 1

Posted Aug 7, 2020 0:59 UTC (Fri) by pallas (guest, #128204) [Link]

This seems sane to me: if the match is really just an attempted assignment against the case statements, and then we improve assignment pattern matching, everyone wins. It's becomes borderline SFINAE.

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 6:22 UTC (Thu) by stephenjudd (guest, #3227) [Link]

Yay, this is bringing something genuinely useful from the ML family of languages into the Python world, analogous to bringing Haskell's list comprehensions over. It's just a question of finding a syntax that feels right.

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 7:46 UTC (Thu) by rwmj (subscriber, #5474) [Link]

I use proper pattern matching in OCaml all the time, it's a very useful feature. The authors of this feature might wish to read about how the implementation can be optimized by the compiler: https://caml.inria.fr/pub/papers/xleroy-zinc.pdf (page 64 and onwards).

"Structural pattern matching" for Python, part 1

Posted Aug 6, 2020 13:18 UTC (Thu) by jmaa (guest, #128856) [Link]

I love pattern matching in Standard ML/Ocaml, but can't get myself to care about it's inclusion in Python. How are you supposed to support data deconstruction, when your data can be represented in a bazillion different ways. Objects, enums, dicts, sets, lists, etc. etc. Object representation in Python honestly feels pretty bloated, and I doubt adding algebraic data types (a whole new class of types!) will help. Sure, with pattern matching, you can create functions that support all of these in turn, that's a lot of boilerplate to fix a self-created problem.

SymPy

Posted Aug 7, 2020 8:13 UTC (Fri) by marcel.oliver (subscriber, #5441) [Link]

It seems to me that SymPy might benefit enormously from such a facilty and would use it in nontrivial ways. Perhaps the development of this feature could even be guided by the complex requirements of symbolic mathematics.

Wow, impressive

Posted Aug 14, 2020 11:00 UTC (Fri) by HelloWorld (guest, #56129) [Link] (1 responses)

Python might eventually catch up with what OCaml had in 1995.

Wow, impressive

Posted Aug 15, 2020 17:18 UTC (Sat) by HelloWorld (guest, #56129) [Link]

Well, actually that's not quite right. This match statement appears to be, well, a statement, whereas in OCaml in 1995 it was already an expression. Even Java figured this one out with the switch expressions in Java 12. Once again, what Python brings to the table is... not very good, and kinda embarrassing.

"Structural pattern matching" for Python, part 1

Posted Aug 14, 2020 16:06 UTC (Fri) by felix.s (guest, #104710) [Link] (2 responses)

It seems that the new, neutered version (without __match__) will not be able to express something like this:
def matcher(*args, **kwargs):
    import re
    class PatternMatcher:
        __PATTERN = re.compile(*args, **kwargs)
        def __match__(cls, subject):
            m = cls.__PATTERN.match(subject)
            return m.groups() if m else None
    return PatternMatcher

rx_hex = matcher(br'0[Xx]([0-9A-Fa-f]+)')
rx_bin = matcher(br'0[Bb]([01]+)')
rx_oct = matcher(br'0[Oo]([0-7]+)')
rx_dec = matcher(br'([0-9]+)')

def parse_number(s):
    match s:
        case rx_hex(digits):
            return f'hexadecimal: {int(digits, 16)}'
        case rx_bin(digits):
            return f'binary: {int(digits, 2)}'
        case rx_oct(digits):
            return f'octal: {int(digits, 8)}'
        case rx_dec(digits):
            return f'decimal: {int(digits, 10)}'
        case _:
            return 'no match'
Pity, I might have wanted to use it for this.

"Structural pattern matching" for Python, part 1

Posted Aug 16, 2020 7:03 UTC (Sun) by bjartur (guest, #67801) [Link]

You can use if, elif and else.

"Structural pattern matching" for Python, part 1

Posted Aug 16, 2020 11:46 UTC (Sun) by HelloWorld (guest, #56129) [Link]

If that is what you're after, I'd encourage you to try Scala. Your code can be translated pretty much verbatim:
  import Integer.valueOf

  val rxHex = "0[Xx]([0-9A-Fa-f]+)".r
  val rxBin = "0[Bb]([01]+)".r
  val rxOct = "0[Oo]([0-7]+)".r
  val rxDec = "([0-9]+)".r

  def parseNumber(s: String) =
    s match {
      case rxHex(digits) =>
        s"hexadecimal: ${valueOf(digits, 16)}"
      case rxBin(digits) =>
        s"binary: ${valueOf(digits, 2)}"
      case rxOct(digits) =>
        s"octal: ${valueOf(digits, 8)}"
      case rxDec(digits) =>
        s"decimal: ${valueOf(digits, 10)}"
      case _ =>
        "no match"
    }
And in Scala 3 you won't even need braces around the case clauses! In fact, Scala 3 is going to be terrific: much easier and more regular, yet binary compatible with 2.13 (no split like between Python 2 and 3), and they even implemented significant whitespace syntax, which as far as I'm concerned is the only feature in Python that is worth having. You should really give it a try!

"Structural pattern matching" for Python, part 1

Posted Aug 26, 2020 22:47 UTC (Wed) by mcortese (guest, #52099) [Link]

I wonder why two such separate concepts as destructuring and case switching must go hand in hand.

I think destructuring ought to have its own syntax, something like

match point into (x, y)
which should return true or false whether the match succeeds or not. It's up to you to use it inside a if clause.

Totally orthogonal to this is the desire to provide a nice way to express a choice between several cases, similar to C's switch statement.

The two will often be used together, but needn't to.

"Structural pattern matching" for Python, part 1

Posted Sep 2, 2020 12:25 UTC (Wed) by ReallyNiceGuy (guest, #60085) [Link]

I call Greenspun's tenth rule


Copyright © 2020, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds