zlacker

[parent] [thread] 26 comments
1. TheDon+(OP)[view] [source] 2024-02-24 05:22:50
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' reference in rust speak, and having 'slices.Compact' and 'Delete' take an owned slice, and return a new owned slice.
replies(3): >>lifthr+G1 >>maskli+u8 >>shakow+8k
2. lifthr+G1[view] [source] 2024-02-24 05:51:17
>>TheDon+(OP)
You don't even need a notion of ownership. A distinction between an immutable and mutable slice should be enough, because an immutable slice can never be changed which implies that its excess capacity (if any) can't be exploited for optimization.
replies(2): >>tsimio+03 >>DinaCo+0a
◧◩
3. tsimio+03[view] [source] [discussion] 2024-02-24 06:10:14
>>lifthr+G1
None of these functions would apply to an immutable slice, so how is it related?
replies(1): >>lifthr+C3
◧◩◪
4. lifthr+C3[view] [source] [discussion] 2024-02-24 06:19:51
>>tsimio+03
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 default, which contributes to the current confusion. See my older comment: >>39112735

[2] Originally "...are different" but edited for clarity.

replies(3): >>ikiris+m4 >>makapu+t5 >>tsimio+gC
◧◩◪◨
5. ikiris+m4[view] [source] [discussion] 2024-02-24 06:29:47
>>lifthr+C3
This sounds awful in practice having to memorize different functions for the same thing based on mutability of the thing.
replies(2): >>ttymck+75 >>shakow+qk
◧◩◪◨⬒
6. ttymck+75[view] [source] [discussion] 2024-02-24 06:41:35
>>ikiris+m4
In practice it's not that bad
◧◩◪◨
7. makapu+t5[view] [source] [discussion] 2024-02-24 06:46:57
>>lifthr+C3
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.
replies(3): >>comex+B7 >>Releas+r8 >>lifthr+29
◧◩◪◨⬒
8. comex+B7[view] [source] [discussion] 2024-02-24 07:20:16
>>makapu+t5
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.

replies(1): >>Releas+M8
◧◩◪◨⬒
9. Releas+r8[view] [source] [discussion] 2024-02-24 07:29:59
>>makapu+t5
> 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).

replies(1): >>maskli+Bi
10. maskli+u8[view] [source] 2024-02-24 07:31:00
>>TheDon+(OP)
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.

◧◩◪◨⬒⬓
11. Releas+M8[view] [source] [discussion] 2024-02-24 07:35:36
>>comex+B7
`#[must_use]`. I really don't know why Rust made that error.
replies(1): >>lifthr+Z9
◧◩◪◨⬒
12. lifthr+29[view] [source] [discussion] 2024-02-24 07:38:43
>>makapu+t5
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<T: ...>(input: Cow<'_, [T]>) -> Cow<'_, [T]> { ... }
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.
replies(1): >>nrabul+wa
◧◩◪◨⬒⬓⬔
13. lifthr+Z9[view] [source] [discussion] 2024-02-24 07:56:40
>>Releas+M8
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.
replies(1): >>kibwen+iC
◧◩
14. DinaCo+0a[view] [source] [discussion] 2024-02-24 07:56:59
>>lifthr+G1
You'd need three types: immutable slice of an immutable array, mutable slice of a mutable array (ie basically a reference to a mutable array), and an immutable slice of a mutable array.
◧◩◪◨⬒⬓
15. nrabul+wa[view] [source] [discussion] 2024-02-24 08:04:34
>>lifthr+29
must_use doesn’t have to be on a type, it can be applied to a function
◧◩◪◨⬒⬓
16. maskli+Bi[view] [source] [discussion] 2024-02-24 10:11:58
>>Releas+r8
> The Go compiler generates an error when you are (silently) ignoring the return value of any function.

It does not. You can actually infer that from TFA listing cases as problematic which would be caught by such a compilation error, and confirming it by just compiling them:

    a := []int{}
    // compiler says nothing
    slices.Delete(a, 0, 0)
The builtins are special cased to error if their return value is ignored (except for copy and recover).

> C++ has at least `[[nodiscard]]` since 17 and C with C23 - which is "too little and too late", as always

You can't even mark your own functions or types as nodiscard in Go. You need third-party tooling even just to ensure you're not unwittingly ignoring error results:

    f, err := os.Create("/tmp/filename")
    if err == nil {
        return
    }
    // compiler doesn't say anything even though f.Close returns error
    f.Close()
replies(1): >>Releas+3k
◧◩◪◨⬒⬓⬔
17. Releas+3k[view] [source] [discussion] 2024-02-24 10:34:56
>>maskli+Bi
Sorry, yes, you are of course right. That's the linter which complains, not the compiler.

I have to say that I don't understand the rationale of a compiler that errors on unused variables but lets the user silently ignore function return values. As a solution to explicitly ignore return values already exists in the language.

18. shakow+8k[view] [source] 2024-02-24 10:35:42
>>TheDon+(OP)
IMHO, all these comes from the million dollar mistake that Go made from the very beginning, with slices being a bastard between both an owned and non-owned data container. Indeed, any function accepting a slice may simply use it as an immutable view on whatever data it needs, or modify it, potentially re-alloc it, and wreak havoc by invalidating all previous references to it and/or its elements. And God forbid you passed a subslice to such a function, you're in for a nasty surprise.

Even without going the whole 9 yards of the Rust memory model, something like the views in C++, or the java {Array,Linked}Lists containing references to objects and thus being resistant to re-alloc, or even plainly immutable list like in OCaml are all working and simple enough solutions to the issue.

I still can't wrap my mind around how supposedly experienced people like Pike & co. may have designed slices, then went ‶yep, that makes perfect sense for a robust, basic, widely used data structure to create our language around″, outside of a few toy programs.

replies(1): >>bsaul+6f1
◧◩◪◨⬒
19. shakow+qk[view] [source] [discussion] 2024-02-24 10:40:43
>>ikiris+m4
Programming is hard and one can not destroy complexity, only move it; other news at 12.
◧◩◪◨
20. tsimio+gC[view] [source] [discussion] 2024-02-24 14:28:23
>>lifthr+C3
That would be nice, sure, but it still doesn't fix the problem for mutable slices. Immutable slices are a distraction - this discussion is explicitly about how the mutable version is supposed to work. Unless you want to argue that mutable arrays/slices shouldn't exist at all, of course.
◧◩◪◨⬒⬓⬔⧯
21. kibwen+iC[view] [source] [discussion] 2024-02-24 14:28:44
>>lifthr+Z9
> While we can put `#[must_use]` to any type now, the set of types that absolutely have to be `#[must_use]` remains relatively small

Agreed, although conversely if we could start Rust development all over again I'd probably argue that must_use on functions (as opposed to on types) should probably be the default behavior. (These days it basically is the default behavior for functions in the standard library.) Though Rust didn't gain the ability to annotate functions with must_use until 1.27. Switching the default could perhaps be a good candidate for an Edition.

replies(1): >>tialar+401
◧◩◪◨⬒⬓⬔⧯▣
22. tialar+401[view] [source] [discussion] 2024-02-24 17:11:54
>>kibwen+iC
It's too late to propose new features for Edition 2024, and you would first need to get some attribute agreed which has the opposite effect, then write up how your proposal works and see if it's generally agreed. I doubt that but you should certainly try.
◧◩
23. bsaul+6f1[view] [source] [discussion] 2024-02-24 18:53:34
>>shakow+8k
my feeling with slices is that go wanted to really make programmers mindful of the cost of allocating new space on array operations.

By having this weird API on slices, they force you to be explicit on allocating new memory.

replies(2): >>maskli+Li1 >>shakow+1y1
◧◩◪
24. maskli+Li1[view] [source] [discussion] 2024-02-24 19:18:53
>>bsaul+6f1
None of that makes any sense. Slices don't force you to be explicit on allocating new memory in any way. you can literally do this:

    s := []int{}
    for i := range 29754 {
        s = append(s, i)
    }
do you see explicit allocation of new memory? As far as I'm concerned that's not in any way more explicit than e.g.

    var s = new List();
    foreach(var i in Enumerable.Range(0, 29754)) {
        s.Add(i);
    }
replies(1): >>bsaul+mj1
◧◩◪◨
25. bsaul+mj1[view] [source] [discussion] 2024-02-24 19:23:06
>>maskli+Li1
s = append(s,i) makes you realize something happened to the memory behind s. Otherwise you would simply call s.append(i)
replies(1): >>maskli+Js1
◧◩◪◨⬒
26. maskli+Js1[view] [source] [discussion] 2024-02-24 20:38:43
>>bsaul+mj1
The something that happens is that slices are not heap collections (unlike hashmap which say even less about allocations, but I'm sure you'll find an other excuse), so you can't even increment their length without returning a new copy.

I also fail to see how this would translate to

> make programmers mindful of the cost of allocating new space on array operations.

anyway. `append` is not taking in an allocator, reporting allocation failures, or reporting reallocations here. Like proper vectors, it's silently reallocating (and over-allocating) as needed.

◧◩◪
27. shakow+1y1[view] [source] [discussion] 2024-02-24 21:28:46
>>bsaul+6f1
> they force you to be explicit on allocating new memory.

Not at all. With e.g. `append` being a “maybe I'll allocate, maybe not, take a guess” kind of method, it's basically just like Java's `List.add` with extra steps and much worse egonomics.

[go to top]