Wednesday, December 16, 2009

A Simple Ruby Pattern

By being a multi-paradigm language, Ruby provides numerous possible styles to write your programs with. I am a big fan of metaprogramming in Ruby, and one style strikes my fancy especially. By defining methods in your Class instances, your subclasses start to act like a domain specific language. You end up setting up a structural framework for how your subclasses behave, then declare the behaviors specific to each specific subclass.

To illustrate this, consider a simple calculator application. You could tackle such an application any number of ways, but we can come up with an interesting solution using class methods.

For starters, we need our base CalculatorBase class, which will provide the plumbing for dealing with input, handling if the input is numeric versus operations, and overall flow. I am not interested in these details for this post, so I will leave them as a homework problem for you to play with. Don't worry, it's fun!

First, your base calculator needs to expose a way to define operations:

class CalculatorBase
def self.operation(op, arity, &block)
define_method(op.to_sym) { |*args|
unless args.size == arity
raise "Wrong number of arguments"
end
block.call *args
}
end
end


From this, you can declaratively define what operations your calculator may support:

class Calculator < CalculatorBase
operation(:+, 2) { |x, y| x + y }
operation(:-, 2) { |x, y| x - y }
operation(:/, 2) { |x, y| x / y }
operation(:*, 2) { |x, y| x * y }
operation(:sin, 1) { |x| Math.sin x }
operation(:cos, 1) { |x| Math.cos x }
operation(:tan, 1) { |x| Math.tan x }
end


Now, you can invoke operations with ease:

calc = Calculator.new
calc.+ 2, 3
calc.sin 3.14159


However, I feel we can do better than this. There is a lot of repetition going on with the arity. To make our calculator implementation more domain specific, let's allow binary and unary operators to be defined easier:

class CalculatorBase
def self.operation(op, arity, &block)
define_method(op.to_sym) { |*args|
unless args.size == arity
raise "Wrong number of arguments"
end
block.call *args
}
end

def self.binary(op, &block)
operation op, 2, &block
end

def self.unary(op, &block)
operation op, 1, &block
end
end


With these new methods, we can make our operation declarations a bit easier:

class Calculator < CalculatorBase
binary(:+) { |x, y| x + y }
binary(:-) { |x, y| x - y }
binary(:/) { |x, y| x / y }
binary(:*) { |x, y| x * y }
unary(:sin) { |x| Math.sin x }
unary(:cos) { |x| Math.cos x }
unary(:tan) { |x| Math.tan x }
end


This isn't a particularly difficult approach in Ruby, but I really like the results whenever I am able to employ it. It can remove a lot of repetitive method declaration, and make your main class very clearly declare the behaviors it employs.

Of course, with great power comes great responsibility. It is easy to obfuscate the behavior you are creating with this pattern. Used appropriately, you can design an API and then speak the language of your domain in a much easier to comprehend fashion.

No comments: