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
Post a Comment