Search code examples
rubysplat

Ruby splat operator changing value inside loop


I want to define a method which can take an optional amount of arguments and hashes, like so

def foo(*b, **c)
  2.times.map.with_index { |i|
    new_hash, new_array = {}, b
    c.map { |key, value| new_hash[key] = value[i] unless value[i].nil? }
    new_array << new_hash if new_hash.length > 0
    send(:bar, new_array)
  }
end

def bar(*b)
  p b
end

If I've understood the splat and double splat operators correctly (which I doubt), then this should send the array b to the bar method, and only adding the new_hash from foo if it contains something. However, something weird happens - I'll try and illustrate with some snippets below

# invoking #foo 
foo(a, key: 'value')

# first iteration of loop in #foo
  # i is 0
  # b is []
  # c is { :key => ['value1'] }
  # send(:bar, new_array) => send(:bar, [{:key => 'value1'}])
  # bar yields: [{:key => 'value1'}]

Now, however, something happens

# second iteration of loop in #foo
  # i is 1
  # b is [:key => 'value1'] <---- why?
  # c is { :key => ['value1']

Why has the value of b changed inside the loop of foo?

edit Updated the code to reflect a new array is created for each iteration


Solution

  • new_hash, new_array = {}, b
    

    This doesn't create a copy of b. Now new_array and b point to the same object. Modifying one in-place will modify the other.

    new_array << new_hash
    

    That modifies new_array (and thus b) in place, so the new element remains on the next iteration. Use something like +, which creates a copy:

    send(:bar, *(b + (new_hash.empty? ? [] : [new_hash])))