9

This is a common initializer pattern:

def initialize(title, val, type)
  @title, @val, @type = title, val, type
end

Is there a shortcut that is equivalent to "take every argument, create an attribute of the same name, and set the attribute equal to the argument's value"?

I'm looking for a gem-free solution.

Jonah
  • 15,806
  • 22
  • 87
  • 161

4 Answers4

5

You will lose the function to check against wrong arguments, but can do this:

def initialize(*args)
  @title, @val, @type = args
end

But if you are repeatedly doing this, then your code is not right. You should better redesign your API to take named arguments:

def initialize(title:, val:, type:)
  ...
end

WhateverClass.new(title: "foo", val: "bar", type: "baz")
sawa
  • 165,429
  • 45
  • 277
  • 381
3

You can use Struct as mentioned above or do it dynamically like:

class MyClass

  def initialize input
    input.each do |k,v|
      instance_variable_set("@#{k}", v) unless v.nil?
    end
  end

end

Anyways If it's my code I'll go using Struct.

Eki Eqbal
  • 5,779
  • 9
  • 47
  • 81
2

Here's how you could do that. One instance variable and an associated read/write accessor will be created for each of initialize's parameters, with the variable having the same name, preceded by @, and each instance variable will be assigned the value of the associated parameter.

Code

class MyClass
  def initialize(< arbitrary parameters >)
    self.class.params.each { |v|
      instance_variable_set("@#{v}", instance_eval("#{v}")) }

    < other code >
  end

  @params = instance_method(:initialize).parameters.map(&:last)
  @params.each { |p| instance_eval("attr_accessor :#{p}") }

  class << self
    attr_reader :params
  end  

  < other code >     
end

Example

class MyClass
  def initialize(a, b, c)
    self.class.params.each { |v|
      instance_variable_set("@#{v}", instance_eval("#{v}")) }
  end

  @params = instance_method(:initialize).parameters.map(&:last)
  @params.each { |p| instance_eval("attr_accessor :#{p}") }

  class << self
    attr_reader :params
  end  
end

MyClass.methods(false)
  #=> [:params]
MyClass.instance_methods(false)
  #=> [:a, :a=, :b, :b=, :c, :c=]

m = MyClass.new(1,2,3)
m.a #=> 1
m.b #=> 2
m.c #=> 3
m.a = 4
m.a #=> 4

Explanation

When class MyClass is parsed, the class instance variable @params is assigned an array whose elements are initialize's parameters. This is possible because the method initialize been created when the code beginning @params = ... is parsed.

The method Method#parameters is used to obtain initialize's parameters. For the example above,

instance_method(:initialize).parameters
  #=> [[:req, :a], [:req, :b], [:req, :c]]

so

@params = instance_method(:initialize).parameters.map(&:last)
  #=> [:a, :b, :c]

We then create the read/write accessors:

@params.each { |p| instance_eval("attr_accessor :#{p}") }

and a read accessor for @params, for use by initialize:

class << self
  attr_reader :params
end  

When an instance my_class of MyClass is created, the parameter values passed to MyClass.new are passed to initialize. initialize then loops though the class instance variable @params and sets the value of each instance variable. In this example,

MyClass.new(1,2,3)

invokes initialize(a,b,c) where

a => 1
b => 2
c => 3

We have:

params = self.class.params
  #=> [:a, :b, :c]

params.each { |v| instance_variable_set("@#{v}", instance_eval("#{v}")) }

For the first element of params (:a), this is:

instance_variable_set("@a", instance_eval(a) }

which is:

instance_variable_set("@a", 1 }

causing @a to be assigned 1.

Note the accessor for @params is not essential:

class MyClass
  def initialize(a, b, c)
    self.class.instance_variable_get(:@params).each { |v|
      instance_variable_set("@#{v}", instance_eval("#{v}")) }
  end

  @params = instance_method(:initialize).parameters.map(&:last)
  @params.each { |p| instance_eval("attr_accessor :#{p}") }
end
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
1

I like to just use an args hash and send each attribute to the instance. However this will not create accessors for these attributes, so you will need to add these if they are desired.

def initialize(args)
  args.each do |method, value|
    self.send("#{method}=", value)
  end
end
jlesse
  • 564
  • 5
  • 11