Comment by TheDong

2 years ago

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.

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.

  • 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.

    • 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);
          }

      3 replies →

    • > 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.

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.

  • 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 default, which contributes to the current confusion. See my older comment: https://news.ycombinator.com/item?id=39112735

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

      16 replies →

  • 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.

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.