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
Major Major
  • 2,697
  • 3
  • 27
  • 35
  • 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 – engineersmnky Oct 09 '14 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. – Major Major Oct 09 '14 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 http://stackoverflow.com/questions/1872110/is-ruby-pass-by-reference-or-by-value it will help you with this topic. – Nobita Oct 09 '14 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. – Jeff Price Oct 09 '14 at 18:27
  • This would most definitely happen in Java too. If you, for example, modified the contents of a passed-in List. – Nick Veys Oct 09 '14 at 18:55
  • I don't believe this is related to reader/accessor methods, the @pos variable comes from web parameters and the find_max_option method is solely to return an array. I'll get a Ruby book and read more into how it all works together, I just thought arguments were a one-way street. – Major Major Oct 09 '14 at 22:11

2 Answers2

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.

Community
  • 1
  • 1
Nobita
  • 23,519
  • 11
  • 58
  • 87
  • 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. – Major Major Oct 09 '14 at 18:22
  • 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. – Jeff Price Oct 09 '14 at 18:23
  • 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. – Nobita Oct 09 '14 at 18:24
  • @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_dup`ing. – engineersmnky Oct 09 '14 at 19:09
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.

Mark Meeus
  • 597
  • 4
  • 11