.add .addAll behaviour

Hi!

the documentation of .add and .addAll mentions that

This method may return a new ArrayedCollection. For this reason, you should always assign the result of addAll to a variable - never depend on add changing the receiver.

Can we say exactly when is it going to change the receiver or is that unpredictable?

It seems that this depends on the size of the receiver, because when you do:

z = Array.fill(10, {rrand(1,10)}).sqrt.round(0.1);
z.add(-1).addAll([9,8,7]);
-> [ 3.0, 2.6, 3.0, 2.2, 2.2, 2.4, 2.0, 2.8, 1.4, 1.7, -1, 9, 8, 7 ]

The receiver changes without the need of an additional variable.

But when doing

x = Array.fill(128, {rrand(1,10)}).sqrt.round(0.1);
x.add(-1).addAll([9,8,7]);
x.size;
-> 128

the receiver is not modified…

Typically, blocks of storage are allocated in powers of 2, such that when the number of items crosses a power of 2, a new block of memory will be allocated.

So in the second example you are going from less than or equal to 2^7 (128) to greater than it, therefore, the result is in a new memory block.

In other words, always reassign the variable, no exceptions.

1 Like

Right, that’s necessary for instances of Array
One could use List instead which exists just for that reason. However, it doesn’t support all methods that you might expect, so it’s worth getting used to this habit of writing array extensions.

1 Like

Just to clarify, in terms of classes, most class internal’s do this for you… however, the most common class, Array, does not, which makes things complicated. So always reassigning means you never have to think about this again.

.add
For some reason this is necessary only with ArrayedCollections, List’s implementation uses an Array, and just internally does this for you.

List's add

add { arg item; array = array.add(item); }

.addAll
This one you never need to reassign.

Array's .add and .addAll
	add { arg item;
		// add item to end of array.
		// if the capacity is exceeded, this returns a new
		// ArrayedCollection.
		_ArrayAdd
		^this.primitiveFailed;
	}
	addAll { arg aCollection;
		var array;
		_ArrayAddAll
		array = this;
		aCollection.asCollection.do({ arg item; array = array.add(item) }) ;
		^array
	}

The receiver is the result of the first add, which is altered.

Also, this isn’t the only method to do this, .put does this too, but it isn’t documented. So I’d er on the side of caution and always reassign if the size might grow.

1 Like

Thanks a lot! Is this likely to be changed for Array? Or is this kind of change not in the developers’ horizon?

It’s not really a bug, but a document quirk, supercollider has many of these (it’s the undocumented ones you’ve got to watch out for).

For beginners, i’d advise writing in a more functional style where you never mutate an existing object, but write functions that always return new objects. Likewise, using inline variables can help. Supercollider has some built in methods that do this, .collect .reduce .inject .injectr. Whilst this approach avoids many pitfalls one is likely to come across, it is significantly slower that other approaches and it does require getting used to.

I think it’s built into the basics of SC lang and very unlikely to be changed.

@jordan’s advice to avoid the mutation of objects is a very valid point. However, often it’s not quite clear when an object is mutated. In contrast to other languages like Lisp, so-called destructive methods are not always marked clearly, sometimes not even in the docs.

E.g., see the doc of sort for SequenceableCollection

"Sort the contents of the collection using the comparison function argument. The function should take two elements as arguments and return true if the first argument should be sorted before the second argument. If the function is nil, the following default function is used. "

Sooner or later, one realizes by unexpected behaviour that sort returns the mutated collection. If you want to keep the original, you’d have to copy it before sorting. Not to blame anyone, documentation needs volunteers, just be aware of those kind of pitfalls.

1 Like

I’ve never ever seen it do that.

Source code includes:

        if (index < 0 || index >= obj->size)
            return errIndexOutOfRange;

And a test:

a = [1, 2, 3, 4];

a.put(4, 2000);  // grow?

// nope:
^^ The preceding error dump is for ERROR: Primitive '_BasicPut' failed.
Index out of range.
RECEIVER: [ 1, 2, 3, 4 ]

I think no. It would impact the behavior of too many classes.

SC4 could revisit the topic but it’s vaporware so far.

hjh

1 Like

Oh, I actually tested that… I must have messed it up somehow

1 Like

Usually, everyone just writes a = a.add(item) everywhere, this will be safe and won’t break if one day array should automatically grow.

1 Like