[SUMMARY] QAPrototype (#91)

While this quiz is based on the fun idea of an interactive Ruby conversation,
the answers reinforced my belief that it holds practical value in prototyping
Ruby objects. Here's a sample run of me playing with one of the solutions:

  $ irb -r qap.rb -r enumerator
  
  Call #start if you want to get a head start on QAPrototype...
  
  >> class PascalsTriangle; include QAPrototype; end
  => PascalsTriangle
  >> tri = PascalsTriangle.new
  => #<PascalsTriangle:0x33082c>
  >> tri.next
  
  next is undefined.
  Please define what I should do, starting with arguments
  this method should accept (skip and end with newline):
  
  def next
    case row
    when 0 then (@rows = [[1]]).last
    when 1 then (@rows << [1, 1]).last
    else
      ( @rows <<
        [1] + @rows.last.enum_for(:each_cons, 2).map { |l, r| l + r } + [1]
        ).last
    end
    ensure
      @row += 1
    
  end
  
  Okay, I got it.
  
  Calling the method now!
  
  row is undefined.
  Please define what I should do, starting with arguments
  this method should accept (skip and end with newline):
  
  def row
    @row ||= 0
    
  end
  
  Okay, I got it.
  
  Calling the method now!
  
  => [1]
  >> tri.next
  => [1, 1]
  >> tri.next
  => [1, 2, 1]
  >> tri.next
  => [1, 3, 3, 1]
  >> tri.dump
  class PascalsTriangle
    def next
      case row
      when 0 then (@rows = [[1]]).last
      when 1 then (@rows << [1, 1]).last
      else
        ( @rows <<
          [1] + @rows.last.enum_for(:each_cons, 2).map { |l, r| l + r } + [1]
          ).last
      end
      ensure
        @row += 1
    end
    def row
      @row ||= 0
    end
  end
  => nil

Notice how I just used the non-existent row() method in my definition of next().
I knew I would get a chance to fill it in when it was needed. Building up a
class this way lets you work from the high level down, coding as you go. It's
an interesting new way to think about programming. I encourage everyone to play
with it a little and decide what you think.

The code for this is not hard to implement. Here's a trivial solution by Erik
Veenstra:

  module QAPrototype
    def method_missing(method_name)
      puts "#{method_name} is undefined"
      puts "Please define what I should do (end with a newline):"
  
      (code ||= "") << (line = $stdin.gets) until line and line.chomp.empty?
  
      self.class.module_eval do
        define_method(method_name){eval code}
      end
  
      at_exit do
        puts ""
        puts "class #{self.class}"
        puts " def #{method_name}"
        code.gsub(/[\r\n]+$/, "").split(/\r*\n/).each{|s| puts " "*4+s}
        puts " end"
        puts "end"
      end
    end
  end

This code defines a QAPrototype module you can mix into any class you wish to
interactively add methods to. The only method in it currently is
method_missing(), which Ruby calls whenever an unknown method is called. This
method prompts you for a method body, gathers some code, defines a new method on
the current class that uses the given code, and finally arranges to have the
method printed when IRb exits. That literally covers everything asked for in
the quiz, including my editor's note.

A limitation of the above code is that it doesn't deal with method arguments.
That means you can only use it to create and call unparameterized methods.
Another element that made many of the solutions interesting is the extra methods
they defined to further evolve the interaction process.

Let's look at another solution that handles arguments and adds some nice extras.
Here's the beginning of the code used in the opening example of this summary, by
Matt Todd:

  module QAPrototype
    
    attr :_methods_added
    
    def method_missing name, *args, &block
      puts "\n#{name} is undefined.\n"
      puts "Please define what I should do, starting with arguments"
      puts "this method should accept (skip and end with newline):\n\n"
      
      # get arguments
      print "def #{name} "; $stdout.flush; arguments = $stdin.gets
      
      # get method body
      method = ""
      while (print ' '; $stdout.flush; line = $stdin.gets) != "\n"
        method << " " << line
      end
      puts "end\n"
      
      if method == ""
        puts "\nOops: you left the method empty so we didn't add it.\n\n"
        return
      end
      
      puts "\nOkay, I got it.\n\n"
      
      # now define a new method
      self.class.class_eval <<-"end;"
        def #{name} #{arguments}
          #{method}
        end
      end;
      
      # and store the results to the stack for undoes and dumps
      @_methods_added ||= []
      @_methods_added << { :name => name,
                           :arguments => arguments.chomp,
                           :body => method.chomp }
      
      puts "\nCalling the method now!\n\n"
      
      return self.method(name).call(*args, &block)
    end
    
    # ...

This method is not too different from the one we examined earlier. Again the
user is prompted to enter code, but this time the code prompts for an argument
list first. This allows you to specify fixed or variable argument lists, with
or without default values. Once it has the arguments, the method reads the body
of code until you feed it a blank line. If you leave the body blank, the code
skips adding it. Otherwise a quick call to class_eval() installs the method.
Note the clever use of end; to close the heredoc in this code. A Hash of method
details is also added to the @_methods_added Array, so the code can look up
information in later operations. Before this method exits, it triggers the call
you wanted to make in the first place.

Here's a new feature provided by this module:

    # ...
    
    def undo
      the_method = @_methods_added.pop
      
      if the_method.nil?
        puts "\nYou have not interactively defined any methods!\n"
        return
      end
      
      self.class.class_eval { remove_method the_method[:name] }
      
      puts "\n#{the_method[:name]} is now gone from this class.\n\n"
    end
    
    # ...

When called, undo() pulls the last method added back out of its internal list
and removes it from the class. Future calls to the same method will again be
funneled through method_missing() so you can redefine the code.

Here are a few more methods you can use to get information back out of the
object:

    def dump filename = nil
      body = ""
      @_methods_added.each do |method|
        body << <<-"end;"
    def #{method[:name]} #{method[:arguments]}
  #{method[:body]}
    end
  end;
      end
      
      klass = <<-"end;"
  class #{self.class}
  #{body.chomp}
  end
  end;
      
      if !filename.nil?
        File.open(filename, File::CREAT|File::TRUNC|File::RDWR, 0644) do |file|
          if file.write klass
            puts "\nClass was written to #{filename} successfully!\n\n"
          end
        end
      else
        puts klass
      end
    end
      
    def added_methods
      @_methods_added
    end
    
    alias :methods_added :added_methods
    
  end
  
  # ...

The main point of interest here is the dump() method. When called, is builds up
method definitions for all interactively defined methods and wraps those in a
class definition. If you provided a filename with the call, the entire
definition is dumped to the indicated file. Otherwise the code is printed to
STDOUT.

This code has one final interesting feature, a sort of jump-start mode for IRb:

  # ...
  
  class Foo; include QAPrototype; end
  
  puts "\nCall #start if you want to get a head start on QAPrototype...\n\n"
  
  def start
    puts "\nirb(prep):001:0> @f = Foo.new"
    puts "irb(prep):002:0> @f.bar\n\n"
    
    puts "For your convenience in testing, I've created a class called"
    puts "Foo and have already mixed in QAPrototype! Aren't you glad?"
    puts "And while I was at it, I went ahead and created an instance"
    puts "of Foo and put it into @f. Now we can all shout for joy!"
    puts "Heck, we even started up the conversation with method_missing!"
    
    @f = Foo.new
    @f.bar
  end

This code just loads the module into a class and provides a globally available
method to create one of these objects and start the method definition process
with a call to a non-existent method. This is just a nice shortcut for getting
started right away when you are playing with this module.

My thanks to all for their creative development of an exciting new way to
incrementally and interactively develop code. I think we may be on to something
here, for a least some use cases.

Tomorrow I will launch the last queued quiz submission. We've had a sensational
run of submissions, so don't let it end now! Keep the ideas rolling on in...