4

It seems that the arguments are copied when using the splat operator to pass arguments to a block by reference.

I have this:

def method
  a = [1,2,3]
  yield(*a)
  p a
end

method {|x,y,z| z = 0}
#=> this puts and returns [1, 2, 3] (didn't modified the third argument)

How can I pass these arguments by reference? It seems to work if I pass the array directly, but the splat operator would be much more practical, intuitive and maintainable here.

alf
  • 18,372
  • 10
  • 61
  • 92

4 Answers4

8
  1. In Ruby when you write x = value you are creating a new local variable x whether it existed previously or not (if it existed the name is simply rebound and the original value remains untouched). So you won't be able to change a variable in-place this way.

  2. Integers are immutable. So if you send an integer there is no way you can change its value. Note that you can change mutable objects (strings, hashes, arrays, ...):

    def method
      a = [1, 2, "hello"]
      yield(*a)
      p a
    end
    
    method { |x,y,z| z[1] = 'u' }
    # [1, 2, "hullo"]
    

Note: I've tried to answer your question, now my opinion: updating arguments in methods or blocks leads to buggy code (you have no referential transparency anymore). Return the new value and let the caller update the variable itself if so inclined.

tokland
  • 66,169
  • 13
  • 144
  • 170
  • Very clear, thanks! The reason for yielding in this method is to give to the caller an opportunity to do some preprocessing on the elements of the array. I like your idea of returning the new values. I guess I'll go with that. Thanks again! – alf Jul 31 '12 at 19:20
2

The problem here is the = sign. It makes the local variable z be assigned to another object.

Take this example with strings:

def method
  a = ['a', 'b', 'c']
  yield(*a)
  p a
end

method { |x,y,z| z.upcase! }   # => ["a", "b", "C"]

This clearly shows that z is the same as the third object of the array.

Another point here is your example is numeric. Fixnums have fixed ids; so, you can't change the number while maintaining the same object id. To change Fixnums, you must use = to assign a new number to the variable, instead of self-changing methods like inc! (such methods can't exist on Fixnums).

Sony Santos
  • 5,435
  • 30
  • 41
  • "This clearly shows that arguments was passed by reference" – No, it doesn't. It shows that Ruby is not a pure functional language. That's all it shows. In Ruby, arguments are *always* passed by value. *Always*. No exceptions, no ifs, no buts. – Jörg W Mittag Aug 01 '12 at 00:12
  • I mean `z` is not `a[2].dup`, but `z` and `a[2]` share the same object_id (which is a kind of "reference" to the object). Maybe "reference" is not a proper word, as it may not have the same meaning as in other languages (ie, "pointer"). – Sony Santos Aug 01 '12 at 12:00
  • The thing is that if arguments were passed by reference, as you say, then we wouldn't have this conversation, because the OP's code would have worked as he expected and he never would have asked this question in the first place! Here's a short code snippet which you can use to determine whether Ruby (or indeed any language) passes arguments by value or by reference: `def foo(bar); bar = 'reference' end; baz = 'value'; foo(baz); puts "Ruby is pass-by-#{baz}"` – Jörg W Mittag Aug 01 '12 at 13:10
0

Yes... Array contains links for objects. In your code when you use yield(*a) then in block you works with variables which point to objects which were in array. Now look for code sample:

daz@daz-pc:~/projects/experiments$ irb
irb(main):001:0> a = 1
=> 1
irb(main):002:0> a.object_id
=> 3
irb(main):003:0> a = 2
=> 2
irb(main):004:0> a.object_id
=> 5

So in block you don't change old object, you just create another object and set it to the variable. But the array contain link to the old object.

Look at the debugging stuff:

def m
  a = [1, 2]
  p a[0].object_id
  yield(*a)
  p a[0].object_id
end

m { |a, b| p a.object_id; a = 0; p a.object_id }

Output:

3
3
1
3
Danil Speransky
  • 29,891
  • 5
  • 68
  • 79
  • 1
    Your example is not correct, becuase number actually is not an object. Try 3.object_id or 5.object_id, you'll always get the same ids. – megas Jul 31 '12 at 17:06
  • There's a magic, the id of number is always '2*number+1', how it possible to get different ids? ;) – megas Jul 31 '12 at 17:18
  • I think what @SperanskyDanil is trying to say is that splat operator is passing by reference. So the OP's question is stating incorrect assumption. Fact that integers have fix object_ids and are immutable is not related to this. you can use strings and you will get similar outputs. – Iuri G. Jul 31 '12 at 17:24
0

How can I pass these arguments by reference?

You can't pass arguments by reference in Ruby. Ruby is pass-by-value. Always. No exceptions, no ifs, no buts.

It seems to work if I pass the array directly

I highly doubt that. You simply cannot pass arguments by reference in Ruby. Period.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653