2

I have an array made from variables, and I want to perform the same operation on each, and store the result in the original variable:

(one, two, three) = [1, 2, 3]

[one, two, three].map!{|e| e += 1}
# => [2, 3, 4] 

# But:
[one, two, three]
# => [1, 2, 3]

# You have to:

(one, two, three) = [one, two, three].map{|e| e += 1}
# => [2, 3, 4] 

[one, two, three]
# => [2, 3, 4]

This doesn't seem like the "right way" to do it, but I'm not managing to find that "right way". I also have some vague ideas as to what's going on, but I'm not too sure, so an explanation would be appreciated.


My actual use case is that I've got named parameters and I'm e = File.new(e) if e.is_a? String

3 Answers 3

4

Numbers (such as Fixnum) in Ruby are immutable. You cannot change the underlying value.

Once you assign one = 1, it is not possible to change the value of one without a new assignment. When you do one += 1. You are actually assigning the new value 2 to the variable one; it's a whole new object.

You can see this more clearly by looking at the object_id (a.k.a. __id__):

one = 1
1.object_id     # => 2
one.object_id   # => 2
one += 1
one.object_id   # => 5
2.object_id     # => 5

Now in your Array#map! statement, you're not actually changing the one object. A reference to this object is stored in the array; not the actual variable. When you enumerate with map!, the object returned by the block is then stored in the internal reference location at the same position. Think of the first pass over map! similar to the following:

one = 1
one.object_id     # => 2

arr = [one]

arr[0].object_id  # => 2

arr[0] += 1   # You are re-setting the object at index 0
              # not changing the original `one`'s value

arr[0]            # => 2
arr[0].object_id  # => 5

one               # => 1
one.object_id     # => 2

Since these Fixnum objects are immutable, there is no way to change their value. This is why you have to de-reference the result of your map back to the original values:

(one, two, three) = [1, 2, 3]
one.object_id      # => 3
two.object_id      # => 5
three.object_id    # => 7

(one, two, three) = [one, two, three].map{|e| e += 1}
one.object_id      # => 5
two.object_id      # => 7
three.object_id    # => 9
Sign up to request clarification or add additional context in comments.

Comments

2

Try this:

a = [one, two, three]
a.map!{|e| e += 1}

The problem is that [one, two, three] is not a variable that stores an array, it's a brand new array each time you write it. Once you set a = [one, two, three], you have a variable that stores that value which you can then operate on.


Darshan pointed out in the comments that this doesn't actually modify the values of the original variables one, two, and three, and he's correct. But there is a way to do it:

["one", "two", "three"].each{ |e| eval "#{e} += 1" }

But that's pretty ugly, relies on using strings in the array instead of the actual variables, and is probably a lot worse than what you already came up with:

(one, two, three) = [one, two, three].map{|e| e += 1}

1 Comment

This doesn't work. one, two and three remain unchanged.
0

If you really want to change the value of variables that refer to fixnums, then what you're doing is the best you can do in Ruby. That said, you might be better off not storing them as three separate variables. Rather than having one, two, and three, you can have a[0] through a[2] and pass a around, or h[:one] through h[:three] and pass h around.

a = [1, 2, 3]
a.map!{|e| e += 1}
a # => [2, 3, 4]

h = {:one=>1, :two=>2, :three=>3}
h.each_key{|k| h[k] += 1}
h # => {:one=>2, :two=>3, :three=>4}

The second option, using a Hash, is probably closer to what you want, because h[:some_name] is closer to using a variable name.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.