8 ms·
The semantics of when stuff is copied, moved, or passed by reference are all over the place in language design. C started with the idea that functions returned
by Animats 2y ago
The semantics of when stuff is copied, moved, or passed by reference are all over the place in language design.
C started with the idea that functions returned one int-sized value in a register. This led to classic bugs where the function returns a pointer to a local value. Compilers now usually catch this. C eventually got structure return by copy. Then C++ added return value by move, and automatic optimization for that. It's complicated.[1]
Most hard-compiled languages only let you return values of fixed length, because the caller has to allocate space. Dynamic languages where most things are boxed just return the box.
Rust makes you declare boxed types explicitly. Vec and String are already boxed, which handles the common cases.
More dynamic languages tend to let you return anything, although there can be questions over whether you have your own mutable copy, a copy-on-write copy, a read-only copy, or a mutable reference to the original. That's what got the OP here, at
thing := findThing(things, "record")
thing.Name = "gramaphone"
They thought they had a mutable reference to the original, but they had a mutable copy.
There's a good argument for immutability by default, but many programmers dislike all the extra declarations required.
[1] https://stackoverflow.com/questions/17473753/c11-return-value-optimization-or-move https://stackoverflow.com/questions/17473753/c11-return-valu...
- mariozski 2y agoActually Go is always pass by value. Even when you pass the pointer you’ll get a copy of it.
- the_gipsy 2y agoWhat matters is that a function can modify the value pointed at, without necessarily returning it.
- binary132 2y agoThat is no different than C or C++. A pointer is just another type of value.
- everybodyknows 2y ago> Actually Go is always pass by value. False, this depends upon the underlying type. The Go 'map' type is always pass (and assign) by reference. https://go.dev/play/p/ovfuNBNtiza https://go.dev/play/p/ovfuNBNtiza
- leg100 2y agoNo, it's definitely passed by value: https://go.dev/play/p/mIehOwUWz95 https://go.dev/play/p/mIehOwUWz95 A map is implicitly a pointer to the underlying map structure. When you pass a map, you're passing a copy of a pointer.
- B-Con 2y agoTechnically no, but I get why it feels like this. :-) I think the confusion stems from the fact that Go hides the distinction of "dot" access vs "pointer dot" access. (eg, the difference between `foo.Bar` and `foo->Bar` in C++). Go knows if an object is a pointer and needs to be de-referenced, and hides the de-reference operation from you syntactically. So you can use a pointer without knowing it's a pointer. This is the confusion: You never have a "map object that is passed by reference", you instead have a "pointer to a map object that is always passed by value". This is less confusing for structs the programmer defines themselves, since they can check the type of "foo" see if "foo.Boo" is accessing Bar directly or de-referencing "foo" and then accessing "Bar". But it's more confusing when applied to builtin type for which you never see the implementation. How would you know if `map[string]bool{}` returns a value or a pointer? You don't! The map type is passed by value but its implementation is that it is a thin wrapper around a pointer to a struct. Ergo in practice it feels like passing by reference. But Go is technically always pass by value. Map does not get any special implementation in the language. You can do this yourself. `type MyStruct struct{ data *MyType }`. Now if you pass MyStruct by value, any operations on it are effectively by reference since all operations have to dereference `data` to get at the actual content. (Same goes for strings, except they are immutable so no one notices.) This might feel like semantics, but it's important to remember that Go doesn't treat any of those built-ins with special "pass by reference" rules. Instead, it's behaving consistently like it would for any types you defined yourself and one of the learning curves of the language is to think in Go's terms of pointers vs non-pointers and learn which native types are pointers. If you think of it that way, learning Go's built-ins is no different than learning the API methods for a custom struct Go's built-ins just happen to be, well, built-in and thus get syntactic sugar that your custom structs do not. "Pass by reference" is usually used to refer to the case where you use a value at level N of the call stack, but the code at level N+1 of the callstack decides to make it a pointer. In Go this never happens, instead you technically always had a pointer the whole time!
- zbentley 2y ago> This led to classic bugs where the function returns a pointer to a local value. Compilers now usually catch this. C eventually got structure return by copy. Then C++ added return value by move, and automatic optimization for that. Jesus Christ. Can we now get “return fruit by vegetable?” Some speedups may not be worth having to memorize standards documents.
- Aeglaecia 2y agoprogrammers who get the job done without needing to memorise standards or dig into hardware specifics are surely happy enough with general optimisations applied by the compiler like tail call optimisation or copy elision ...
- binary132 2y agonot complicated stuff to understand, and doesn’t even need to be understood to use correctly. Just don’t return pointers to locals.
- beltsazar 2y ago> There's a good argument for immutability by default, but many programmers dislike all the extra declarations required. That's one little reason why Rust is loved by many: immutability by default. Meanwhile, it's not even possible in Go to declare immutable variables!
- Cthulhu_ 2y agoI hope it gets added, it adds so much safety to the language. Mind you, Go is not a very safe language in general, its type system is pretty loose compared to e.g. Java or Typescript. I don't believe it wants to be though.
- tapirl 2y agoIt is possible to add more immutability in Go. There are many proposals for this: https://github.com/go101/go101/wiki/Go-immutable-value-proposal-list https://github.com/go101/go101/wiki/Go-immutable-value-propo.... The main reason nothing happened in this direction is the core team think it is not important enough.
- Animats 2y agoIt's tough to retrofit new restrictions to old code. C/c++ code still doesn't have "const" everywhere it could. You have to make the default immutable - write "var" or "mut", not "const" - or it doesn't get used everywhere it should. But that breaks old code.
- kiitos 2y agoIt's very clear that the reason these kinds of proposals haven't been accepted has nothing to do with the core team not believing they're important enough, but instead because of the impact that they have on the rest of the language.
- tapirl 2y agoWhat impact in your opinion?
- pram 2y agoThe one thing I’ve personally wanted in Go is something like final in Java, for structs.
- kzrdude 2y agoWell, that's another nice thing with Rust. Maybe it's not the saviour language but at least it brings clarity to ownership of values.
- foldr 2y agoI think the same thing could happen in Rust. foo.x = bar will compile if foo is a copy or if it's a mutable reference. As in Go, you'd have to explicitly type 'foo' in order for any compiler error to show up.
- tialaramex 2y agoLet's work through the example 1. There's a user-defined type named Thing with a String inside it. 2. We've got a function named findThing, which takes an array of Things and a string name, performs a linear search of the array and returns a mutable reference to the Thing with that name or if it's not found it doesn't. In Go, this function accidentally gives back a mutable reference to a copy of the Thing. 3. We make an array of things, use findThing to find one of them and try to mutate it. We can translate 1 into Rust easily, it's just struct Thing { Name: String } and the compiler will moan because this is deeply unconventional naming in Rust and that warns by default. We can translate 3 into Rust easily too, the compiler moans about our naming again. But when we try to translate 2 by writing findThing we struggle. Rust wants to understand what lifetime to associate with this mutable reference we're returning. If we try to make a copy, where does the copy live? Rust doesn't have garbage collection, so if it just goes out of scope the lifetime ends and Rust will reject this function as nonsense - you can't return references which have expired, silly programmer, try again. If we don't make a copy we can successfuly tie the lifetime to the array, but then we don't have the Go bug, so the thing you say "could happen in Rust" doesn't happen. Here's a Godbolt link for a working example: https://rust.godbolt.org/z/shKj6rEz7 https://rust.godbolt.org/z/shKj6rEz7 Now, try to adjust it to have the same bug, I expect it will be much harder to do this wrong.
- deleted 2y ago[deleted]
- deleted 2y ago[deleted]
- mmaniac 2y agoThe killer IMO is the combination of mutability and implicit references. With either immutability or explicit references, the hazards are greatly lessened. In the absence of mutability, there's no observable difference between passing a reference and passing a value. With explicit references, there's always an indication when objects are liable to nonlocally mutate. Go and C are quite wise to use pass by value and opt-in references. It avoids the programmer's burden of immutability by default while making it more obvious* when to be aware of potential mutation. A programming style incorporating local mutation also comes very naturally, which is simple, practical, and remains easy to reason about. *Go and Rust would be wiser if they retained -> from C so that this obviousness would remain without having to check type declarations which may even have been inferred. Python gets hit the worst by this combination. On top of having mutable references everywhere, it also doesn't make a big deal about object identity (unlike e.g. Java) and has a lot of declarative constructs. Which means you get surprising stuff like this. >>> foo = [[]] * 10 >>> foo[0].append("bar") >>> foo [['bar'], ['bar'], ['bar'], ['bar'], ['bar'], ['bar'], ['bar'], ['bar'], ['bar'], ['bar']] >>> def baz(arg = []): ... print(arg) ... arg.append("qux") ... >>> baz() [] >>> baz() ['qux']