From: Ruby Quiz Date: 2006-08-25T03:27:23+09:00 Subject: [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 => # >> 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...