[SOLUTION] metakoans.rb (#67)

I've got two.

Here's the first one I did:

class Module
   def attribute( name, &default_block )
     name, default_value = name.to_a.first if name.is_a? Hash
     default_block ||= proc { default_value }

     ivar = "@#{ name }".to_sym

     define_method name do
       value = instance_variable_get ivar
       unless value or instance_variables.include? ivar.to_s
         instance_variable_set ivar, instance_eval( &default_block )
       else
         value
       end
     end

     define_method "#{ name }=" do |value|
       instance_variable_set ivar, value
     end

     alias_method "#{ name }?", name
   end
end

23 lines.

Then here's the second one I did, after Ara clarified that I needn't use
instance variables:

class Module
   def attribute( name, &default_block )
     name, default_value = name.to_a.first if name.is_a? Hash
     default_block ||= proc { default_value }

     name = name.to_sym

     define_method( "__default__#{ name }", &default_block )

     define_method( :__attributes__ ) do
       @__attributes__ ||= Hash.new { |h, k| h[k] = send "__default__#{ k }" }
     end

     define_method( name ) { __attributes__[name] }
     define_method( "#{ name }=" ) { |value| __attributes__[name] = value }
     alias_method "#{ name }?", name
   end
end

18 lines.

Actually, this last one can be mildly golfed down to 13 lines, at the
cost of getting a little ugly and a little slow:

class Module
   def attribute( name, &default_block )
     name, default_value = name.to_a.first if name.is_a? Hash
     default_block ||= proc { default_value }
     define_method( "__default__#{ name }", &default_block )
     define_method( :__attributes__ ) do
       @__attributes__ ||= Hash.new { |h, k| h[k] = send "__default__#{ k }" }
     end
     define_method( name ) { __attributes__[name] }
     define_method( "#{ name }=" ) { |value| __attributes__[name] = value }
     alias_method "#{ name }?", name
   end
end

Anyway, that's all. Both these versions only do one attribute at a
time, since that's all the tests asked for. Plus I don't think the
block would make sense for multiple attributes.

And Ara? This Quiz rocked so much. Thank you!

-mental

Here's my solution: Took me about 15 minutes to solve the koans. I agree with everyone else: Great quiz =). Continuous fiddling with it made me late for a date, and when I tried explaining why, she just stared at me. "Meta cones? What's that?" Ah well!

My koan solution, 9 lines:

def one_attribute(arg,&b)
   name = (arg.is_a?(Hash) ? arg.keys[0] : arg).to_s
   p = Hash.new( arg.is_a?(Hash) ? arg[name] : nil )
   fun = lambda { |*args|
     p[self] = *args unless args.empty?
     (p.include?(self) or !b) ? p[self] : instance_eval(&b)
   }
   ['','?','='].each { |ch| define_method(name+ch,&fun) }
end

And to make it accept multiple arguments to work with the benchmark suites (10 lines):

def attribute(arg,&b)
   attribute(*rest,&b) if rest.any?
   name = (arg.is_a?(Hash) ? arg.keys[0] : arg).to_s
   p = Hash.new( arg.is_a?(Hash) ? arg[name] : nil )
   fun = lambda { |*args|
     p[self] = *args unless args.empty?
     (p.include?(self) or !b) ? p[self] : instance_eval(&b)
   }
   ['','?','='].each { |ch| define_method(name+ch,&fun) }
end

With comments:

def attribute(arg,&b)
   # Allow multiple attributes to be set and defined.
   attribute(*rest,&b) if rest.any?

   # The name of the attribute.
   name = (arg.is_a?(Hash) ? arg.keys[0] : arg).to_s

   # p holds all the attributes for each object.
   # This is wasteful since in real use, this would cause
   # Each object that's set an attribute to be kept in
   # memory.
   p = Hash.new( arg.is_a?(Hash) ? arg[name] : nil )

   # The only method I define: It takes 1 or more arguments.
   # If it's given an argument, it assigns. In all cases,
   # It returns.
   fun = lambda { |*args|
     # Assign if necessary.
     p[self] = *args unless args.empty?
     # If it's been assigned, or there's no block, return
     # Its saved value (Or Hash's default)
     (p.include?(self) or !b) ? p[self] : instance_eval(&b)
   }

   # Assign that method to all 3 methods we need.
   ['','?','='].each { |ch| define_method(name+ch,&fun) }
end

More quizzes like this would be much appreciated. It rocked.

Ara++

- Greg

The only interesting thing I have to say here that hasn't been said is that this test seems like a variation of Pair Programming, where one person writes the tests and the other writes the code. No better way to ensure you only write code to make a failing test pass than to know nothing about the problem space except the most recent test failure.

class Object
  def attribute(arg, &block)
    name = (arg.class == Hash ? arg.keys[0] : arg)
    define_method(name) do
      first_access_action(arg,&block) unless instance_eval("defined? " + "@" + name)
      instance_variable_get "@" + name
    end
    attr_writer name
    alias_method name+"?",name
  end

  def first_access_action(arg,&block)
    name = (arg.class == Hash ? arg.keys[0] : arg) send(name+"=",instance_eval(&block)) if block_given?
    send(name+"=",arg.values[0]) if arg.class == Hash
  end
end

knowledge.rb (523 Bytes)

As with everyone, it seems, some more twiddling and playing with this yielded me a better, faster, and smaller solution.

This also beats my prior solution in that a? returns only true or false, and variables are kept with the object rather than in a hash with the object as a key. The reader methods also won't take an argument now. (They did before.)

9 lines with a little golfing: (8 if you ditch the line for multiple definitions, which isn't needed for passing the koans).

It's pretty much the same as most of the other solutions out there, now.

def attribute(arg,*rest,&b)
   attribute(*rest,&b) if rest.any? # Allow multiple definitions.
   n = (arg.is_a?(Hash) ? arg.keys[0] : arg).to_s
   b ||= lambda { arg.is_a?(Hash) ? arg[n] : nil }
   attr_writer n
   define_method(n) { instance_variables.include?('@'+n) ? \
       instance_variable_get('@'+n) : instance_eval(&b) }
   define_method(n+'?') { ! [nil,false].include?(send(n)) }
end

Blast it, Ara, how do I stop myself from doing more and more tweaking? :wink:

- Greg

Here's a better version of my solution. Obviously these modules would have to be included in Object to work. The names should be self explainitory. All lines are less than 80 chars.

module Attribute20PercentGolfed
  def attribute(arg, &b)
    return attribute(arg.keys[0]) { arg.values[0] } if arg.class == Hash
    define_method(arg) do
      init = block_given? && !instance_eval("defined? "+"@"+arg)
      init ? send(arg+"=",instance_eval(&b)) : instance_variable_get("@"+arg)
    end
    define_method(arg+"?") { !!send(arg) }
    attr_writer(arg)
  end
end

module Attribute75PercentGolfed; def attribute(a, &b)
  return attribute(a.keys[0]) { a.values[0] } if a.class == Hash
  define_method(a) { block_given? && !instance_eval("defined? "+"@"+a) ?
    send(a+"=",instance_eval(&b)):instance_variable_get("@"+a) }
  define_method(a+"?") { !!send(a) } && attr_writer(a); end; end

knowledge.rb (704 Bytes)

I came up with a solution that involved creating a default initializer
method for each attribute set. Great quiz!

class Object
  def attribute(arg, &block)
    # Determine if we have an initial value for the attribute or not, and
put
    # them all into a common form.
    if arg.is_a?(Hash)
      props = arg.collect { |k, v| k }
    else
      # Note this sets the initial value to nil, which is a no-op below, if
      # no block was specified.
      props = [arg]
      arg = { arg => block }
    end

    props.each do |p|
      instance_var, init_meth = "@#{p}".to_sym, "#{p}_init".to_sym

      if (val = arg[p])
        # set up initializer methods for block or given value. Note a
        # method is created for each attribute given that has a value
associated
        self.instance_eval do
          if val.is_a?(Proc)
            define_method init_meth, val
          else
            define_method(init_meth) { val }
          end
          # set visibility
          private init_meth
        end
      end

      # define attribute accessor methods
      class_eval do
        attr_writer p.to_sym

        # for first time access, look to appropriate init method, if any and
        # get value. In either case, the instance_variable will be defined
after
        # this method if it wasn't before.
        define_method(p.to_sym) do
          unless x = instance_variable_get(instance_var) || val.nil?
            instance_variable_set(instance_var, x = self.send(init_meth))
          end
          x
        end

        # Define query accessor. Only returns true if the instance variable
is defined,
        # regardless of its value.
        define_method("#{p}?") do
          ! instance_variable_get(instance_var).nil?
        end
      end
    end
  end
end

···

On 2/20/06, Greg Millam <ruby-talk@lethalcode.net> wrote:

As with everyone, it seems, some more twiddling and playing with this
yielded me a better, faster, and smaller solution.

This also beats my prior solution in that a? returns only true or false,
and variables are kept with the object rather than in a hash with the
object as a key. The reader methods also won't take an argument now.
(They did before.)

9 lines with a little golfing: (8 if you ditch the line for multiple
definitions, which isn't needed for passing the koans).

It's pretty much the same as most of the other solutions out there, now.

def attribute(arg,*rest,&b)
   attribute(*rest,&b) if rest.any? # Allow multiple definitions.
   n = (arg.is_a?(Hash) ? arg.keys[0] : arg).to_s
   b ||= lambda { arg.is_a?(Hash) ? arg[n] : nil }
   attr_writer n
   define_method(n) { instance_variables.include?('@'+n) ? \
       instance_variable_get('@'+n) : instance_eval(&b) }
   define_method(n+'?') { ! [nil,false].include?(send(n)) }
end

Blast it, Ara, how do I stop myself from doing more and more tweaking? :wink:

- Greg