Go list-ops: are Append and/or Concat supposed to mutate?

func (s IntList) Append(lst IntList) IntList {...}

func (s IntList) Concat(lists []IntList) IntList {...}

Are these functions supposed to mutate s?

FWIW the Python track intentionally and explicitly doesn’t say either way. I usually nudge Python students away from mutating.

The List Ops exercise is a weird fit for most languages. Or rather, languages that aren’t Lisps or MLs. I have been wondering for a long time what to do with it outside of those language families.

I’d like to keep the topic go-specific.

In go, is it more usual to create a method on a type that (a) returns a value of that type or (b) mutates the value receiving the method call?

The simple answer is that if it passes the tests it’s fine and the Go track doesn’t impose a strong opinion on this. I’d say there’s a preference always to return new slices. The first hint that you are supposed to return new slices is that the functions return IntList instead of not returning anything if you were supposed to mutate the slice directly. However, in Go there’s a discussion to be had about what qualifies as a “new slice”.

Doing appends or concats by mutating slices directly is not really possible in Go, as append will always return you a new slice. That’s the reason you often do s = append(s, ...) and not just append(s, ...) - append does not mutate the slice - it creates a new one, which may or may not use the same underlying array as the slice you passed in (often it does though). So technically speaking, you are only working with new slices when you use append.

Also note that since (s IntList) is a value receiver and not a pointer receiver, technically, s is a shallow copy of the slice struct you call the method on. Since the slice struct contains a pointer to the data, along with length and capacity fields, you can however mutate the data it points to using s using index operations like s[i]. So, despite s being technically a copy, indexing operations will operate on the underlying array of the slice, modifying data through the pointer in the slice struct. But when talking about appends, a new slice struct is always returned, with the caveat that it might reuse the same underlying array if append decides so. This is one definition of “new slice” - if it’s a new slice struct, it is a new slice. For this exercise, just relying on this behavior of append always returning a new slice according to this definition of “new” is fine. What I mean by this, is you can keep using the receiver s and the other lists you receive as parameters as the first argument to the append calls.

However, you might be a bit unsatisfied with that definition of “new slice”, because how “new” it truly is if it has a possibility of sharing the underlying array with some other slice? To address this, optionally, you might want to go further and ensure that these new slices append returns don’t use the same underlying array as the slices passed as arguments or in the receiver for the method. The simple way to do this would be to keep doing the appends, but doing them on a new base slice:

ret := IntList{}
ret = append(ret, s...)

The first line ensures you create a new slice, with a new initially empty underlying array.
Then on the second line s... will create copies of all elements in s and append them to the new underlying array. This copy only happens if the elements of s are primitive types or structs with only primitive types. If they are pointers or struct instances with pointers inside, Go will copy the pointers but not the data pointed by the pointer, i.e it’s a shallow copy for pointers). You can also perform this copying operation using the copy built-in. For more info, see “copy” on Slice Tricks.

(Sorry if this turned into a bit of a mentoring session, but I do get really excited when talking about Go stuff)

1 Like

Bookmarked!

It was the return type that clued me in to create a new slice. I did have a very productive mentoring session with Sebastian.

I like the slice tricks. Happy to say I did matching-brackets and implemented a Stack type using just those Push and Pop implementations.