22

I am using Ruby on Rails 3.0.9 and I am trying to set "dynamically" some variable values. That is...

... in my model file I have:

attr_accessor :variable1, :variable2, :variable3


# The 'attributes' argument contains one or more symbols which name is equal to 
# one or more of the 'attr_accessor' symbols.

def set_variables(*attributes)

  # Here I should set to 'true' all ":variable<N>" attributes passed as symbol
  # in the 'attributes' array, but variable names should be interpolated in a 
  # string.
  # 
  # For example, I should set something like "prefix_#{':variable1'.to_s}_suffix".

end

How can I set those variable values to true?


I tried to use the self.send(...) method, but I did not succeed (but, probably, I don't know how to use at all that send method... is it possible do to that I need by using the send method?!).

Backo
  • 18,291
  • 27
  • 103
  • 170

5 Answers5

65
attr_accessor :variable1, :variable2, :variable3

def set_variables(*attributes)
  attributes.each {|attribute| self.send("#{attribute}=", true)}
end
Arun Kumar Arjunan
  • 6,827
  • 32
  • 35
9

Here's the benchmark comparison of send vs instance_variable_set:

require 'benchmark'

class Test
  VAR_NAME = '@foo'
  ATTR_NAME = :foo

  attr_accessor ATTR_NAME

  def set_by_send i
    send("#{ATTR_NAME}=", i)
  end

  def set_by_instance_variable_set i
    instance_variable_set(VAR_NAME, i)
  end
end

test = Test.new

Benchmark.bm do |x|
  x.report('send                 ') do
    1_000_000.times do |i|
      test.set_by_send i
    end
  end
  x.report('instance_variable_set') do
    1_000_000.times do |i|
      test.set_by_instance_variable_set i
    end
  end
end

And the timings are:

      user     system      total        real
send                   1.000000   0.020000   1.020000 (  1.025247)
instance_variable_set  0.370000   0.000000   0.370000 (  0.377150)

(measured using 1.9.2)

It should be noted that only in certain situations (like this one, with accessor defined using attr_accessor) are send and instance_variable_set functionally equivalent. If there is some logic in the accessor involved, there would be a difference, and you would have to decide which variant you would need of the two. instance_variable_set just sets the ivar, while send actually executes the accessor method, whatever it does.

Another remark - the two methods behave differently in another aspect: if you instance_variable_set an ivar which doesn't exist yet, it will be created. If you call an accessor which doesn't exist using send, an exception would be raised.

Mladen Jablanović
  • 43,461
  • 10
  • 90
  • 113
  • 1
    You're comparing apples and oranges. As I explained in another comment, the attribute names are coming in without leading `@`, so you'd need to compare `send("#{ATTR_NAME}=", i)` with `instance_variable_set("@#{ATTR_NAME}", i)` to replicate the OP's actual use case. How do those numbers look? – Marnen Laibow-Koser Dec 21 '12 at 08:44
  • 1
    I didn't forget. Now, how about an answer to my question? – Marnen Laibow-Koser Dec 24 '12 at 22:21
  • 2
    Your first comment on skorks' answer was that you think `send` was _better alternative_, and then downvoted it "for messing with internals". For god sake, it is an instance method, its job _is_ to "mess with internals", i.e. change the internal state of the object. Both ways do the job, they change the ivars, so it's OK to prefer one or the other, but it's NOT ok to downvote. So please revert the downvote and then we can continue. – Mladen Jablanović Dec 26 '12 at 10:29
  • 1
    You make a good point about the job of an instance method being to mess with internals. However, I think it's reprehensible for you to hold this discussion hostage to my changing my vote. I'll change my vote if I am convinced to do so by good argumentation; I will not change it solely in order to continue a discussion. That would hurt the integrity of the vote system. – Marnen Laibow-Koser Dec 26 '12 at 16:13
  • 2
    There's no need arguing NOT downvoting. You would need to provide a credible argument for downvoting. And OP's question is broad enough to allow for more than a single possible solution. We could argue which would be better or more suitable for the particular need. But none of the given answers is neither wrong nor misleading. – Mladen Jablanović Dec 26 '12 at 18:49
4

The method you're after is instance_variable_set so in your case:

def set_variables(*attributes)
  attributes.each {|attribute| self.instance_variable_set(attribute, true)}
end
skorks
  • 4,376
  • 18
  • 31
  • 1
    I think `send` is the better alternative here. – Marnen Laibow-Koser Aug 16 '11 at 14:04
  • Why? String interpolation is plain ugly there. Also should be slower as you have additional method calls. – Mladen Jablanović Aug 16 '11 at 20:04
  • It won't be any slower, I think. And `instance_variable_set` is messing too much with the object's internals, whereas `send` works through the interface. `instance_variable_set` is dangerous and rarely a good idea. – Marnen Laibow-Koser Aug 18 '11 at 05:25
  • The more I think about, the more I think that this merits a downvote for messing with internals. – Marnen Laibow-Koser Sep 04 '12 at 15:42
  • 1
    @MladenJablanović Rereading your original comment. The idea that there are "additional method calls" is incorrect -- every `send` call in my way of doing it corresponds to an `instance_variable_set` call here. – Marnen Laibow-Koser Oct 26 '12 at 20:43
  • See the benchmark which I posted as a separate answer. BTW, it was not fair to downvote a perfectly good answer just because you prefer the other way. – Mladen Jablanović Oct 26 '12 at 22:12
  • @MladenJablanović I wouldn't have downvoted if it were simply a matter of preference. But this was not a "perfectly good answer". You see, `instance_variable_set` breaks encapsulation by disregarding the object's interface, so it may leave the object in an inconsistent state. Granted, since the receiver is `self`, that's less of a problem here, but it's still terrible practice. By contrast, `send` uses the object's interface and ensures that the object is in a consistent state. `instance_variable_set` is virtually never a good idea in Ruby. – Marnen Laibow-Koser Dec 21 '12 at 08:36
  • @MladenJablanović Also, you'll have to do string interpolation either way. The attribute names are being passed into the function without leading `@`, which means your sample code won't even work. The correct call would be `instance_variable_set("@#{attribute}", true)`, so your claim that string interpolation is not necessary is false. – Marnen Laibow-Koser Dec 21 '12 at 08:42
3
def set_attributes(*attributes)
  attributes.each do |attr|
    self.send "#{attr}=", true
  end
end

Remember that setter method names end with = in Ruby.

Marnen Laibow-Koser
  • 5,959
  • 1
  • 28
  • 33
1

I know the question was for Rails 3, but the question came up when searching for Rails 4 answers about "how to dynamically access variable values". I tested this on my model and it worked great as an alternative to the proposed solutions:

def set_variables(*attributes)
  attributes.each {|attribute| self["#{attribute}"] = true}
end
streetlogics
  • 4,640
  • 33
  • 33