tags:

views:

127

answers:

3

I am in the midst of learning Ruby and thought I was clever with the following piece of code:

[@start,@end].map!{ |time| time += operation == :add ? amount : -(amount) }

where @start, @end are two module level variables, operation can be one of :add or :sub, and amount is an float amount to adjust both @start and @end by.

Granted it only saves me a line of code, but why doesn't this approach work, and how can I get something similar that does?

(My expected output is for @start/@end to be modified accordingly, however unit tests show that they stay at their original values.)

+2  A: 

The addition operation in the block doesn't modify 'time', it returns a new value. So the elements in the array aren't modified, they're replaced.

dbarker
This makes sense. I guess there's no way to get my code to work as I want to write it.
cfeduke
+3  A: 

The += operator changes the value of time but it returns the old value of time, therefore the right code is:

@start,@end = [@start,@end].map!{ |time| time + (operation == :add ? amount : -amount) }

EDIT Updated the code to actually change @start and @end.

Koraktor
Hmm still no dice. @start += operation == :add ? amount : -(amount) is working, but not when I try to do it through Array#map block. :(
cfeduke
Yup this works, thanks! (I'm picking Chuck's answer because he's done a good job of explaining what's happening under the hood, and how I can think alternatively about Ruby.)
cfeduke
+4  A: 

It's important in Ruby to remember the distinction between variables and the objects they hold. Simply setting a variable will never change the object referenced by that variable. When you do a += b, it's just shorthand for a = a + b. So you're assigning a new value to the variable a, not changing the object that used to be there or changing any other references to that object. So changing the variable time doesn't change @start.

In order to assign to an instance variable, you need to actually assign to that instance variable. Here's a way to do what you were looking for:

operation = :+
amount = 12
@start, @end = [@start, @end].map {|time| time.send(operation, amount)}

You'll notice that we're not faffing around with that :add and :sub business either — we can just pass the actual name of the message we want to send (I used + in this case, but it could be anything).

If you had a big, dynamically generated list of ivars you wanted to set, it's only a little bit more complicated. The only difference there is that need to get and set the ivars by name.

ivars = [:@start, :@end, :@something_else]
operation = :+
amount = 12
ivars.each {|ivar| instance_variable_set(ivar, instance_variable_get(ivar).send(operation, amount))}
Chuck
Great, thanks. Shows how much my C# assumptions just do not apply in Ruby.
cfeduke