Live data from Hacker News

Go(lang): Robust generic functions on slices

go.dev

11–20 of 85 posts

Re: Go(lang): Robust generic functions on slices

#11

Earlier quoted context omitted.

None of these functions would apply to an immutable slice, so how is it related?

If immutable and mutable slices are differently typed [2], it is natural to define two functions (say, `slices.Compact` vs. `slices.Compacted`) to handle each type, like Python `list.sort` vs. `sorted`. It should be natural to expect `slices.Compacted` to never alter its input, and any attempt to use a mutable version will be very explicit except for slicing [1]. [1] Especially given that the capacity is preserved by…

This sounds awful in practice having to memorize different functions for the same thing based on mutability of the thing.

Re: Go(lang): Robust generic functions on slices

#12
post #11

Earlier quoted context omitted.

If immutable and mutable slices are differently typed [2], it is natural to define two functions (say, `slices.Compact` vs. `slices.Compacted`) to handle each type, like Python `list.sort` vs. `sorted`. It should be natural to expect `slices.Compacted` to never alter its input, and any attempt to use a mutable version will be very explicit except for slicing [1]. [1] Especially given that the capacity is preserved by…

This sounds awful in practice having to memorize different functions for the same thing based on mutability of the thing.

In practice it's not that bad

Re: Go(lang): Robust generic functions on slices

#13

Earlier quoted context omitted.

None of these functions would apply to an immutable slice, so how is it related?

If immutable and mutable slices are differently typed [2], it is natural to define two functions (say, `slices.Compact` vs. `slices.Compacted`) to handle each type, like Python `list.sort` vs. `sorted`. It should be natural to expect `slices.Compacted` to never alter its input, and any attempt to use a mutable version will be very explicit except for slicing [1]. [1] Especially given that the capacity is preserved by…

This would not allow the previous errors to be checked by the compiler since the main thing you're relying on is the name. Nothing prevents you to call deleted(mutable) and discard the result apart from the name.

Re: Go(lang): Robust generic functions on slices

#14
post #2

A missing tidbit that may help contextualize this post: One of the things about Go that surprised me is that if you have a slice which does not represent the full capacity of the underlying array, you can go ahead and reslice it up to that full capacity even though it's a panic to access the things you're reslicing directly: https://go.dev/play/p/oThz2bNFwgr Consequently, the GC has to assume that anything forward of…

In your playground example, if you print the capacity and the length before and after “re-extension”, it becomes clear what happened. In fact, accessing item 5 after reduction gives a size panic, where as accessing item 6 after re-extension gives you a capacity panic.

Understanding rsc’s “Go Slices” blog is very helpful here. Coming from Java or something, this exposure of underlying storage could be jarring, but coming from C, Go slices are basically built in fat arrays, and this behavior doesn’t surprise me. Maybe it was a design mistake to expose so much of the underlying machinery. Ymmv.

Re: Go(lang): Robust generic functions on slices

#15
post #13

Earlier quoted context omitted.

If immutable and mutable slices are differently typed [2], it is natural to define two functions (say, `slices.Compact` vs. `slices.Compacted`) to handle each type, like Python `list.sort` vs. `sorted`. It should be natural to expect `slices.Compacted` to never alter its input, and any attempt to use a mutable version will be very explicit except for slicing [1]. [1] Especially given that the capacity is preserved by…

This would not allow the previous errors to be checked by the compiler since the main thing you're relying on is the name. Nothing prevents you to call deleted(mutable) and discard the result apart from the name.

Indeed. Though, Rust does have a way to mark a function such that any caller that implicitly discards its return value gets a compiler warning. This feature largely solves the problem you’re talking about. But it’s orthogonal to Rust’s borrowing system or mutable versus immutable distinction.

That said, I’d also point out that, while you can more or less replicate the Go example with Rust slices, in Rust it would be more idiomatic to pass around a Vec (or a mutable reference to a Vec) if a callee needs to do something like change the length. And you can’t resize a Vec if there are other references to its contents.

Re: Go(lang): Robust generic functions on slices

#16
post #13

Earlier quoted context omitted.

If immutable and mutable slices are differently typed [2], it is natural to define two functions (say, `slices.Compact` vs. `slices.Compacted`) to handle each type, like Python `list.sort` vs. `sorted`. It should be natural to expect `slices.Compacted` to never alter its input, and any attempt to use a mutable version will be very explicit except for slicing [1]. [1] Especially given that the capacity is preserved by…

This would not allow the previous errors to be checked by the compiler since the main thing you're relying on is the name. Nothing prevents you to call deleted(mutable) and discard the result apart from the name.

> Nothing prevents you to call deleted(mutable) and discard the result

The Go compiler generates an error when you are (silently) ignoring the return value of any function. Or, to put it in other words, every compiler which does allow to (silently) ignore the return value of a function, should not be used at all (C++ has at least `[[nodiscard]]` since 17 and C with C23 - which is "too little and too late", as always).

Re: Go(lang): Robust generic functions on slices

#17
post #7

The problems with the API they point out are almost all things that rust's ownership system was built to solve. Things like: slices.Sort(s) // correct slices.Compact(s) // incorrect slices.Delete(s, ...) // incorrect s := slices.Delete(s, ...) // incorrect if 's' is referenced again in the outer scope s = slices.Delete(s, ...) // correct All of those are solved by having functions like 'slices.Sort' take a '&mut' ref…

Long before that, the problems with the API are solved by having a separate heap type for length manipulations rather than confusing the interfaces.

If Delete or Compact are only available there and it’s modified in place, the problems don’t arise in the first place.

Re: Go(lang): Robust generic functions on slices

#18
post #15
post #13

Earlier quoted context omitted.

This would not allow the previous errors to be checked by the compiler since the main thing you're relying on is the name. Nothing prevents you to call deleted(mutable) and discard the result apart from the name.

Indeed. Though, Rust does have a way to mark a function such that any caller that implicitly discards its return value gets a compiler warning. This feature largely solves the problem you’re talking about. But it’s orthogonal to Rust’s borrowing system or mutable versus immutable distinction. That said, I’d also point out that, while you can more or less replicate the Go example with Rust slices, in Rust it would be…

`#[must_use]`. I really don't know why Rust made that error.

Re: Go(lang): Robust generic functions on slices

#19
post #13

Earlier quoted context omitted.

If immutable and mutable slices are differently typed [2], it is natural to define two functions (say, `slices.Compact` vs. `slices.Compacted`) to handle each type, like Python `list.sort` vs. `sorted`. It should be natural to expect `slices.Compacted` to never alter its input, and any attempt to use a mutable version will be very explicit except for slicing [1]. [1] Especially given that the capacity is preserved by…

This would not allow the previous errors to be checked by the compiler since the main thing you're relying on is the name. Nothing prevents you to call deleted(mutable) and discard the result apart from the name.

While that's a valid concern, it is an orthogoal issue as it can be similarly replicated in Rust as well. Rust references always track mutability but we can sidestep that by using `std::borrow::Cow`:

    fn compacted(input: Cow) -> Cow { ... }
Then it is clear that, for example, `compacted(vec![...].into());` as a statement will exhibit the same behavior because `Cow` doesn't have `#[must_use]`. Rust avoids this issue mainly by encouraging explicitly mutable or immutable values by default, and at this point the language should have substantially altered that Go can do the same.

Re: Go(lang): Robust generic functions on slices

#20
post #15

Earlier quoted context omitted.

Indeed. Though, Rust does have a way to mark a function such that any caller that implicitly discards its return value gets a compiler warning. This feature largely solves the problem you’re talking about. But it’s orthogonal to Rust’s borrowing system or mutable versus immutable distinction. That said, I’d also point out that, while you can more or less replicate the Go example with Rust slices, in Rust it would be…

`#[must_use]`. I really don't know why Rust made that error.

I think it is notable that both `try!` (`?` today) and `#[must_use]` (originally restricted to `Result`, then made available in general later) appeared in the same release (0.10). In the other words, `#[must_use]` was strongly tied to `Result` back then. While we can put `#[must_use]` to any type now, the set of types that absolutely have to be `#[must_use]` remains relatively small, with a major addition being iterators and futures. Once they have been covered, any additional value from adding `#[must_use]` is not large enough to make it default, I think.
Post reply on HN