Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby: Automatically set instance variable as method argument?

Are there any plans to implement ruby behavior similar to the CoffeeScript feature of specifying an instance variable name in a method argument list? Like

class User
  def initialize(@name, age)
    # @name is set implicitly, but @age isn't.
    # the local variable "age" will be set, just like it currently works.
  end
end

I'm aware of this question: in Ruby can I automatically populate instance variables somehow in the initialize method? , but all the solutions (including my own) don't seem to fit the ruby simplicity philosophy.

And, would there be any downsides for having this behavior?

UPDATE

One of the reasons for this is the DRY (don't repeat yourself) philosophy of the ruby community. I often find myself needing to repeat the name of an argument variable because I want it to be assigned to the instance variable of the same name.

def initialize(name)
  # not DRY
  @name = name
end

One downside I can think of is that it may look as though a method is doing nothing if it has no body. If you're scanning quickly, this may look like a no-op. But I think given time, we can adapt.

Another downside: if you're setting other instance variables in the body, and you try to be readable by putting all the assignments at the beginning, it can take more cognitive "power" to see that there assignments also happening in the argument list. But I don't think this is any harder than, say, seeing a constant or method call and having to jump to its definition.

# notice: instance var assignments are happening in 2 places! 
def initialize(@name)
  @errors = []
end
like image 849
Kelvin Avatar asked Jun 01 '12 19:06

Kelvin


People also ask

Can class methods access instance variables Ruby?

class . There are no "static methods" in the C# sense in Ruby because every method is defined on (or inherited into) some instance and invoked on some instance. Accordingly, they can access whatever instance variables happen to be available on the callee.

How do you declare an instance variable in Ruby?

The ruby instance variables do not need a declaration. This implies a flexible object structure. Every instance variable is dynamically appended to an object when it is first referenced. An instance variable belongs to the object itself (each object has its own instance variable of that particular class)

Is it possible to define initialize method without passing any argument?

Below are some points about Initialize : We can define default argument. It will always return a new object so return keyword is not used inside initialize method. Defining initialize keyword is not necessary if our class doesn't require any arguments.


2 Answers

After some pondering, I wondered if it's possible to actually get the argument names from a ruby method. If so, I could use a special argument prefix like "iv_" to indicate which args should be set as instance variables.

And it is possible: How to get argument names using reflection.

Yes! So I can maybe write a module to handle this for me. Then I got stuck because if I call the module's helper method, it doesn't know the values of the arguments because they're local to the caller. Ah, but ruby has Binding objects.

Here's the module (ruby 1.9 only):

module InstanceVarsFromArgsSlurper
  # arg_prefix must be a valid local variable name, and I strongly suggest
  # ending it with an underscore for readability of the slurped args.
  def self.enable_for(mod, arg_prefix)
    raise ArgumentError, "invalid prefix name" if arg_prefix =~ /[^a-z0-9_]/i
    mod.send(:include, self)
    mod.instance_variable_set(:@instance_vars_from_args_slurper_prefix, arg_prefix.to_s)
  end

  def slurp_args(binding)
    defined_prefix = self.class.instance_variable_get(:@instance_vars_from_args_slurper_prefix)
    method_name = caller[0][/`.*?'/][1..-2]
    param_names = method(method_name).parameters.map{|p| p.last.to_s }
    param_names.each do |pname|
      # starts with and longer than prefix
      if pname.start_with?(defined_prefix) and (pname <=> defined_prefix) == 1
        ivar_name = pname[defined_prefix.size .. -1]
        eval "@#{ivar_name} = #{pname}", binding
      end
    end
    nil
  end
end

And here's the usage:

class User
  InstanceVarsFromArgsSlurper.enable_for(self, 'iv_')

  def initialize(iv_name, age)
    slurp_args(binding)  # this line does all the heavy lifting
    p [:iv_name, iv_name]
    p [:age, age]
    p [:@name, @name]
    p [:@age, @age]
  end
end

user = User.new("Methuselah", 969)
p user

Output:

[:iv_name, "Methuselah"]
[:age, 969]
[:@name, "Methuselah"]
[:@age, nil]
#<User:0x00000101089448 @name="Methuselah">

It doesn't let you have an empty method body, but it is DRY. I'm sure it can be enhanced further by merely specifying which methods should have this behavior (implemented via alias_method), rather than calling slurp_args in each method - the specification would have to be after all the methods are defined though.

Note that the module and helper method name could probably be improved. I just used the first thing that came to mind.

like image 128
Kelvin Avatar answered Oct 21 '22 03:10

Kelvin


Well, actually...

class User
  define_method(:initialize) { |@name| }
end

User.new(:name).instance_variable_get :@name
# => :name

Works in 1.8.7, but not in 1.9.3. Now, just where did I learn about this...

like image 42
Matheus Moreira Avatar answered Oct 21 '22 04:10

Matheus Moreira