How to Dynamically Create Instance Variables in Ruby
This post shows one way to dynamically initialize multiple instance variables in a Ruby class using metaprogramming. If you need to pass multiple, separate pieces of data to a constructor (and cannot refactor the code for some reason), it's a pretty good technique to reduce all the repetitive code.
Disclaimer: The point of the post is not that we should all be creating instance variables dynamically from now on, but just to show one way to do it for those rare cases when you actually do need it.
Update: This post got to the front-page of the Hackernews for a whole day, and a bunch of interesting discussion followed. Check it out!
Reading the Rails codebase is a very effective way I've found to learn Ruby, especially its advanced concepts, including metaprogramming. Rails makes an abundant use of metaprogramming, and I always learn something new when reading the Rails source code.
Late into last night, while browsing the code for ActionView framework in Rails, I came across an interesting pattern to dynamically initialize multiple instance variables. It uses a little metaprogramming, but nothing scary. Here's the highly simplified version:
# actionview/lib/action_view/base.rb
module ActionView
class Base
def initialize(assigns)
assign(assigns)
end
def assign(assigns)
assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end
end
end
Apparently, Rails loves the instance_variable_set method and uses it a lot. Let's take a deeper look to understand the problem it's solving, and the solution.
Problem
Imagine that you have a class constructor with too many arguments. You want to assign each argument to an instance variable of that class.
class Person
def initialize(name, age, address, data, ...)
@name = name
@age = age
@address = address
@data = data
# remaining assignments
end
end
You'd like to avoid all the repetitive typing to assign all the instance variables.
If you were programming in TypeScript, you could use parameter properties shorthand to assign multiple properties:
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// No body necessary
}
}
const a = new Params(1, 2, 3);
console.log(a.x); // 1No such shorthand exists in Ruby (and I'm glad that Ruby doesn't have the feature bloat like TypeScript and C#), so let's examine how you could use Ruby's metaprogramming to initialize these instance variables on the fly.
💡 Before using any advanced techniques, I suggest you try to refactor the code so you don't need to pass so many parameters. For example, you could introduce a parameter object, or preserve the whole object. Resort to metaprogramming only when you can't do it in a simpler way.
Solution
Use the Object#instance_variable_set method provided by Ruby to initialize instance variables. From the documentation,
instance_variable_set sets the instance variable named by symbol to the given object. This may circumvent the encapsulation intended by the author of the class, so it should be used with care. The variable does not have to exist prior to this call. If the instance variable name is passed as a string, that string is converted to a symbol.Let's add a method that will initialize instance variables using the above method.
class Person
def initialize(assigns)
assign(assigns)
end
private
def assign(attributes)
attributes.each do |key, value|
instance_variable_set("@#{key}", value)
end
end
end
ak = Person.new(name: 'Akshay', age: 31)
puts ak.instance_variable_get("@name") # Akshay
The assign method takes a Hash of variable names and their values. Then, for each key value pair, it calls the instance_variable_set method, passing the stringified key (prepended with the @ character to indicate an instance variable) and its value.
Now, just because you can do it, doesn't mean you should. As I mentioned in the beginning of the post, the point here is not to dynamically create all your instance variables from now on, but to show one way to do so, for those rare occasions when you do need it. In fact, if you scan the Rails codebase, you'll find a few excellent situations where you do need this approach. I'll leave this as an exercise for the readers.
💡 To be honest, I can't think of a situation where I'll be using it in my application code, but it's an interesting pattern, nonetheless. And it doesn't hurt to know all the capacities of this beautiful programming language.
What do you think? Do you have a real-world use case where you might need this in a Rails application?
P.S. If you liked this post, you might enjoy my notes from the excellent Metaprogramming in Ruby book.
Sign up for my newsletter
Let's learn to become better developers.
Comments (3)
Hey, what if I need dynamic string variable with conditional assignment operator. For example: ['var1', 'var2'].each do |var| define_method(:"method_#{var}") do variable_name = "method_#{var}" @variable_name ||= Label.where(name: var) end end Is it possible?
Yes, it's possible. But @variable_name will always refer to the literal instance variable @variable_name, not @method_var1 or @method_var2. To set dynamic instance variables, use instance_variable_set and instance_variable_get. Let me know if you want an example.
I try this: class << self ['var1', 'var2'].each do |var| method_name = "method_#{var}" define_method(method_name.to_sym) do variable_name = "@#{method_name}" if instance_variable_defined?(variable_name) instance_variable_get(variable_name) else instance_variable_set(variable_name, Label.where(name: var)) end end end end But it not work how I expect. If I call Class.method_var1 in loop, for example three times, query to Label.where(name: var)) will be requested also three times. I want that query in this case will request only one time.