One thing often touted as a missing feature in Ruby is the lack of a constructor form that initializes fields. A few other languages have this feature, including for example Groovy, another JVM dynamic language. The general idea is that if you want to construct an object and initialize a number of fields, you often want to do it in one shot. Rather than modify the class to have additional initializers for all the fields you want to set, there's another option.
Because Ruby is so cool, you can add this feature yourself to all classes at the same time.
class Class
def new!(*args, &block)
# make sure we have arguments
if args && args.size > 0
# if it's not a Hash, perform a normal "new"
return new(*args, &block) unless Hash === args[-1]
# grab the last arg in the list
last_arg = args.pop
# make sure all fields actually exist
last_arg.each_key {|key|
unless public_instance_methods.include?("#{key}=") do
raise ArgumentError.new(
"No attr setter for name: #{key}")
end
}
# create the object and set its fields
new_obj = new(*args, &block)
last_arg.each {|key, value|
new_obj.send "#{key}=", value
}
else
# no args, just do a normal "new" with any block passed
new_obj = new(&block)
end
new_obj
end
end
So with such a simple piece of code, we now have a new! method on all classes that accepts a final parameter--a hash of field names and values--that can be given using Ruby's named-parameter-like syntax. Given a simple class, like the following:
class MyObject
attr_accessor :foo
attr_accessor :bar
def initialize(msg)
puts msg
end
end
No additional work is needed to use our new! method:
x = MyObject.new!("yippee",
:foo => "hello", :bar => "goodbye")
=> "yippee"
p [x.foo, x.bar]
=> ["hello", "goodbye"]
y = MyObject.new!("blah", :yuck => "baz")
=> error: "No attr setter for name: yuck"
The reason this works is that all classes are instances of the Class class. So the MyObject class definition above is roughly equivalent to saying:
MyObject = Class.new {
# class def logic here
}
This means that instances of Class, like MyObject, inherit methods defined on Class, like new!. Since all classes in the system are Class objects, all classes instantly gain a new! method.
This is a perfect example of why Ruby is such a powerful language, and why it's so easy in Ruby to use the coolest metaprogramming tricks. And it's a primary reason why frameworks like Rails have been able to do such amazing things. With a language that's this powerful and this easy, you can imagine what else is possible.
Are we having fun yet?
7 comments:
Hmmm nice to see Ruby taking hints from Groovy... ;-)
This is surely helpful, what are the odds of this feature making into mainstream Ruby?
Actually, I think my point was that Ruby doesn't need to have language-level features like this added because it's trivial to extend the language to support them. There's probably no chance of this getting into mainstream Ruby since anyone could load those 20-25 lines of code themselves and have the feature available. That's what I love about Ruby...it may not have feature X from some other language, but it's almost always trivial to add it using a tiny amount of code.
Sure, but I think this feature is really handy and perhaps a good number of developers like to have it in their projects, how many copies/versions of the code will exist? perhaps it won't make it into mainstream Ruby but I wonder if there is an extension project (like Java's jakarta-commons) where this feature may find a home.
I think this solution, while cute, is too complex.
eg:
class Person < Struct.new(:firstname, :lastname)
You can also just extend OpenStruct if you don't know what fields you will use ahead of time.
Very clever, thank you for posting this.
Cool solution! I also learnt something new, ie, class MyClass...end is same as MyClass = Class.new {...}
One suggestion though... supposing we didn't want setters for some of our attributes (to be used internally only) -- in that case we'll not have "#{key}=", however, we may want the attribute initialized using the constructor (new!). For this, I'd use
new_obj.instance_eval "@#{key}=#{value}"
instead of
new_obj.send "#{key}=", value
Do you think that's okay?
Also, I wanted to ask something regarding the explanation about putting new! in Class. How is it different from putting it in Object?
Aman: yes, initializing the instance variables directly might be a better option. And the reason I puw new! in class is because only classes can be instantiated; putting new on all Objects wouldn't be appropriate, since you can't new an arbitrary object.
e.g.
a = "foo"
a.new! # doesn't make sense
Post a Comment