Saturday, January 12, 2008

Ruby: Hash#to_mod

I was recently working with some code where I wanted to initialize an object with a hash. The following example should illustrate what I'm trying to do.

Movie.new(:title => 'Godfather', :subtitles => 'English', 
:audio_track_1 => "English", :audio_track_2 => "French")

I'm sure that doesn't look very surprising if you are familiar with creating ActiveRecord::Base (AR::B) subclasses; however, my current project has no database and the Movie class was not a subclass of AR::B. Additionally, the Movie instance needed readers for title, subtitles, audio_track_1, and audio_track_2, but the instance is immutable so writers are not necessary. Lastly, I wanted my Movie instances to be able to have any attribute that the hash passes in as a key.

Since I needed the attributes to be dynamic based on the hash I started with a loop that defined methods on the instance.

class Object
def metaclass; class << self; self; end end
end

class Movie
def initialize(options)
metaclass.class_eval do
options.each_pair do |key, value|
define_method key do
value
end
end
end
end
end

movie = Movie.new(:title => 'Godfather', :subtitles => 'English')
movie.title # => "Godfather"
movie.subtitles # => "English"

The above code works fine, and the constructor logic could be abstracted and defined in Class instead to make the Movie definition cleaner; however, I thought it would be nice to create a solution that didn't need to class_eval on the metaclass. Extending a module is another easy way to define additional behavior on an object, so I put together the following code that converts a hash into a module that has a method defined for each key and the return value of the method is the associated value.

class Hash
def to_mod
hash = self
Module.new do
hash.each_pair do |key, value|
define_method key do
value
end
end
end
end
end

With the Hash#to_mod method available the constructor of Movie simply extends the new instance with the module created by the hash.

class Movie
def initialize(options)
self.extend options.to_mod
end
end

movie = Movie.new(:title => 'Godfather', :subtitles => 'English')
movie.title # => "Godfather"
movie.subtitles # => "English"

Pat Farley pointed out that this solution is also nice because it allows me to avoid costly typos by converting my Hash instances into an object with methods. For example, If I have a params hash in a controller I can easily create an object and access it's methods instead of using keys with the hash.

class PersonService
def self.process(name, blog)
# ...
end
end

class PersonController < ApplicationController
def create
params # => {:person=>{:name=>"Pat Farley", :blog=>"http://www.klankboomklang.com/"}}
person = Object.new.extend(params[:person].to_mod)
PersonService.process(person.name, person.blog)
end
end

Of course, Pat also pointed out that Hash#to_obj could be defined to make things look even a bit nicer.

class Hash
def to_mod
hash = self
Module.new do
hash.each_pair do |key, value|
define_method key do
value
end
end
end
end

def to_obj
Object.new.extend self.to_mod
end
end

class PersonService
def self.process(name, blog)
# ...
end
end

class PersonController < ApplicationController
def create
params # => {:person=>{:blog=>"http://www.klankboomklang.com/", :name=>"Pat Farley"}}
person = params[:person].to_obj
PersonService.process(person.name, person.blog)
end
end

Lastly, Pat recommended a recursive version that would allow you to access the entire hash by way of method calls.

class Object
def to_obj
self
end
end

class Hash
def to_mod
hash = self
Module.new do
hash.each_pair do |key, value|
define_method key do
value.to_obj
end
end
end
end

def to_obj
Object.new.extend self.to_mod
end
end

class PersonService
def self.process(name, blog)
name # => "Pat Farley"
blog # => "http://www.klankboomklang.com/"
end
end

class PersonController < ApplicationController
def create
params # => {:person=>{:blog=>"http://www.klankboomklang.com/", :name=>"Pat Farley"}}
PersonService.process(params.to_obj.person.name, params.to_obj.person.blog)
end
end

14 comments:

  1. Jay,

    Check out the small constructor.rb library at http://atomicobjectrb.rubyforge.org

    Pretty much does all of this already, along with accessors generation and strict key checking.

    ReplyDelete
  2. Mike Berrow8:24 PM

    This looks a lot like what you can do with OpenStruct, a ruby core class.
    http://www.ruby-doc.org/core/classes/OpenStruct.html

    require 'ostruct'
    movie = OpenStruct.new(:title => 'Godfather', :subtitles => 'English')
    movie.title # => "Godfather"
    movie.subtitles # => "English"

    You could first base your movie class on OpenStruct and then take it from there.

    class; Movie < OpenStruct; end

    But, of course you knew about OpenStruct (you've blogged about it before).
    So, I guess I am not clear on what the advantage is here.

    -- Mike Berrow

    ReplyDelete
  3. Mike,

    OpenStruct, in my opinion, is the coolest class that should never be used.

    My main gripe is that any typo while using an OpenStruct will return nil instead of a NoMethodError. Those bugs are usually hard to find.

    Cheers, Jay

    ReplyDelete
  4. Jay, so it the typo returning nil your main reason for rolling your own? Is there a better reason not to use OpenStruct?

    ReplyDelete
  5. Ben,

    Yes, typos are the main reason that I do not use OpenStruct objects.

    Cheers, Jay

    ReplyDelete
  6. James5:23 AM

    I am new to Ruby
    I experimpented with the Hash to_mod example

    I wanted to ask if there is any reason not to self.class.send(:define_method ..

    class Movie
    def initialize(options)
    options.each_pair {|k,v|self.class.send(:define_method, k, Proc.new {v})}
    end
    end

    Thanks

    ReplyDelete
  7. James,

    If you defined methods in the way that you described they would be defined on all instances of Movie, not just the instance that was being constructed. If that's okay for what you are doing, then that implementation is fine.

    Cheers, Jay

    ReplyDelete
  8. James4:58 AM

    Thanks Jay!

    ReplyDelete
  9. I've been bitten by a typo when using OpenStruct and it wasn't a fun experience :)

    In this particular case we opted to this in lieu of OpenStruct:

    def initialize(params)
    singleton_class = (class << self; self; end)
    params.each do |key, value|
    singleton_class.module_eval do
    define_method(key) { value } unless method_defined? key
    define_method("#{key}=") { |new_value| params[key] = new_value } unless method_defined? "#{key}="
    end
    end
    end

    ReplyDelete
  10. Thanks for turning us onto OpenStruct but it has completely bitten us in the ass. OpenStruct Crashed My Mongrel

    ReplyDelete
  11. Hey Jay,

    wouldn't this

    class OpenStruct
    def method_missing(method)
    raise NoMethodError
    end
    end

    address your issue with using OpenStruct?

    Cheers,
    Your Italian Mate for a Glass of White Wine in Berlin

    ReplyDelete
  12. Marco,
    Yeah, it looks like that would work fine.
    Cheers, Jay

    ReplyDelete
  13. Re: openstruct method_missing

    implementing method_missing prevents you from adding any attributes after instantiation

    ReplyDelete
  14. Bit late to comment, but if you are concerned about the crazy flexibility of OpenStruct, why not use plain "Struct"? It too is in the standard library and allows you to create an attribute-based model with little work.

    ReplyDelete

Note: Only a member of this blog may post a comment.