When implementing a generic processing method that needs to handle a special case (for example, in a parser), use blocks to elegantly handle these cases instead of writing the code inline.
Applicability
This mini-pattern is specific to Ruby, and while my example deals with parsing text, the technique could be useful in other situations.
Motivation
Recently I needed to create Ruby objects based on output from a command-line program. The output was generally well-structured, but not completely. As a result of these here-and-there inconsistencies, my parser turned into a real mess. Here’s a simplified version of the text to be parsed:
# Text describing a "Foo" object
Length : 20
Width : 10
# Text describing a "Bar" object
Status : OK
Widget Notifier : ON
Channel-Device : 0,1
And here’s the classes I want to create from that text:
# Easy case
class Foo
attr_accessor :length, :width
end
# Little bit harder
class Bar
attr_accessor :status, :notifier, :channel, :device
end
Foo is the easiest possible case; you simply take each line and split around the “ : “. The left side matches the attribute name, the right side matches its value. But what about Bar? Two issues complicate things: the names don’t match right, and one line has two attributes. What to do now?
Implementation
The best way to handle mapping text names to attribute names is with a hash. Sometimes you can get away with applying transforms to the text–in the Foo example, just downcase the name–but in real life you usually have too many exceptions to the rule. The hash for Foo would look like this:
# Map Foo text keys => attributes
map = {
"Length" => :length=,
'Width' => :width=
}
# (This code is in a separate parser class...)
# Parser can now assign key/val pair to
# attribute like so:
foo_instance.send(map[key], val)
The last line calls the attribute setter for the key (length, width), and passes along the value.
Now for Bar. Rather than map “Channel-Device” to a symbol, map it to a block instead:
# Map Bar text keys => attributes
map = {
"Status" => :status=,
"Widget Notifier" => :notifier=,
"Channel-Device" => lambda do |bar, text|
channel, device = text.split(/,/)
bar.channel = channel
bar.device = device
end
# Parser now checks for mapping type
case (map[key])
when Proc: map[key].call(item, val) # <--- Call the proc
when Symbol: item.send(map[key], val)
end
The lambda creates an object of class Proc, and when the parser sees a Proc for a key, it just calls the Proc. This lets the parser remain highly generic, and cleanly packages the special-case handling in our key/attribute map.
Credit
The idea for this technique was based on a discussion with Chad Fowler and Ara Howard at the Boulder/Denver Ruby User’s Group. While this isn’t rocket science – in fact it’s pretty common stuff for Ruby and other languages with closures – it’s a handy trick which is worth documenting.
Full Source Code
The full example file is presented below.
sample_foo_text = <<endtext
Length : 20
Width : 10
endtext
sample_bar_text = <<endtxt
Status : OK
Widget Notifier : ON
Channel-Device : 0,1
endtext
class Foo
attr_accessor :length, :width
def self.create_from_text(text)
map = {
"Length" => :length=,
'Width' => :width=
}
Parser.create_from_text(Foo, map, text)
end
end
class Bar
attr_accessor :status, :notifier, :channel, :device
def self.create_from_text(text)
map = {
"Status" => :status=,
"Widget Notifier" => :notifier=,
"Channel-Device" => lambda do |bar, text|
channel, device = text.split(/,/)
bar.channel = channel
bar.device = device
end
}
Parser.create_from_text(Bar, map, text)
end
end
# One parser class can create several Foo/Bar/etc.
# classes given a class and text.
#
class Parser
def self.create_from_text(item_class, map, text)
item = item_class.new
text.each_line do |line|
key, val = line.split(/:/)
key.strip!
val.strip!
case (map[key])
when Proc: map[key].call(item, val)
when Symbol: item.send(map[key], val)
end
end
item
end
end
foo = Foo.create_from_text(sample_foo_text)
puts "Foo:"
puts foo.length
puts foo.width
bar = Bar.create_from_text(sample_bar_text)
puts "Bar:"
puts bar.status
puts bar.notifier
puts bar.channel
puts bar.device