QAPrototype (#91)

by Caleb Tennis

I remember playing with some old AI programs of which you could ask questions and if it didn't know the answer, it would ask you what the answer is:

Hi, I'm an AI program. What's your question?

>> How are you today?

I'm afraid I don't know the answer to that. Please tell me what I should say.

>>> Just fine, thanks.

Okay, I will remember that. Please ask a question.

>>> How are you today?

Just fine, thanks.
Please ask another question.

This got me thinking about an interesting concept in Ruby.

Your Quiz: Write a Ruby module that can be mixed into a class. The module does the following: upon receiving a method call that is unknown to the class, have Ruby inform the user that it doesn't know that particular method call. Then, have it ask the user for Ruby code as input to use for that method call the next time it is called.

Example:

> object.some_unknown_method

some_unknown_method is undefined
Please define what I should do (end with a newline):

>> @foo = 5
>> puts "The value of foo is #{@foo}"
>>

Okay, I got it.

>>> object.some_unknown_method
"The value of foo is 5"

[Editor's Note:

I envision this could actually be handy for prototyping classes IRb. Bonus points if you can later print the source for all methods interactively defined.

--JEG2]


Quiz Summary

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:

ruby
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:

ruby
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:

ruby
# ...

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:

ruby
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:

ruby
# ...

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