Practical Common Ruby - Do you really need macros?
11:16 PM EST Monday, November 26 2007
Chapter 9 of Practical Common Lisp has us diving deeper into Lisp macros by creating a test framework, which I will of course, try to port to Ruby. The more I continue to work on this, the more I'm convinced this is a great way to learn a new language.
First off, you are forced to digest the examples that you convert, because you have to think about what the code is doing and how you would do it in the language that you know. The bonus payoff is that you now have code in both languages to compare, so you can evaluate the two languages side-by-side. So on with the code.
Right off bat we run into a small problem, which is that you can't have "+" or "*" characters in the name of a method in Ruby. That's ok, we can work around that:
def test_plus
1 + 2 == 3 &&
1 + 2 + 3 == 6 &&
-1 + -3 == -4
end
Which works:
paulbarry@paulbarry: ~/projects/practical_common_ruby $ irb
irb(main):001:0> load 'chap9.rb'
=> true
irb(main):002:0> test_plus
=> true
Next we tackle the version with test case info:
def test_plus
puts "#{eval("1 + 2 == 3") ? 'pass' : 'FAIL'} ... 1 + 2 == 3"
puts "#{eval("1 + 2 + 3 == 6") ? 'pass' : 'FAIL'} ... 1 + 2 + 3 == 6"
puts "#{eval("-1 + -3 == -4") ? 'pass' : 'FAIL'} ... -1 + -3 == -4"
end
Now a little refactoring by adding report_result:
def report_result(result, form)
puts "#{result ? 'pass' : 'FAIL'} ... #{form}"
end
def test_plus
report_result(1 + 2 == 3, "1 + 2 == 3")
report_result(1 + 2 + 3 == 6, "1 + 2 + 3 == 6")
report_result(-1 + -3 == -4, "-1 + -3 == -4")
end
This isn't really very clean, first of all because of the obvious duplication of the actual code and the name, but secondly, we have our code as an argument to the report_result method. This works in lisp, because everything is just an expression, but in Ruby, once this method gets more complicated, this is going to be ugly. So I'm going to refactor report_result to allow for two conditions. The first just takes the code as a string and evals it, using the code as the "name" of the test. Second, the string is the "name" and the block has the code. I think this makes sense, because even in lisp, if the code is some long, multiline expression, you aren't going to want that to be the name of the test, you are going to want to supply a shorter, more descriptive name.
def report_result(test)
if block_given?
result = yield
else
result = eval(test)
end
puts "#{result ? 'pass' : 'FAIL'} ... #{test}"
end
def test_plus
report_result "1 + 2 == 3"
report_result "1 + 2 + 3 == 6"
report_result "adding negative numbers" do
-1 + -3 == -4
end
end
The next refactoring is to define a check method that takes multiple "tests" and prints the results of each one, mainly because the repeated calls to report_result is considered unappealing. I'm not sure there is a particularly cleaner version of this in Ruby, but here's the best I've got:
def report_result(test)
if block_given?
result = yield
else
result = eval(test)
end
puts "#{result ? 'pass' : 'FAIL'} ... #{test}"
end
def check(*tests)
tests.each do |t|
if t.is_a?(Hash)
report_result t.keys.first, &t.values.first
else
report_result t
end
end
end
def test_plus
check(
"1 + 2 == 3",
"1 + 2 + 3 == 6",
"adding negative numbers" => lambda{ -1 + -3 == -4 }
)
end
I found this tutorial on Ruby's Procs and Blocks to be helpful while working on this. In reality, I think most Ruby programmers would stick to the original version of test_plus over this version that uses Hash and Lambdas, but I'll stick with this one because it's closer to the actual code in the example. A couple of small changes give us the ability to track if the test has a failure:
def report_result(test)
if block_given?
result = yield
else
result = eval(test)
end
puts "#{result ? 'pass' : 'FAIL'} ... #{test}"
result
end
module Enumerable
def each?
result = true
each do |i|
result = false unless yield(i)
end
result
end
end
def check(*tests)
tests.each? do |t|
if t.is_a?(Hash)
report_result(t.keys.first, &t.values.first)
else
report_result(t)
end
end
end
I choose to implement the combine_results function from the example as an iterator called each? mixed-in to Enumerable, so that we can call it in the idiomatic Ruby way tests.each?. each? is similar to the all? method that already exists in Enumerable, except that all? short-circuits and stops iterating through over the items once the block evaluates to false. We want to always perform the block on each item in the enumerable, and then return false if any are false. I choose to name it each? rather than combine_results because it's more descriptive, more Rubyish. I'm actually kind of surprised Ruby doesn't have a method like that.
Now this next bit require dynamic variables. This is a new concept for me and I have to say, it is pretty cool. The bad news? Ruby doesn't have dynamic variables :(. The good news? We can fake it. Just download this code and we are ready to go. We add in a line to define the variable:
Dynamic.variable :test_name
Then we add it into our test method. We also have to do a little finagling to get them to still return the right value:
def test_plus
result = false
Dynamic.let :test_name => 'test_plus' do
result = check(
"1 + 2 == 3",
"1 + 2 + 33 == 6",
"adding negative numbers" => lambda{ -1 + -3 == -4 }
)
end
result
end
def test_times
result = false
Dynamic.let :test_name => 'test_times' do
result = check(
"2 * 2 == 4",
"3 * 5 == 15"
)
end
result
end
Lastly we just add the dynamic variable to the print statement in report_result:
def report_result(test)
if block_given?
result = yield
else
result = eval(test)
end
puts "#{result ? 'pass' : 'FAIL'} ... #{Dynamic.test_name}: #{test}"
result
end
So now we want to clean up this redundant test code, but alas, Ruby does not have macros. Here we go again, coming pretty close. Define a method we will use to define tests:
def test(name)
test_name = "test_#{name}"
method = lambda do
result = false
Dynamic.let :test_name => "#{Dynamic.test_name} #{test_name}" do
result = yield
end
result
end
Object.send(:define_method, test_name, method)
end
I'm not even going to try to explain how this code works. This is pretty dense. I'm not sure if it is more or less dense than the Lisp code. Now that we have our Ruby "macro" created, we can define our tests like this:
test "plus" do
check(
"1 + 2 == 3",
"1 + 2 + 3 == 6",
"adding negative numbers" => lambda{ -1 + -3 == -4 }
)
end
test "times" do
check(
"2 * 2 == 4",
"3 * 5 == 15"
)
end
test "arithmetic" do
%w{test_plus test_times}.each? do |t|
send t
end
end
test "math" do
test_arithmetic
end
And voila, we have a feature complete version of the test framework in Ruby:
irb(main):096:0> test_math
pass ... test_math test_arithmetic test_plus: 1 + 2 == 3
pass ... test_math test_arithmetic test_plus: 1 + 2 + 3 == 6
pass ... test_math test_arithmetic test_plus: adding negative numbers
pass ... test_math test_arithmetic test_times: 2 * 2 == 4
pass ... test_math test_arithmetic test_times: 3 * 5 == 15
=> true
irb(main):097:0> test_arithmetic
pass ... test_arithmetic test_plus: 1 + 2 == 3
pass ... test_arithmetic test_plus: 1 + 2 + 3 == 6
pass ... test_arithmetic test_plus: adding negative numbers
pass ... test_arithmetic test_times: 2 * 2 == 4
pass ... test_arithmetic test_times: 3 * 5 == 15
=> true
irb(main):098:0> test_plus
pass ... test_plus: 1 + 2 == 3
pass ... test_plus: 1 + 2 + 3 == 6
pass ... test_plus: adding negative numbers
=> true
So in conclusion, macros are one feature of Lisp that have no direct equivalent in Ruby, but you can define methods that define methods, which is pretty close to what macros do. Given Ruby's metaprogramming features like eval, send, and define_method, etc., Do you really need macros?. The search for the answer continues...