Problems with the metakoans.rb Ruby Quiz

Mon, 05 Feb 2007 15:25:00 -0800
ruby, training, metaprogramming

There are indeed many ways to solve Ruby Quiz #67: metakoans.rb, as James Gray II says. By "solve," I mean getting all of the "koans" to pass. But you don't have to actually solve the quiz to make all of the koans pass. Here's a solution that passes all of the koans, but doesn't solve the problem completely:

class Module
  def attribute(params, &block)
    initial = nil
    if params.is_a?(Hash)
      name = params.keys[0]
      initial = params.values[0]
    else
      name = params
    end

    define_attribute_methods(name, initial, &block)
  end

  private

  def define_attribute_methods(name, initial, &block)
    define_method(name) do
      initial ||= instance_eval &block if block_given?

      @attr ||= initial
    end
    define_method(name + '=') do |value|
      @attr = value
    end
    define_method(name + '?') do
      !@attr.nil?
    end
  end
end

While I was solving the solution, there were a number of times when all of the koans passed, but I had a sense that my solution wasn't correct. Similarly, when I was using this quiz as a Ruby teaching tool, a number of people told me that they solved the quiz, but don't know why the code passed all of the koans. For a self-testing quiz, this is a problem. Don't get me wrong, this quiz is awesome, but it'd be better if it had more thorough assertions to ensure that the "student" has correct "knowledge".

The first change that I made to the assertions was to change assertions like:

assert { (c.a = nil) == nil }

to

assert { c.a = nil; c.a == nil }

While these two assertions are similar, they are not the same, and the difference is subtle. The first assertion tests that the return value of the c.a= method is nil. The second assertion tests that the return value of the c.a method is nil. This is, I think, what the metakoans.rb author intended. Without using the second assertion, the return value of the c.a method could be incorrect after the internal attribute variable has changed via a call to c.a=.

The second change that I made was to make it so that, rather than using the number 44 as the default value for the 'a' attribute in every koan, I incremented the number by one. For koans that had a second attribute that took a block as a default value, I had them return a + 1 instead of simply a. This is important, because without this change, a single instance variable, such as the one I use in the erroneous solution above, could be used as the value for all attributes, and the koans would still pass.

You can get my metakoans.rb with the updated assertions here.

I'm not sure if there are other ways to make the koans pass without the solution being correct. If there are, please let me know, and I'll update the file.