Heading image for post: Solving Ara Howard's Metakoans

Ruby

Solving Ara Howard's Metakoans

Profile picture of Nick Palaniuk

Taking a look at Ruby Quiz #67

Inspiration for this post comes from an old ruby quiz that I recently came upon. For more info check here. A test suite is provided describing the behavior of an attribute method and the challenge is to get the provided tests passing. There is a solution write-up already there, but I thought I'd write up my own summary of steps taken to solve this. This really boils down to an exercise in keeping track of self. From the intro:

metakoans.rb is an arduous set of exercises designed to stretch meta-programming muscle. the focus is on a single method 'attribute' which behaves much like the built-in 'attr', but whose properties require delving deep into the depths of meta-ruby. usage of the 'attribute' method follows the general form of

class C
  attribute 'a'
end

o = C::new
o.a = 42  # setter - sets @a
o.a       # getter - gets @a
o.a?      # query  - true if @a

The following will be a fairly quick walkthrough of my solution.

Our test descriptions (taken from the quiz spec):

  1. 'attribute' must provide getter, setter, and query to instances
  2. 'attribute' must provide getter, setter, and query to classes
  3. 'attribute' must provide getter, setter, and query to modules at module level
  4. 'attribute' must provide getter, setter, and query to modules which operate correctly when they are included by or extend objects
  5. 'attribute' must provide getter, setter, and query to singleton objects
  6. 'attribute' must provide a method for providing a default value as hash
  7. 'attribute' must provide a method for providing a default value as block which is evaluated at instance level
  8. 'attribute' must provide inheritance of default values at both class and instance levels
  9. into the void

I'm going to define this new attribute method in a module called Attribute and include it in Module. Since Module is the superclass of Class,

[5] pry(main)> Class.ancestors.include?(Module)
=> true

we will have access to the attribute method in classes or modules.

I'm going to define #attribute on the instance level in the module which will create the ::attribute method class level after Attribute is included in Module.

module Attribute
  def attribute(name, &block)

  end
end

Module.class_eval do
  include Attribute
end

A quick sidebar to help show this behavior. Below Foo is actually an instance of class Module so Foo responds to the defined instance method #baz in class Module

class Module
  def baz; end
end

module Foo; end

[1] pry(main)> Foo.respond_to?(:baz)
=> true

Next we will define getter, setter, and query methods for the instance variable based on the first argument to ::attribute

module Attribute
  def attribute(name, &block)
    define_method "#{name}" do
      instance_variable_get(:"@#{name}")
    end

    define_method "#{name}=" do |value|
      instance_variable_set(:"@#{name}", value)
    end

    define_method "#{name}?" do
      instance_variable_defined?(:"@#{name}")
    end
  end
end

Module.class_eval do
  include Attribute
end

This will actually make the first 5 tests pass now. For the sake of brevity, I won't show all of the code from these passing tests, but I'll use pry to show that ::attribute will exist in the call chain in a couple of these different contexts.

# 'attribute' must provide getter, setter, and query to instances
c = Class.new {
  require 'pry'; binding.pry;
  attribute 'a'
}

[1] pry(#<Class>)> self.respond_to?(:attribute)
=> true

# 'attribute' must provide getter, setter, and query to classes
c = Class.new {
  class << self
    require 'pry'; binding.pry;
    attribute 'a'
  end
}

[1] pry(#<Class>)> self.respond_to?(:attribute)
=> true

# 'attribute' must provide getter, setter, and query to modules at module level
m = Module.new {
  class << self
    require 'pry'; binding.pry;
    attribute 'a'
  end
}

[1] pry(#<Class>)> self.respond_to?(:attribute)
=> true

To pass the 6th test we need to allow ::attribute to receive a hash to set a default value and we need to return false if a defined instance variable is set to nil

def koan_6
  c = Class::new {
    attribute 'a' => 42
  }

  o = c::new

  assert{ o.a == 42 }
  assert{ o.a? }
  assert{ (o.a = nil) == nil }
  assert{ not o.a? }
end

We add a conditional to check for value and set the instance variable if it exists and add a guard clause to the query method for nil

module Attribute
  def attribute(name, &block)
    if name.is_a?(Hash)
      name, value = name.first
    end

    define_method "#{name}" do
      if value    
        instance_variable_set(:"@#{name}", value)
      end
      instance_variable_get(:"@#{name}")
    end

    define_method "#{name}=" do |value|
      instance_variable_set(:"@#{name}", value)
    end

    define_method "#{name}?" do
      return false if instance_variable_get(:"@#{name}").nil?
      instance_variable_defined?(:"@#{name}")
    end
  end
end

Module.class_eval do
  include Attribute
end

Being able to use a block to set a default value will get us past tests 7 and 8.

def koan_7
  c = Class::new {
    attribute('a'){ fortytwo }
    def fortytwo
      42
    end
  }

  o = c::new

  assert{ o.a == 42 }
  assert{ o.a? }
  assert{ (o.a = nil) == nil }
  assert{ not o.a? }
end

def koan_8
  b = Class::new {
    class << self
      attribute 'a' => 42
      attribute('b'){ a }
    end
    attribute 'a' => 42
    attribute('b'){ a }
  }

  c = Class::new b

  assert{ c.a == 42 }
  assert{ c.a? }
  assert{ (c.a = nil) == nil }
  assert{ not c.a? }

  o = c::new

  assert{ o.a == 42 }
  assert{ o.a? }
  assert{ (o.a = nil) == nil }
  assert{ not o.a? }
end

And the code:

module Attribute
  def attribute(name, &block)
    if name.is_a?(Hash)
      name, value = name.first
    end

    define_method "#{name}" do
      if value    
        instance_variable_set(:"@#{name}", value)
      elsif block
        instance_variable_set(:"@#{name}", instance_eval(&block))
      end
      instance_variable_get(:"@#{name}")
    end

    define_method "#{name}=" do |value|
      instance_variable_set(:"@#{name}", value)
    end

    define_method "#{name}?" do
      return false if instance_variable_get(:"@#{name}").nil?
      instance_variable_defined?(:"@#{name}")
    end
  end
end

Module.class_eval do
  include Attribute
end

To get the final test to pass we need check if the instance variable is already defined in the getter.

def koan_9
  b = Class::new {
    class << self
      attribute 'a' => 42
      attribute('b'){ a }
    end
    include Module::new {
      attribute 'a' => 42
      attribute('b'){ a }
    }
  }

  c = Class::new b

  assert{ c.a == 42 }
  assert{ c.a? }
  assert{ c.a = 'forty-two' }
  assert{ c.a == 'forty-two' }
  assert{ b.a == 42 }

  o = c::new

  assert{ o.a == 42 }
  assert{ o.a? }
  assert{ (o.a = nil) == nil }
  assert{ not o.a? }
end
module Attribute
  def attribute(name, &block)
    if name.is_a?(Hash)
      name, value = name.first
    end

    define_method "#{name}" do
      unless instance_variable_defined?(:"@#{name}")
        if value    
          instance_variable_set(:"@#{name}", value)
        elsif block
          instance_variable_set(:"@#{name}", instance_eval(&block))
        end
      end
      instance_variable_get(:"@#{name}")
    end

    define_method "#{name}=" do |value|
      instance_variable_set(:"@#{name}", value)
    end

    define_method "#{name}?" do
      return false if instance_variable_get(:"@#{name}").nil?
      instance_variable_defined?(:"@#{name}")
    end
  end
end

Module.class_eval do
  include Attribute
end

Now with all our tests passing we can do some refactoring and pull out the instance variable logic into a new method and get a final solution.

class Module
  def attribute(name, &block)
    name, value = name.first if name.is_a?(Hash)
    create_attribute(name, value, &block)
  end

  def create_attribute(name, value = nil, &block)
    define_method("#{name}") do
      unless instance_variable_defined?(:"@#{name}")
        instance_variable_set(:"@#{name}", block_given?? instance_eval(&block) : value)
      end
      instance_variable_get(:"@#{name}")
    end

    define_method("#{name}=") do |value| 
      instance_variable_set(:"@#{name}", value)
    end

    define_method("#{name}?") do
      !!instance_variable_get(:"@#{name}")
    end
  end
end
~/hashrocket/projects/ruby $ ruby metakoans.rb knowledge.rb
koan_1 has expanded your awareness
koan_2 has expanded your awareness
koan_3 has expanded your awareness
koan_4 has expanded your awareness
koan_5 has expanded your awareness
koan_6 has expanded your awareness
koan_7 has expanded your awareness
koan_8 has expanded your awareness
koan_9 has expanded your awareness
mountains are again merely mountains

More posts about Ruby

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project