Incredibly insightful comment, even the performance concern.
Apologies for doing a glorified +1 comment, but if you take the above comment plus the article's in-loop mutation algorithm and try it in different languages you get insights on who you are as a coder and what are the language's strengths.
Another detail is that the programmer in the article prefers an mutation iterator format while the language forces him/her to use an array/index. This has further impact on pointer vs value accesses.
Also by mapping on players list the above post means: just discard the previous one, create a new one however you want and make the new one the new source of truth. No need for mutability considerations.
> just discard the previous one, create a new one however you want
I want to create it by re-using the memory of the previous one that I've just discarded, please (well, the one that will end up discarded, actually: it has to live until the map ends executing, hasn't it). Unfortunately, most of the languages' implementations/runtimes don't employ the Perceus algorithm.
Can you describe why you need it? Is it because of real performance concerns or just because it feels better knowing that you don't waste memory space and CPU cycles?
If you really need it, you can always fall back to using mutables for this special list, and use immutables for the rest? This is probably the reason why languages don't implement the Perceus algorithm. If you really needed it you might as well do it by hand.
Notable is that you claim here is an opinion not an argument.
That immutability is not needed is also not an argument against it.
The point here is that working with immutables relieves you from some cognitive stress and repetitive work such as writing accessors and making fields private.
I always felt like it was kind of the other way around? Speaking from experience , most languages where immutability is the default and/or the only option can usually simulate mutability or have it as a stdlib-provided escape hatch (elixir and haskell come to mind), but most languages that are mutability-first really struggle to provide any sort of immutability that is reliable.
At a datastructure level, perhaps, although functional datastructures are pretty fringe. (cf. Okasaki)
In general, they can still be implemented by traditional imperative languages (obviously).
At the type/variable level, it's the opposite. We see it with const. Once it's in here, it's in there.
For reasons it’s never really bothered me, but the *other* loop bug does bite me some times. This is the one where the loop values are values.
The common mistakes wiki lists the capture bug twice, but doesn’t mention this bug.
Yeah, I guess you're right, the author didn't call it a bug...
I believe the author doesn't mean it is a bug in the general sense. The author just means it is a shortcoming, and this shortcoming is surely fixable. The author provides a solution,
but I think the zig way is better:
for _, *player := range players {
player.score = 0
}
Not to be the guy who brings Rust into the Go thread, but… Rust supports value semantics, and also supports `for player in &mut players { player.score = 0; }`.
Likewise in other languages with similar value semantics, but as we know those are PhD features only relevant for folks playing with language research.
I honestly don't think you can. If you try to iterate over values instead of references, you'll take ownership of the vector and not be able to use it again.
If the value is Copy, you will make a copy and won’t take ownership, right? (Iterate over a vector of ints). Granted Rust structs are not Copy by default.
Same with any C++ class for which there is no copy constructor.
> why would Go even allow you to mutate the temporary variable?
Because it's a variable. That's what you do with variables, you mutate them to hold different values. There are no "named, non-constant holders for values that you can't mutate directly" in Go.
Go favors concurrency safety and concurrency efficiency. Therefore it has to copy loop values to be able to parallelize them. With a reference this would be unsafe to do.
What are you talking about? Go doesn't parallelize its loops, in fact, concurrent mutating iteration is unsafe.
Reads of memory locations larger than a single machine word are encouraged but not required to meet the same semantics as word-sized memory locations, observing a single allowed write w. For performance reasons, implementations may instead treat larger operations as a set of individual machine-word-sized operations in an unspecified order. This means that races on multiword data structures can lead to inconsistent values not corresponding to a single write. When the values depend on the consistency of internal (pointer, length) or (pointer, type) pairs, as can be the case for interface values, maps, slices, and strings in most Go implementations, such races can in turn lead to arbitrary memory corruption.
The for-range in Go doesn't introduce any synchronization points.
27 comments
[ 3.5 ms ] story [ 64.1 ms ] threadWith immutables you simply map on players list and continue working with the copy.
Bugs avoided. Performance concerns - maybe they exist - but show me them first.
Apologies for doing a glorified +1 comment, but if you take the above comment plus the article's in-loop mutation algorithm and try it in different languages you get insights on who you are as a coder and what are the language's strengths.
Another detail is that the programmer in the article prefers an mutation iterator format while the language forces him/her to use an array/index. This has further impact on pointer vs value accesses.
Also by mapping on players list the above post means: just discard the previous one, create a new one however you want and make the new one the new source of truth. No need for mutability considerations.
I want to create it by re-using the memory of the previous one that I've just discarded, please (well, the one that will end up discarded, actually: it has to live until the map ends executing, hasn't it). Unfortunately, most of the languages' implementations/runtimes don't employ the Perceus algorithm.
If you really need it, you can always fall back to using mutables for this special list, and use immutables for the rest? This is probably the reason why languages don't implement the Perceus algorithm. If you really needed it you might as well do it by hand.
The real thing here is the definition of the Player type.
It should have a mutating method that resets it's score (a method defined on *Player).
Raw access on fields is often only ok for naive, trivial implementations. Methods are often needed anyway.
That's straightforward. Immutability doesn't make any sense here.
... Examples on how to avoid the bug ...
Notable is that you claim here is an opinion not an argument.
That immutability is not needed is also not an argument against it.
The point here is that working with immutables relieves you from some cognitive stress and repetitive work such as writing accessors and making fields private.
With mutability, it's still possible to make things immutable if properly encapsulated.
If everything is immutable by default and you need mutability for some reason, good luck.
The heuristic is simple too. If you need something to be mutable, you should deal with pointers. So *Player and []*Player here.
At the type/variable level, it's the opposite. We see it with const. Once it's in here, it's in there.
The Go code
has the exact same "bug."This isn't something that should be fixed by the language. Instead, a linter rule that detects that the write is never read would catch this.
The author's title literally refers to it as a bug.
(You can still write the same bug in Rust or C++ accidentally.)
I honestly don't think you can. If you try to iterate over values instead of references, you'll take ownership of the vector and not be able to use it again.
Same with any C++ class for which there is no copy constructor.
For example, in Nim (it also prefers value semantics) this would result in a compile error:
var players = [1, 2, 3]
for player in players:
Instead, you have to use the explicit `mitems` iterator, to get mutable references:var players = [1, 2, 3]
for player in players.mitems:
Because it's a variable. That's what you do with variables, you mutate them to hold different values. There are no "named, non-constant holders for values that you can't mutate directly" in Go.
Go favors concurrency safety and concurrency efficiency. Therefore it has to copy loop values to be able to parallelize them. With a reference this would be unsafe to do.
They don't yet parallelize loops, but the idea was there. Most others do it already
[0] https://github.com/golang/go/issues/24210#issuecomment-36979...