In Rust we know we only gave the append function an immutable reference which lives until the next iteration. If append is OK with that, we're golden. Maybe internally it clones these references -shrug.
If append() needs the thing, not just an immutable reference which expires soon, its signature would demand we move one into it, and we don't have one so we'd need to e.g. make one with Clone.
TBF Rust also intentionally used the semantics Go is migrating to from the start, because:
- experience with the issue in languages with wider scoping e.g. it’s a common issue in JS, as well as Python (though slightly less so)
- Rust’s iterators were originally internal so that was pretty natural
(1) is also why for(let and for(const have different scoping than for(var in JS: `var` has function scoping, `let` and `const` were introduced with block scoping, and for loops they were specifically specced with “inner” (per-iteration) scope.
If you take a Rust for loop, de-sugar it to produce the actual loop { } which would run, and then modify that loop to have the Go semantics - with a single long-lived variable which is re-assigned each iteration, Rust detects the faulty Go cases of course.
[Edited: I tried to explain what's going on here, but I don't think my explanation was helpful so I've just left the surface]
Yes Rust’s ownership rules make it rather complicated to reproduce the faulty behaviour, as it’s about sharing mutable state which Rust intensely dislikes. You’d need to wilfully share (and update) internally mutable structures (cells, atomics) which is pretty noticeable and not something you do by mistake.
Aside from Python and the Shell, it never occurred to me that any language could possibly think of other semantics. It's news to me that C++ also assigns rather than initializes on each iteration.
It's simply a very bad idea that provides no use yet creates many bugs.
The thing is it only create many bugs when you throw closures into the mix. So historically languages did it because it was easy to implement (create a counter, increment the counter, run the loop body).
Before the early aughts, closures were mostly really common in functional languages which tend towards immutable bindings (and immutability in general), and very closure-focused languages closured everything so didn’t hit that issue (e.g. you wouldn’t hit it in Smalltalk because your counter would be a parameter to a block, so closing over that was no issue).
It’s really in the 00s with the explosion of callbacks-pile-javascript (and more generally the functionalisation of imperative languages) that the problem became a serious concern: you loop over a thing, you start some sort of async operation (network request for instance), and you find out that despite the request being correct the entire thing goes wonky (then again things commonly went wonky which didn’t help).
> The thing is it only create many bugs when you throw closures into the mix. So historically languages did it because it was easy to implement (create a counter, increment the counter, run the loop body).
It's a problem with references in general as this shows.
I also don't feel it's easier to implement at all.
One can either rewrite:
for $id:var in $exp:iter { $code:body }
to:
{ let $id:var;
while(True) {
let result = $exp:iter.next();
if(result.is_none()) break;
$id:Var <- result.extract();
$code:body
}
}
Or
while(True) {
let result = $exp:iter.next();
if(result.is_none()) break;
let $id:var = result.extract();
$code:body
}
The latter implementation is as far as I see easier, not more complex. Obviously all the code to create scoping already exists in the compiler and for-loops over an iterator work with a syntactic rewrite to an infinite loop with a break.
> Most languages don't have references, and in those that do before the issue was understood, the explicitness made it a much smaller issue.
But Go and C++ do, where this issue arose with or without closures.
> Now try lowering to bytecode or assembly instead of high-level pseudocode.
It doesn't matter, because as I said, all that is already in the compiler.
It would be needlessly complex and error-prone for compilers to hardcode custom code generation for such abstractions; it's transformed to something else the compiler already understands at a far higher level. I know for a fact that in Rust, for-loops already desugar to a simple infinite loop construct with a break at the H.I.R. level and all further optimizations only happen from there.
Which makes it more confusing why it was originally as it was because it's really not harder to implement as any compiler implements it as desugaring before optimizations een occur and this form is simpler.
The only explanation I see is that they really gave it no thought at all whatsoever and it wasn't a tradeoff but simply not thinking clearly.
Maybe that's my C/C++/Rust experience.