Practical Common Ruby - Do you really need macros?

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...

Posted in Technology | Tags Macros, Ruby, Lisp

Comments Disabled