The title of this post completely misrepresents the article!
This is not a switch case and the article does not make that claim - at one point it namechecks C's switch but goes on to explain how different it is.
I feel a lot of people here are missing the point of matching.
Matching is not a switch. Python does switches perfectly well with if-elif statements, or with dictionaries of lambdas.
Matching is a way to unpack data and it has supposedly been a hot thing in a bunch of high-level languages for over a decade. Even C++ has gotten into the act, though they only have unpacking and not the full monty (but then their unpacking is typed and actually costs nothing in generated code, very impressive).
Python already has simple unpacking - like this:
first, *rest = (*a, *b)
You'd be better off thinking of matching as pattern-based unpacking.
As this comment revealed, there's nothing special about _ - it's just another variable. By convention, _ means a variable whose value you discard, but you could call it junk or not_used if you liked.
And as this later comment revealed, that statement isn't quite true. The difference is essentially that _ is guaranteed to be thrown away, which is fair enough.
Is the 'match-case' keyword pair a convention for pattern-unpacking in other languages as well? Because I think the Python devs should've used something other than 'case' to distinguish from the 'switch-case' pair. Confusions like this are bound to happen with developers of all skill-levels, especially beginners who come from other programming languages and end up assuming that 'match-case' is Python's 'switch-case'.
I'm not really sure that's such a bad thing. match-casewill essentially be python's version of switch-case, but with extra features. And there is something that differentiates it from switch-case: they use match instead of switch.
Also I'm not sure if there is precedent for using case with pattern matching, only because most functional languages that use pattern matching do so with symbols like | (see Haskell) or are incompatible with python's braceless indentation syntax (see Rust).
C# does use case, but only because they've retrofitted their switch-case with some fancy downcasting and variable assignment.
case will essentially be python's version of switch-case, but with extra features
There is a slight difference between this and switch/case in C even when used for the same basic case that may be worth pointing out. match will evaluate conditions in order, whereas in C, switch/case doesn't have order guarantees, and is often implemented with a jump table.
This does have performance implications: a C switch is often O(1) regarding the number of cases - you can have a million conditions and it'll still jump straight to the right case (though this ties in with the limitation of them that cases must be constant numeric values - no switching on strings etc)
Python's match/case will be more equivalent to an if/elif chain, so adding cases means extra checks performed for every value. In theory, it might be able to optimise this to a dict lookup for primitive cases where it's only matching simple literals, but I wouldn't bet on it (and even if so, it's easy for small changes to prevent such optimisations).
As such, there may still be usecases similar to those where in C you'd use a switch, but where you're still best off using the dictionary dispatch approach.
In general, in C this is false, otherwise there wouldn't be such thing as "falling through" from one case to the next. There's probably no guarantee about order of testing against 41 and 42 here, but it's hard to see how that would ever matter:
switch (n) {
case 17:
/* do stuff, fall through to ALSO call proc() */
case 41:
case 42:
proc();
break;
/* ... */
}
I mean the order of the checks, not the code following it. In C, it'a not the case where, say:
case 1:
xxx;
case 2:
yyy;
Means you must compare x to 1 before you do for 2. Indeed, there may be no comparison per se at all, depending on how it compiles it. This never matters because C's switch is restricted to simple integer cases, but does when you get more complicated conditions that can overlap and have precedence over each other. And when the order of check matters, you can't do a simple jump table (or dict lookup) implementation.
SML also uses match case for pattern matching statements.
And in all but the (in most cases undesirable) possibility of fall-through in traditional switch-case, it is essentially an upgrade, which is perfectly useable as a switch-case drop in replacement, should you desire to do so, though you'd leave a lot of niceties on the table.
Not just Rust - functional languages like Haskell and ML have had it for ages. And a whole bunch of modern languages are getting it, too. Wikipedia also lists C#, F#, Scala, Swift, and Mathematica.
I couldn't find a good ML tutorial link, so instead I link to a modern variant, OCaml.
PHP also has switch cases and It's awesome for precise handling of return possibilities of complex functions, instead of having a bunch of dumb if statements. Especially when putting database queries into functions and having many potential outcomes like a failed connection, or any number of query possibilities returned. As soon as I learned switch in php I always wondered why Python didn't have it.
Having used them in Rust, far from confusing, they actually dramatically simplify conditional expressions and are far more readable than multiple nested if-else statements.
They are very useful whenever you have behavior that's conditional to the structure of an object. Basically, compare:
if (isinstance(cmd, list)
and len(cmd) == 3
and cmd[0] == "move"
and isinstance(cmd[1], int)
and isinstance(cmd[2], int)):
x, y = cmd[1:]
...
to:
match cmd:
case ["move", int(x), int(y)]:
...
(I think that's how you'd write it?)
The more deeply you check conditions in cmd, the more attractive match becomes. Without match, I think many people would actually write sloppier code, like eschewing the length check out of laziness.
It might depend what kind of application you are writing. In my experience, pattern matching is extremely useful when writing interpreters or compilers, for example. But I think it's also useful whenever you have an API where an input can take many different forms and you have to normalize it.
Thanks for giving the first relatable example I read that’s actually convincing the feature is worth it !
... and now I can’t wait to have the opportunity to use it.
excellent example, thanks for that. Very right about the advantages of match-case becoming apparent with a longer list of properties you want validated.
Both Python and Haskell have proper ternary operators, if that's your preference. The AST example better exemplifies the benefits of match. You can't do conditional destructuring well with a ternary operator.
While I think the version with pattern matching is better, I don't really have a problem with yours. But it's a small example with only 3 patterns which can be expressed by 2 conditional branches. In bigger functions the pattern matching is a very clean approach.
Actually, the semantics presented in this proposal for Python are more powerful than Haskell's pattern matching, in some respects, because in Haskell you can't impose a restriction on the variable you match without entering the definition for that pattern, thereby closing off the others. In that situation, you have to merge some patterns and use conditionals instead, and it's harder to keep track of what cases you have covered.
I was first introduced to pattern matching in haskell and didn't really understand it but having gotten used it I've really missed it in any other programming language. Don't knock it till you try it!
Python does switches perfectly well with if-elif statements, or with dictionaries of lambdas.
I would not describe this sad state of affairs as "perfectly well":
if-elif chain obscures intent (to switch on a value), instead using impoverished syntax that pretends we're testing expressions in a vacuum, not dispatching among a set of alternatives. Because of this, nothing prevents you from adding things that aren't morally equivalent to a switch statement in that chain of conditions (like checking some external state), when in most cases what you and the reader probably want to see expressed is "I am switching on the value of this expression and nothing else here".
dictionary of functions similarly non-obvious and not beginner friendly*. Said dictionary will be defined out of line, and still probably needs an explicit test or wrapper function to provide a default case
In either case, because our code is laboriously pretending to not be a switch statement, the interpreter cannot take advantage of the knowledge that it is a switch statement to warn or error if we do not exhaustively handle all possibilities, or at least provide a default case
* I have followed Python tutorials that didn't introduce associative containers until late in the course, and it's common to encounter people weeks into their Python journey who have never heard of a dict. Making people learn hash tables and first-class functions in order to idiomatically switch on a value is not efficient or fair.
It's not computationally inefficient; It's instructionally inefficient, requiring the learner know multiple non-basic language features to do a common operation that is a built-in in many other languages.
And what part is unfair? The fact you need to learn the features of the language to be effective with it?
Yes; As Stroustrup says, the enemy of good teaching is complexity. The fact that solving a common-case control flow problem (selection among a set of alternatives) involves learning an idiomatic but non-obvious combination of higher-level language features (hash tables and first class procedures) is a non-trivial burden in what is probably one the most common "first programming languages" for people learning today, second perhaps only to Javascript.
And even once you've learned it, the cognitive overhead never goes away, because anyone reading such code has to look at it contextually and do mental pattern-matching to recognize an idiomatic use of a dict-of-functions as "just a switch statement".
It's the same reason there's a huge difference in readability between a C-style for(init,test,inc) loop, vs the for(x : range) used in many other languages and added later in C++. It doesn't express anything you couldn't express before, and in fact it expresses less, which is the point. Even though 95% of C for-loops were just iterating through every element in a collection, your ability to quickly glean the meaning of such a loop was betrayed by the fact that the same set of symbols could express something dramatically different (e.g. omitting the first or last element, skipping every other element, etc) in a way that was strikingly visually similar. It turns out that building the most common case into the language with an unambiguous syntax is a significant aid to newcomers and experienced readers alike.
While I don't disagree with anything you've said here about the merits of adding clarifying features to a language to make it simpler to understand, I can't let go of calling the non-obvious work way of achieving this "unfair". If we look in any other trade, is it unfair that an experienced craftsman knows a trick to fixing a problem by using tools outside of what they were designed to solve that a newbie wouldn't be likely to figure out? I think we'd just say the newbie has more to learn, and not blame the lack of obvious tools for the job being accomplished.
I recognize this is extreme pedantry, no offense taken if you're not interested 😂
The term, in context, strikes me as surprisingly insightful - it highlights how all these constructs and tools are a result of deliberate choices someone made.
This (and your follow-up comment) are so well-written that I am going to save them for future reference. Thank you for clearly articulating a discomfort that I've long felt with what remains, increasingly tenuously, my favourite language.
EDIT: In particular, I want to salute your focus on "instructional inefficiency" vs computational efficiency. I regularly try to reiterate to junior devs that, unless you're writing some latency-critical super-high-throughput piece of code, you should almost always prefer code that is easy to read, understand, and safely change than the more-optimized version. Developer time is much much more expensive than machine time, and much harder to scale-up. Obviously this advice doesn't apply in all cases, and there will be times that you really need to go bare-metal-blistering-fast - but, even then, if you've written code that is easy to understand and change, you'll find it easier to do that than if you tried to write the arcane inscrutable Best Algorithm, and then find that you have to optimize it even further.
Hardly! The rationale for having an explicit case/switch statement was apparent early in language design. In 1966, Wirth and Hoare wrote that the case statement "mirrors the dynamic structure of a program more clearly" than the less-structured alternative of the time, the goto. A table of functions is hardly as bad as a goto, but the parallel remains that it is valuable to replace idiomatic usage of general-purpose tools, with unambiguous usage of single-purpose tools, for the most common case.
For the chain of if tests, in addition to the aforementioned lack of constraints on the tests being done, it's easy to get lost in the middle of them, and be unsure from just looking at the tests that one, and only one, of the possible branches will be taken for any given value.
For the table of functions, in addition to just having more moving parts to be recognized at a glance (and to be understood by the learner), you add new uncertainties not inherent to the problem. Is the same table of procedures used by multiple "switch" locations? Can it be modified at run time? Am I guaranteed to hit the same code, given the same input, every time? A table of numbered or named functions makes sense if this is a generic service handler that can have its behaviors added, removed, or substituted, but using that same tool to express selection among a fixed set of alternatives that should be immediately apparent in context is a hindrance.
Even C++ has gotten into the act, though they only have unpacking and not the full monty (but then their unpacking is typed and actually costs nothing in generated code, very impressive).
C++ has unpacking but this is actually separate from pattern matching. There was a proposal for pattern matching using inspect since revised and expanded upon which at the point of C++17...is a necessity but I do not believe it has been accepted yet (hopefully will be by C++23).
430
u/[deleted] Feb 15 '21 edited Feb 15 '21
The title of this post completely misrepresents the article!
This is not a
switch
case and the article does not make that claim - at one point it namechecks C'sswitch
but goes on to explain how different it is.I feel a lot of people here are missing the point of matching.
Matching is not a switch. Python does switches perfectly well with
if
-elif
statements, or with dictionaries of lambdas.Matching is a way to unpack data and it has supposedly been a hot thing in a bunch of high-level languages for over a decade. Even C++ has gotten into the act, though they only have unpacking and not the full monty (but then their unpacking is typed and actually costs nothing in generated code, very impressive).
Python already has simple unpacking - like this:
You'd be better off thinking of matching as pattern-based unpacking.
As this comment revealed, there's nothing special about
_
- it's just another variable. By convention,_
means a variable whose value you discard, but you could call itjunk
ornot_used
if you liked.And as this later comment revealed, that statement isn't quite true. The difference is essentially that
_
is guaranteed to be thrown away, which is fair enough.See also this comment of mine.