Comment by beautron

2 years ago

I think slices are wonderfully intuitive... if you've worked with C.

Slices encapsulate an ubiquitous C pattern, where you pass to a function: a pointer (to an initial array element), and a length or capacity (sometimes both). This pattern is directly encapsulated by Go's slices, which can be thought of as something like:

  type Slice struct {
      Ptr *Elem
      Len int
      Cap int
  }

I love Go's slice syntax. It's the right piece of syntactic sugar. It removes tedium and room-for-mistakes from this prevalent C pattern. It lets me work as precisely with memory as I do in C, yet everything is simpler, lighter, and breezier.

For example, I'm making a game in Go. I don't want to be allocating memory throughout the game (which can cause frame drops), so instead I allocate giant arrays of memory on launch. Then I use slices to partition this memory as needed.

Some of these arrays get their elements re-accumulated at some interval (every level, or every frame, etc.). And so it works nicely to first set them to zero length:

  myMem = myMem[:0]

Notice that the myMem slice now has zero length, but still points to its same underlying array. Then I perform the accumulation:

  for ... {
      myMem = append(myMem, elem)
  }

Again, I don't want to be allocating in general, so I care very much that append continues to use myMem's preexisting capacity.

All this is to say, I don't see slices as being the way they are for the "sake of optimization." Rather I see them as an ideal tool for working with, referring to, and partitioning memory.

C doesn't have any slicing operator like Go's `a[x:y]`, which is the main problem I want to point out. Slice itself is just a natural construction.

  • Yes, I'm saying Go's slice operator (and slice type, and related functions) grew out of experience from the way arrays are used in C.

    To me, this becomes obvious if you read The Practice of Programming by Kernighan and Pike, the latter of which was a co-designer of Go. If you read the book, which was written well before Go, and pay attention to how it uses C arrays, you can almost feel Go slices emerging. The slice syntax perfectly encapsulates the C array bookkeeping.

    • I'm not sure how it can be possible. In my experience the notion of three-part slices does exist in C but only implicitly. For example,

          size_t trim_end(const char *p, size_t len) {
              while (len > 0) {
                  if (p[len - 1] != ' ') break;
                  --len;
              }
              return len;
          }
      

      Conceptually this function accepts a slice `(p, len, cap)` and returns a slice `(p, len2, cap)` where `len2 <= len` and the capacity never changes. But the actual argument doesn't have `cap`, and the return argument doesn't have `p`. Everything is implicit and it's typical for C programmers to fully document and follow such implicits. Go's slice operator can't come out of such implicit practices in my opinion.

      In comparison, your claim only makes sense when the following was somehow normal:

          struct slice { const char *p; size_t len, cap; };
          struct slice trim_end(const struct slice *s) {
              struct slice out = *s;
              while (out.len > 0) {
                  if (out.p[out.len - 1] != ' ') break;
                  out = subslice(out, 0, out.len - 1);
              }
              return out;
          }
      

      Note that a hypothetical `subslice` function call maps perfectly to a Go code `out[0:len(out)-1]`, and my complaint will equally apply: there should be two clearly named variants of `subslice` that may or may not keep the capacity. But I hardly saw such construction in C.

      3 replies →