2

I'm new to Ruby and I'm having trouble understanding what's happening in this method.

I make this call in a Rails controller -

@arr = SomeClass.find_max_option(params[:x], @pos, params[:y], some_var)

I'm trying to return the value to @arr, which happens successfully, but manipulations I make to @pos within that method are being brought back as well; the value of @pos changes when I'm only trying to get the value for @arr.

Here's more details on the method

#before going into the method
 @pos = [a,b]

def self.find_max_option(x, pos, y, some_var)

pos.collect! { |element|
    (element == b) ? [c,d] : element
    }
  end

#new value of pos = [a, [c,d]] which is fine for inside in this method

... #some calculations not relevant to this question, but pos gets used to generate some_array

return some_array

But when the method is finished and gets back to the controller, the value of @pos is now [a,[c,d]] as well.

What's going on here? I thought that pos would be treated separately from @pos and the value wouldn't carry back. As a workaround I just created a new local variable within that method, but I'd like to know what this is happening

#my workaround is to not modify the pos variable
pos_groomed = pos.collect { |element|
    (element == b) ? [c,d] : element
    }
  end
6
  • you are passing @pos as an argument this means the pos is a reference to @pos. you could set pos = pos.dup prior to the collect! or just use non-destructive collect. Your code is not exactly straight forward since you are returning a variable that does not exist Commented Oct 9, 2014 at 18:18
  • I'm sorry for the noob question, but this is standard for all Ruby methods? Making a change to the arguments passed in within the method propagate up? All arguments are in/out by default? I don't remember this happening in Java. Commented Oct 9, 2014 at 18:20
  • Don't be sorry, this is not a noob question, and even if it was, you shouldn't be sorry. Check this stackoverflow.com/questions/1872110/… it will help you with this topic. Commented Oct 9, 2014 at 18:26
  • Major Major - are you defining any reader/accessors? Chances are you think you have a local variable named pos and you are really calling a reader method named pos. Commented Oct 9, 2014 at 18:27
  • This would most definitely happen in Java too. If you, for example, modified the contents of a passed-in List. Commented Oct 9, 2014 at 18:55

2 Answers 2

3

Instead of using collect!, just use collect (without the !). So, rewrite your method as:

def self.find_max_option(x, pos, y, some_var)
  pos.collect { |element|
    (element == b) ? [c,d] : element
  }
end

When using the ! version of collect, you are replacing each element with the value returned by the block. However, when using collect without !, a new array is created, and the object where collect is being called it doesn't get changed. See the docs:

collect! vs collect

Using ! at the end of a method name is a common practice in Ruby. This question is related and would be worth taking a look.

Sign up to request clarification or add additional context in comments.

4 Comments

I still need the pos altered to use later in the method (I didn't show the entire thing, just the relevant part), so to use pos.collect I would need to declare a new variable with it like I did in my workaround, right? The issue was more-so why is @pos being altered as well.
As a note on this answer, the ruby convention is that any method with a ! is going to have "unexpected side effects". Most often a ! will change the object that is calling it, but sometimes it has other side effects such as exit! which quits without calling any exit handlers. When using a ! method, always consult the docs and make sure you understand what "else" is happening.
You would need to do a dup as @engineersmnky has suggested in the comment or assign that collect to another variable in the method (like you did with pos_groomed), and use that from then on.
@MajorMajor also just because I was reference here please also remember that dup will make a copy of the outside container but if you are manipulating items inside they will still hold their references. e.g. if you change a inside dup will not help. For a single dimensional array you could use .map(&:dup) which will create a new Array where the objects inside are copies of the original but anything more gets into deep_duping.
1

You are using the destructive version of collect. Destructive methods change the object on which the method is called, while non-destructive methods return new objects.

Ruby developers tend to call these methods 'bang methods', because the convention is that destructive methods have the ! suffix.

pos.collect! # changes pos and returns pos
pos.collect  # creates a new object

Your workaround only works because you use the non-destructive collect, while the original code uses collect!

pos.collect do |element|
 (element == b) ? [c,d] : element
end

Should work just fine.

As to why the object changes outside of the method:

In ruby, when you pass an argument to a method, you are actually passing the reference to the object. So passing an array into a method doesn't make a copy, but simply passes the reference to original array. There is no way to 'pass by value' but you can create a copy yourself with dup or clone, if you really have to.

4 Comments

I would be careful saying 'you are passing a reference to that object'. That is not true in Ruby. Ruby is strictly pass by value.
Yes, you are right, it's pass by value, but the value is a reference to an object. It's a bit tricky
I look at it this way: 'Variables are references to objects'.
Your last paragraph was extremely helpful in understanding what's going on here, thanks.

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.