What utter nonsense.
There was extensive discussion, and it ran into the limitations of Python's semantics. Here's a related problem, a common mistake for new Python coders:
x = []
for a in [1, 2, 3]:
x.append(lambda: a)
print([f() for f in x])
It prints [3, 3, 3], of course, because people don't realize all the lambdas are closing over the same 'a'. And there's simply no way around this with the way Python's scoping works.Read PEP 3103. Every possible implementation was potentially badly misleading to users:
1. Building the dispatch at compile time would result in mysteriously immutable values. 2. Semantics would likely be different outside a function. 3. Or it would sometimes use dispatch and sometimes use if-else chains, leading to inconsistent performance. 4. Or they'd have to add completely new syntax for static data.
If they just made it syntactic sugar for an if-else chain, even that's misleading, because people will invariably be surprised to learn it's not doing a simple dispatch.
There's nothing religious going on here, Python is simply constrained by its original design. If various proposals to add new syntax for static data come to fruition, then a switch statement would make perfect sense and have clear semantics, and they will be glad they didn't try to kludge some half-baked crap together like they did with Enum.
Off the top of my head, something like a switch, but more general and a function rather than a statement:
def switch(key, opts_dict, *, default):
return opts_dict.get(key, default)()
or, if key is of a comparable but non-hashable type: def switch(key, opts_list, *, default):
return next(
(o[1] for o in opts_list if o[0] == key),
default
)()
I can think of other simple variations to do exactly what is wanted in other situations; it's easy enough to code a specific tool for the situation with what Python gives you that, in the situations where a chained set of if/else isn't what is called for, it's trivial to whip up something tailored to the specific need. C doesn't have dicts or iterators or first-class functions in the base language (at least, with convenient syntax), so it needs special syntax to cover a much smaller space than what Python conveniently handles without special syntax dedicated to the problem.As for the switch statement, I'm inclined to agree although if we see `switch` as syntactic sugar for a bunch of other conditional statements, then what makes it a more important candidate for inclusion than the walrus operator?
Thanks in advance, I'm genuinely curious to know.
Specifically on the case of Python, it used to promise simple semantics a long while ago. This changed at some point, and the semantics were already complex before the walrus operator. With the operator added, semantics becomes a bit more complex, but this is not a main feature of the language anymore, so it's not a big change.
Yet, pattern matching would have a very small semantic footprint, and would solve the same kinds of problems (despite having a very different nature), but was very vocally rejected without any of the deep analysis that went into the walrus operator. It looks like this happened because they would have a very large syntactic footprint, basically completely changing the appearance of the language, but the process left the people that focused on semantics lost, without understanding what was happening.
Because it isn't necessary? All you have to do is extend the syntax to now allow the one-and-only assignment operator to function in other places, for example, in an "if" or "for" statement and you are done. Anything else can be taken care of with parenthesis.
For more examples of how I don't see any applicable ambiguity, see my reply below to a different question:
https://news.ycombinator.com/item?id=21426133
I mean, to me this is like adding a wing to the back of a four door family car. It's useless complexity.
We want to reduce complexity, not increase it.
BTW, I read through the entire PEP [0] and still find no way to answer this basic question in favor of the walrus operator:
What's wrong with simply extending the functionality of the existing assignment operator?
I looked through the examples and mentally made the change. I can't see any need for the new operator. Case in point, this is one of the examples given in the PEP:
Current:
reductor = dispatch_table.get(cls)
if reductor:
rv = reductor(x)
else:
reductor = getattr(x, "__reduce_ex__", None)
if reductor:
rv = reductor(4)
else:
reductor = getattr(x, "__reduce__", None)
if reductor:
rv = reductor()
else:
raise Error(
"un(deep)copyable object of type %s" % cls)
Improved:
if reductor := dispatch_table.get(cls):
rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
rv = reductor(4)
elif reductor := getattr(x, "__reduce__", None):
rv = reductor()
else:
raise Error("un(deep)copyable object of type %s" % cls)
My version, after declaring that "=" can now be used this way:
if reductor = dispatch_table.get(cls):
rv = reductor(x)
elif reductor = getattr(x, "__reduce_ex__", None):
rv = reductor(4)
elif reductor = getattr(x, "__reduce__", None):
rv = reductor()
else:
raise Error("un(deep)copyable object of type %s" % cls)
Why do we absolutely need the walrus operator again?And yet I go back to the "Current" pre-walrus code and have to ask: What is wrong with it? It's clear, very clear. And, nothing whatsoever in this logic is changing at the microprocessor level because of the use of the walrus operator. Even with we do not extend "=" to be able to use it in extended form, there is nothing whatsoever wrong with the original code. This basic structure has been used for decades in myriad languages. Not sure why there's a need to reinvent a wheel for basically zero gain. Extend "=" so it works in a few places, sure, that might clean-up code, but walrus? C'mon.
if reductor = dispatch_table.get(cls):
if reductor == dispatch_table.get(cls):
A human being can only distinguish them through careful inspection. The walrus operator not only prevents that problem, but makes the intent unambiguous.> Not sure why there's a need to reinvent a wheel for basically zero gain.
What's being reinvented? No one claimed this was an original idea. It's just inline assignment with a syntactic tweak to prevent a well known problem.
reductor = dispatch_table.get(cls)
if reductor:
rv = reductor(x)
versus: if reductor := dispatch_table.get(cls):
rv = reductor(x)
versus (the case of extending "="): if reductor = dispatch_table.get(cls):
rv = reductor(x)
Now let's look at rough machine-level pseudo code.In the first case:
- Load address of "cls" into appropriate register
- Jump to address where "dispatch_table.get()" code is located
- Upon return, the result, a memory address (pointer) is stored in a register
- Load the memory address (pointer) to where the "reductor" variable's data is located
- Store the address that came back from "dispatch_table.get()" at that location
- Compare this and jump if null
- If not null we execute the code that follows
Great. What would the pseudocode for the second case (walrus) look like: - Load address of "cls" into appropriate register
- Jump to address where "dispatch_table.get()" code is located
- Upon return, the result, a memory address (pointer) is stored in a register
- Load the memory address (pointer) to where the "reductor" variable's data is located
- Store the address that came back from "dispatch_table.get()" at that location
- Compare this and jump if null
- If not null we execute the code that follows
Hmmm, I see a pattern here. What would it look like if we extended the functionality of "=" instead? - Load address of "cls" into appropriate register
- Jump to address where "dispatch_table.get()" code is located
- Upon return, the result, a memory address (pointer) is stored in a register
- Load the memory address (pointer) to where the "reductor" variable's data is located
- Store the address that came back from "dispatch_table.get()" at that location
- Compare this and jump if null
- If not null we execute the code that follows
OK. Well, there's the point. If the code has to do exactly the same thing it will ultimately end-up executing exactly the same code on the processor. If it executes any more code than the old approach it will not, by definition, add any functionality, therefore, it is a waste of time. If it does exactly the same thing we should then question why it is necessary in the first place.Now I use python and love how it is better at separating the expression from the python language (with perl you were never sure if you had escaped things correctly)
but pyython is just... clumsy
m = re.match(r'xyz',str)
if m:
do a
else:
m = re.match(r'wxy',str)
if m:
do b
else:
m = re.match(r'abc', str)
if m:
do c:
else:
...
vs: if m := re.match(r'xyz',str):
do a
elif m := re.match(r'wxy',str)
do b
elif m := re.match(r'abc',str)
do c
...Modern software engineers have very little connection to low level code; what actually happens at the processor level.
If you are even in a position to interview someone for a software dev position, try this:
- Have them write a small program do to something simple; find a sub-string in a long string
- Now have them take their program and convert it to pseudo assembly language, explain what goes on in memory, where, how, etc. In other words, it doesn't have to be real assembly from a real processor, yet it has to resemble how things actually work.
From my experience most applicants flunk this test. They have no clue whatsoever.
And so, we produce tools, languages, libraries, and all manner of "pattern matching in modern high-level languages", they use them without a single clue and the resulting code is bloated and slow, to say the least.
Many years ago I wrote a genetic solver as a part of an iOS app that ran on an, at the time, iPhone 3S. It was an absolute dog. Unusable. You simply could not get results anywhere close to real time. An iPhone 3S, in the context of what the code entailed, is a massively fast computing engine. There was only one reason for which this code ran like crap: Objective-C and all the truly unnecessary "modern high-level language" garbage that came with it.
I re-coded the genetic solver in C++ with great care to manage the entire process efficiently. It ran THOUSANDS of times faster. In fact, it ran in real time (in the context of what the UI had to do) to the point where there was no lag and the user could interact with it as needed.
A lot of this stuff comes at great cost. The problem is modern programmers don't have a clue.
In addition, even when people think they know what is going on under the hood, most people are taught how things worked about circa 1992 at best.
if (x = 3):
Intended, or not?When your assignment operator is "←" you are never going to confuse assignment for comparison.
Assignment:
a ← 23
Comparison:
a == 23
Extending this to something like Python: # Assignment
if x ← 3:
do_something()
# Comparison
if x == 3:
do_something()
In fact, you could argue that once you have a dedicated assignment operator you no longer need the "==" construct, because "=" is enough, which is precisely how it works in APL. The above examples the, would become: # Assignment
if x ← 3: # x is assigned 3 and then evaluated
do_something()
# Comparison
if x = 3: # Is x equal to 3? It even reads exactly as it is meant
do_something()
So, if we want to reduce error and this kind of ambiguity the solution isn't to now have two different assignment operators using easy to mistype characters but rather to have distinct symbols that cannot be confused. Hence the genius of APL, where the notation is, as Iverson put it, a tool for though and the abomination of J, where notation was tossed out the window in favor of ASCII soup.The ambiguity could be resolved easily by adopting notation that makes sense. Why do we use "A*B" for multiplication when we can use a proper multiplication symbol "A×B"? Or division? "A÷B" Not to mention the logical comparisons; Or:"A∨B", And: "A∧B" , Nor: "A⍱B", Nand: "A⍲B" (these are real symbols used in Logic, BTW, as well as APL).
It was to avoid that ambiguity (or error prone pattern, if you prefer) which is present in, say, C.
More accurately, in Python it would be:
if x = 3: # x is assigned 3 and then tested
do_something_here()
This is different from: if x == 3: # x is compared to 3
do_something_here()
Very clear difference between the two statements.What if we want to assign and then compare?
if (x = 3) == y:
do_something_here()
or... if x = 3; x == y: #Python evaluates left to right...
do_something_here()
In fact, you could... if x = 3; x == y; x = x + 1: #Python evaluates left to right...
do_something_here()
Not super elegant, but it makes sense. I wouldn't want to write code like that in any language unless it was very well justified.What if we want to compare and then assign?
if x == y; x = 3: #Python evaluates left to right...
do_something_here()
Side note: Why, oh why, don't we have pre and post increment/decrement (++/--) in Python? Another religious decision.I guess I am having trouble seeing where this ambiguity might be. What is ambiguous about usage that is currently not legal and is later defined as a new way to make assignments?
In other words, nobody is using it that way today because it doesn't work. Tomorrow we say: From now on, you can make assignments inside of several conditional statements.
What's ambiguous about that?
This isn't foreign at all, is it? I mean, we have been able to do this in C for decades:
int i;
for(i=1; i<=3; i++)
{
printf("%d\n", i);
}
In fact, you can do this: int i;
for(i=1; i<=3; i=i+1)
{
printf("%d\n", i);
}
That's TWO assignments, not just one. No special operator or syntax. Last I checked no airplanes have crashed, MRI machines stopped working and nobody died because of this. Forgive me but, I just don't see the ambiguity at all. Once you define the extension of functionality the rest is not a problem.That said, I am eager to learn. I've only been designing hardware and software since about 1982 or so and have programmed in nearly every mainstream language in existence on platforms ranging from embedded to Silicon Graphics supercomputers and even the web. It is quite possible I am confused or just don't understand something. I do have a very pragmatic view of this stuff. By which I mean to say: Simple is usually better and there has to be a very, very good reason to reinvent any wheel.
There are many theoretical and quasi-religious arguments you can make about this point, but I think the real reason is a lot simpler: in practice, allowing assignment during expression evaluation has led to tons and tons of bugs.
The number of times a programmer accidentally uses a single "=" sign instead of a "==" in a conditional statement compared to any efficiency gains of avoiding an extra line of code, has empirically resulted in a net negative in productivity.
This isn't a theoretical or philosophical point, it's one rooted in collective experience. This "collective" experience may not match your own, but honestly, for experienced developers it's such a minor issue, that I'm fine giving up 0.001% of my productivity if it allows less experienced programmers to gain more than that.
Yet, at a more fundamental level, there's nothing wrong with code like this (taken from the PEP):
reductor = dispatch_table.get(cls)
if reductor:
rv = reductor(x)
else:
reductor = getattr(x, "__reduce_ex__", None)
if reductor:
rv = reductor(4)
else:
reductor = getattr(x, "__reduce__", None)
if reductor:
rv = reductor()
else:
raise Error(
"un(deep)copyable object of type %s" % cls)
What is wrong with this code that desperately needed fixing through the introduction of a new operator?There's something in the PEP about programmers wanting to save lines of code. Really? The code at the microprocessor level is the same. Heck, if lines of code were so important I would still be programming in APL, where I could shrink a hundred line C program into just a few characters on a single line of code.
I guess my problem is with the idea of fixing something that isn't broken while ignoring stuff that is just nonsensical. A simple example of this is the lack of pre and post increment/decrement (++/--), frankly, it's just silly. We are writing "x = x + 1" instead. The real irony is that every microprocessor I know has increment and decrement instructions! I mean, it's hilarious! "x = x + 1" is likely to be compiled to such an increment instruction, which, is the machine language equivalent of "++" (in rough strokes). One could not make this stuff up. Who are we fooling?
Your belief conflicts with Swift, which also disallows assignment in if conditions for this reason [0]. With GCC, that warns about "suggest parentheses around assignment used as truth value" when compiling with `-Wall` [1]. With the most popular Javascript linter [2].
The tricky part is that "that code isn't going to do what they wanted" isn't always immediately obvious. For example, when I google'd "comparison assignment typo", this is one of the first results: https://gitlab.freedesktop.org/wayland/weston/commit/209e8f1... If the author was expecting that test to pass with the ==, then = would introduce a subtle but devastating bug. In the future, someone could break that function but it would never get caught by the unit tests.
[0] https://docs.swift.org/swift-book/LanguageGuide/BasicOperato...
No, they don't want to save lines of code, they want to save time to understand code. Disallowing assignments during expression is one way to advance that.
But I'm not really understanding your bigger point here. Who cares about such minutia? Is "x++" versus "x += 1" versus "x = x + 1" really all that different to the programmer? When have you proudly used the shorter versions and actually made code better in some marketable way?
These programmer purity metrics are silly and counter-productive, we should instead focus on programmer quality and speed to get things done.
The other part I don't get is the religious insistence against not implementing something like a "switch" statement. This is one of the most useful constructs in all of computer science and Python rejects it due to what amounts to religious belief. In fact, one of the most powerful tools you get when moving from assembler to C is the "switch()" statement. If you've ever coded and maintained the equivalent in assembler you know exactly what I am talking about.
Because, you know, a pile of "if" statements is somehow better. Ah, OK, with "if" you can use data types other than integer. Sure, because we couldn't define a Python "switch" to work with anything other than integers...like a "switch" that could take a dictionary and route based on key/value matching, etc.
Every single state machine I have written in who knows how many decades --in C, C++, Objective-C, PHP, Verilog, etc.-- has had, at it's core, a single or multi-layer "switch" statement. Can't do that in Python, 'cause.
<rant off>