Ruby
Solving Ara Howard's Metakoans
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):
- 'attribute' must provide getter, setter, and query to instances
- 'attribute' must provide getter, setter, and query to classes
- 'attribute' must provide getter, setter, and query to modules at module level
- 'attribute' must provide getter, setter, and query to modules which operate correctly when they are included by or extend objects
- 'attribute' must provide getter, setter, and query to singleton objects
- 'attribute' must provide a method for providing a default value as hash
- 'attribute' must provide a method for providing a default value as block which is evaluated at instance level
- 'attribute' must provide inheritance of default values at both class and instance levels
- 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