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); // 1
No 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.
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.
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 book "Metaprogramming Ruby 2".