Unexpected collection behaviour

I am getting an unexpected result to the following

a= Array.rand(20,1,14)

a.do({|item,i| if (item < 5, {a.removeAt(i)})})

I would expect that all values lower then 5 would be expunged from a. that is almost what i get
before
→ [ 6, 7, 3, 1, 13, 14, 9, 9, 14, 2, 7, 2, 8, 3, 8, 3, 7, 6, 14, 9 ]
after
→ [ 6, 7, 1, 13, 14, 9, 9, 14, 7, 8, 8, 7, 6, 14, 9 ]
I suspect the 1 is still there because it appears after the 3 but I don’t understand why.
I am aware that select would work better in this case. However, in the actual case I will have two corresponding lists a and b and I will need to expunge values lower then x from a and the corresponding (i.e. in the same position) values from b. So the real code will also remove an item from b.

When you remove an item from the array, all subsequent items “shift left”. The loop continues with the following index, potentially skipping an element.

  1. I would create a new array instead of an in-place operation (I prefer immutability when possible) and use a higher-order function instead of looping.

  2. Another option: iterate from the end of the array to the beginning.

If you choose 1: no side effects, more accessible to reason about and debug, (in parallel systems, it would have a few other “pros”)

If you choose 2, it’s the opposite of what was said about 1, but it’s more memory efficient and faster for very large data. I would reserve this for critical performance situations with large amounts of data. And write good tests.

General principle is don’t modify a collection that you are iterating over. A copy or reverse iteration would work yes, but I think reject is what you’re looking for. It returns a new collection.

I would put that option in my item 1. a new array with higher-order functions (e.g.,filter, select, etc), SC has a few

thanks.
iterate from the end doesn’t work because it ends up trying to access an index that does not exist. Can you elaborate on option 1 - not sure what higher-order function would be.
As I mentioned, I am aware that select or reject would work, but I don’t see how I can combine them with removing the corresponding elements of the corresponding array. In other words, what I would like to do is

a.do({arg item, i; if (item <5, {a.removeAt(i); b.removeAt(i)})}) 

where a and b have the same length.
copying doesn’t really work either. with c a copy of a, replacing a.do with c.do in the above throws a predictable error a and b become shorter but not c so the index exceeds the size of the array.
thanks

1 Like
~testFunc = { |num| num > 3 };
~numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
~new_numbers = ~numbers.select(~testFunc);

in short, higher-order functions like select, filter, etc., use boolean-returning functions to make decisions about elements. These predicates define a condition or test that each element must pass.

wikipedia

I think it works:

~removeItemsInPlace = { |arr, conditionToRemove|
    var i = arr.size - 1;
    while { i >= 0 } {
        if (conditionToRemove.(arr[i])) {
            arr.removeAt(i);
        };
        i = i - 1;
    };
    arr; 
};

~arrayToModify = (1..20);

~conditionToRemove = { |item| item.odd };

~removeItemsInPlace.(~arrayToModify, ~conditionToRemove);

~arrayToModify
// -> -> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

But again, it is not safe. Prefer the first option.

thanks. I opted for a refinement of this approach by first collecting indexes to be removed into a Set . And in the next step removing those from both lists. The advantage there is the option of using more then one condition for removal.