Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Honestly I'm not sure what all the fuss is about. I just skimmed over PEP 634 and PEP 636 (the accompanying tutorial) and I'm actually kind of excited to use this. I've missed this feature after using langs like Rust, Haskell, and Scala. People have pointed out some surprising edge cases such as this:

    NOT_FOUND = 404
    match get_status():
        case NOT_FOUND:
            # this actually assigns to the global NOT_FOUND
            # and is effectively the default case
        case _:
            # this never gets triggered
Yeah, I agree that's a bit ugly. But you can apparently do this:

    class Status(Enum):
        NOT_FOUND = 404

    match get_status():
        case Status.NOT_FOUND:
            # works as expected
        ...
And really how is any of that more ugly than this?

    def func_with_default(foo={}):
        if "bar" not in foo:
            # "bar" is set in the module-level dict that
            # was initialized when the func def was parsed,
            # not in a dict that is created per call
            foo["bar"] = ...
        ...
And we've been living with that one for years. These are just the things that you have to learn when you start using Python, just like any other language.

I, for one, am excited to start using this syntax.



A previous mistake does not make a convincing argument for making a new mistake. Also, the mutable default arguments problem is quite difficult to solve in any other way. Python never copies things for you.

Pattern matching is a desired feature, but the shortcomings are too costly for what it provides. You just outlined yourself the quickest foot-gun I've ever seen.

My second problem is that this violates many of the invariants you rely on while programming Python. f(x) always gives you the value of applying f to x. Nope, not in case. | means bitwise or. Nope, not in case. a means the value of a. NOPE. Not in case.


> Also, the mutable default arguments problem is quite difficult to solve in any other way. Python never copies things for you.

Why would it need to copy anything? What's wrong with just evaluating the default argument expression every time the function is called with a default argument rather than once at definition-time? That's how it works in other languages.


The variables used in the expressions might not be in scope (in fact they usually aren't). Also, I'm rather sure that's how it works in C++ (which by accident copies arguments instead of leaving a reference, but the single time default argument evaluation holds).


> The variables used in the expressions might not be in scope (in fact they usually aren't).

That's solved easily enough by evaluating the expression in the scope where it was defined (again that's what other languages do).

> Also, I'm rather sure that's how it works in C++

Default arguments in C++ work as I described: If you define `void f(int arg = g(x)) {}`, then calling `f()` n times will call `g` n times as well (using the `x` that was in scope when `f` was defined) - and if you never call `f`, `g` isn't called either.

An example demonstrating this: https://ideone.com/vs26Oq


That’s a good idea, I like that!


A variable name has never referred to the value of it in expressions where it is being assigned. In 'def f(a,b,c)' or '(a,*_) = x' the term 'a' doesn't refer to the value of a either. A match statement in this sense is just an assignment that has a graceful failing mechanism (rather than e.g. '(a,*_) = x' which throws for empty lists).

And I think the ship on '|' has sailed with the dictionaries.


Isn't it the same in ML though (using the same syntax in different contexts) ? Compare `| [a, b] => a + b` and `let my_list = [a, b]`. Same in JS, `let {a, b} = {a: 1, b: 2}` and `let d = {a, b}`. It's weird at first but then you just get used to it.

edit: updated list name since pipe ("|") and small L ("l") look kind similar in the code snippets.


> the mutable default arguments problem is quite difficult to solve in any other way. Python never copies things for you.

Why would it be a problem to do a copy in this case?

> f(x) always gives you the value of applying f to x

Not if it is preceded by "def"

> | means bitwise or.

Not if it had been overloaded, like many popular libraries do.

> a means the value of a

Not if it is followed by "="

How is that different from the new pattern matching syntax ?


> Why would it be a problem to do a copy in this case?

How are copy semantics even defined unambiguously?

> Not if it is preceded by "def"

In that case it is analogous, def f(x) DEFINES what f(x) means. For class definitions it's different, and it is indeed debatable if `class A(SuperClass)` is good syntax. I would have preferred `class A from SuperClass`, but that ship has sailed.

> | means bitwise or. Not if it had been overloaded, like many popular libraries do.

You missed the point willingly I think. See sibling comment. You cannot overload its meaning in a match, and the meaning is not related to what it otherwise means (bitwise or, set union, etc.).

> a means the value of a Not if it is followed by "="

And again, this is by analogy. a = 1 means that you substitute a with 1 wherever a occurs. Not so with the pattern matching syntax. If we had a = 1 then a == 1. Where is this analogy for pattern matching? How do you even make the comparison? You do not.

All of your counterpoints have reasonable analogies within the syntax itself (eg: the inverse of a function call is a function definition).


"f(x)" and "|" inside a regular expression mean something different. Similarly here, pattern matching has its own sub-language (a DSL).


Coincidentally, regular expressions are not part of the Python syntax. They're just string literals.


Are you being intentionally ironic here, or do you not know that regex DSL is written only inside of strings in python, not within the Python's syntax?


For what it's worth, I tried a modified version of the first example you've used above in the sandbox:

  NOT_FOUND = 404
  match get_status():
      case NOT_FOUND:
          print("Here")
          # this actually assigns to the global NOT_FOUND
          # and is effectively the default case
      case _:
          # this never gets triggered
          print("There")
I got a meaningful error:

    File "<ipython-input-13-5264d8327114>", line 3
      case NOT_FOUND:
         ^
  SyntaxError: name capture 'NOT_FOUND' makes remaining patterns unreachable


That in itself is weird though. Why throw a syntax error there but not here?:

    def f(x):
      return 1
      y = x + 1
      return y


You’ve braver than me, I had the same thoughts but didn’t dare post in here.

Not entirely surprised by the negative responses and I can see there are gotchas here, which is always thorny. But everyone seems to be picking simple uninteresting use cases so they can knock then down.

This feature would seem to be for cases where you need to do more complex matching.

In terms of the variable assignment, surely the whole point of this feature is to give you nice assignment based on matching, rather than using it as an if or switch statements?


> Yeah, I agree that's a bit ugly.

It's not about ugliness, it's about clarity and robustness. It's about not ingraining landmines into the language, whether beautiful or ugly. This is just asking to be blown up in people's faces over and over again.


The behaviour my intuition would have expected is that if a variable is currently in scope, then it is considered as a literal match, otherwise it's a capture match.

I suppose there's an obvious flaw to that behaviour, though.


No, that's not robust. The idea was simple - to use explicit marker like:

case == FOO: case is True:

That was part of PEP642, but it ended up containing more other questionable things. I hope that particular point will be separated and implemented.


module-level dict?... why...


Uh...that's my point. Everyone new to Python has to eventually learn not to do that.


It's a simple rule: Default arguments are evaluated at function definition time (when the interpreter reaches the 'def' statement) not at the time the function is called.

It's not a "module-level dict", you can create functions inside other functions:

    def foo():
        def bar(a={}):
            ...
        return bar
Each instance of bar gets its own dict when you run foo() and the "def bar(...): ..." statement gets executed. The dict is packaged up inside the function object (sort of like a closure except that the dict and it's name aren't from the enclosing scope of the function, and of course you can "shadow" it by passing in an argument for the parameter. https://en.wikipedia.org/wiki/Closure_(computer_programming) )


I understand this simple rule, and I understand closures, of course, it's just using closure semantics for default arguments doesn't make any sense, aside from some optimization maybe, and pretty dangerous. Other approaches are possible, see ruby, for example, default arguments are evaluated basically as if they're already in the body of running function, every time, only when they're not passed, and with the same lexical scope as the function/method. And what, python retained this behavior even in 3.x? omg.


Ah, sorry for over-explaining.


The folk over at openAI do something similar in order to create a registry, so that you can initialize objects using a name.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: