How to set "dynamically" variable values?

19,424

Solution 1

attr_accessor :variable1, :variable2, :variable3

def set_variables(*attributes)
  attributes.each {|attribute| self.send("#{attribute}=", true)}
end

Solution 2

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.

Solution 3

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

Solution 4

def set_attributes(*attributes)
  attributes.each do |attr|
    self.send "#{attr}=", true
  end
end

Remember that setter method names end with = in Ruby.

Solution 5

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
Share:
19,424
Backo
Author by

Backo

Updated on June 06, 2022

Comments

  • Backo
    Backo almost 2 years

    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?!).

  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 12 years
    I think send is the better alternative here.
  • Mladen Jablanović
    Mladen Jablanović over 12 years
    Why? String interpolation is plain ugly there. Also should be slower as you have additional method calls.
  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 12 years
    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
    Marnen Laibow-Koser over 11 years
    The more I think about, the more I think that this merits a downvote for messing with internals.
  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 11 years
    @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.
  • Mladen Jablanović
    Mladen Jablanović over 11 years
    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.
  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 11 years
    @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
    Marnen Laibow-Koser over 11 years
    @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
    Marnen Laibow-Koser over 11 years
    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
    Marnen Laibow-Koser over 11 years
    I didn't forget. Now, how about an answer to my question?
  • Mladen Jablanović
    Mladen Jablanović over 11 years
    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.
  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 11 years
    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.
  • Mladen Jablanović
    Mladen Jablanović over 11 years
    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.