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?
Hmmm nice to see Ruby taking hints from Groovy... ;-)
ReplyDeleteThis 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.
ReplyDeleteSure, 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.
ReplyDeleteI think this solution, while cute, is too complex.
ReplyDeleteeg:
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.
ReplyDelete