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 T.:

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…