[QUIZ] QAPrototype (#91)

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

···

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

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]

Ruby Quiz <james@grayproductions.net> writes:

[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]

Yeah, I was thinking about that as I read this quiz - the core of the
solution seems obvious, though I'm sure we'll see a bunch of
variation. What really seems interesting to me, though, is the
different ways you could extend this with little helper methods that
are useful when you're writing a prototype method, such as:

def warn_caller_obsolete(current_meth,*args)
  call_line = caller[1]
  called_meth = caller[0].gsub(/^[^`]*`([^']+)'.*/,'\1')
  warn "#{call_line}: `#{called_meth}' is obsolete; use `#{current_meth}'"
  self.send(current_meth,*args)
end

Then, you can do things like this:

irb(main):039:0> def b(f); [[f],f]; end;
irb(main):040:0* def a(f); warn_caller_obsolete(:b,f); end;
irb(main):041:0* a(9)
(irb):41:in `irb_binding': `a' is obsolete; use `b'
=> [[9], 9]

(Say, whatever happened to the call_stack RCR?)

Similar helper methods could do things like simply log the call for
later (assuming the caller can handle getting "nil" back) or delegate
the call along to some other object, possibly while logging the fact.
(Okay, that last one is probably not of much use)

I can imagine a future ruby IDE that would give the option to pop open
an editor when this happened.

Well, here is my solution. I kind of surprised myself at how short it is, but
then, there are many ways I could make this much better. The biggest problem
is that it cannot deal with arguments to the methods. It uses a global to
keep track of the interactively coded methods, I just couldn't get it to work
any other way. I guess this is because modules/mixins are not supposed to
have instance variables or keep track of their own state. The print_method
function will break if your method is a one-liner. I know I could do some
case-analysis to take care of this but I am a lazy, lazy man.

Here is a brief irb session demonstrating it, the code follows...

···

#########################################

load 'quiz91.rb'
class Test; include MethodMaker; end
test = Test.new
test.foobar()

No such method: foobar
Care to define foobar? (y/n) y
Enter method definition ([ctrl-d] when done):
first = "Ruby"
middle = "is teh"
last = "roXX0r"
puts "#{first} #{middle} #{last}"
<ctrl-d>
=> nil

test.foobar()

Ruby is teh roXX0r
=> nil

test.print_method("foobar")

def foobar
  first = "Ruby"
  middle = "is teh"
  last = "roXX0r"
  puts "#{first} #{middle} #{last}"
end
=> nil

test.another_one()

No such method: another_one
Care to define another_one? (y/n) y
Enter method definition ([ctrl-d] when done):
a = 4
b = 5
c = 67
return a + b + c
<ctrl-d>
=> nil

x = test.another_one()

=> 76

x

=> 76

test.print_method("another_one")

def another_one
  a = 4
  b = 5
  c = 67
  return a + b + c
end

...

#########################################

module MethodMaker
  $imethods = Hash.new

  def method_missing(method_name)
    puts "No such method: #{method_name}"
    # It might be a simple typo...
    # so give a chance to bail out.
    print "Care to define #{method_name}? (y/n) "
    if $stdin.getc == 121 # 'y'
      prompt_method(method_name)
    else
      raise NoMethodError, "#{method_name}"
    end
  end

  def prompt_method(name)
    puts "Enter method definition ([ctrl-d] when done):"
    meth = "def #{name}"
    while $stdin.gets
      meth += $_
    end
    meth += "end"
    meth = meth.gsub("\n",";")
    $imethods["#{name}"] = meth
    eval meth
  end

  def print_method(name)
    meth_Array = $imethods[name].split(";")
    puts meth_Array[0]
    meth_Array[1..meth_Array.size-2].each { |line| puts " #{line}" }
    puts meth_Array[-1]
  end
end

########################################

-d
--
darren kirby :: Part of the problem since 1976 :: http://badcomputer.org
"...the number of UNIX installations has grown to 10, with more expected..."
- Dennis Ritchie and Ken Thompson, June 1972

Here is my solution, it focuses on being able to use the methods you
input to patch the original file.
For inputting methods, you need to give the entire method declaration
(or any code that adds it, like attr_*).
The script automatically detects when you're done, except when you
have a syntax error (then you need to force input with "END").

An irb session:
91test.rb is a file with

require 'inputmethod.rb'
class Test
  include InputMethod
end

irb -r91test.rb

irb(main):001:0> t = Test.new
irb(main):002:0> t.foo
Give the method definition for Test#foo
END to force input end, display error and return nil

def foo
  bar
end

Give the method definition for Test#bar
END to force input end, display error and return nil

attr_accessor :bar

=> nil
irb(main):003:0> t.bar=42 ; t.foo
=> 42
irb(main):004:0> puts Test.patches
def foo
  bar
end

attr_accessor :bar
irb(main):005:0> Test.remove_patch :foo
irb(main):006:0> t.test
Give the method definition for Test#test
END to force input end, display error and return nil

def test(x=42)

  if x < 20
    "smaller"
  else
    "greater or equal"
  end
end

=> "greater or equal"
irb(main):007:0> Test.patch!
Adding methods bar,test to Test

91test.rb is now:
class Test
  attr_accessor :bar

  def test(x=42)

    if x < 20
      "smaller"
    else
      "greater or equal"
    end
  end

  include InputMethod
end

Even the indent is added to fit the class definition. :slight_smile:

Pastie: http://pastie.caboo.se/9396
Code:

require 'facets'
require 'facets/core/kernel/singleton'

class Module
  public :class_variable_set, :class_variable_get # public in 1.9 (?)
end

module InputMethod
  def self.included(klass)
    klass.class_variable_set(:@@inputmethods, methods=Hash.new{|h,k| h[k]=})
    include_point = caller.first
    klass.singleton.send(:define_method,:patch!) {
      return if methods.empty?
      _, file, line = /(.*)\:(\d+)/.match(include_point).to_a
      puts "Adding methods #{methods.keys.join(',')} to #{klass}"
      contents = File.readlines(file)

      ix = line.to_i-1
      indent = contents[ix][/^\s*/]
      code = methods.values.join("\n").split("\n").map{|l| indent + l
}.join("\n") # add indent
      contents[ix] = code + "\n\n" + contents[ix] # insert methods
before include statement
      File.open(file,'w') {|f| f << contents.join }
      methods.clear
    }
    klass.singleton.send(:define_method,:patches) {
methods.values.join("\n") }
    klass.singleton.send(:define_method,:remove_patch) {|m|
      methods.delete m
      remove_method m
    }
  end

  def method_missing(method,*args)
    print "Give the method definition for
#{self.class}\##{method}\nEND to force input end, display error and
return nil\n> ";
    method_body = line = gets
    begin
      self.class.class_eval(method_body)
    rescue SyntaxError
      return puts("Syntax error: #{$!.message[/[^\:]+\Z/].lstrip}") if
line.chomp == 'END'
      print '> '
      method_body << line=gets
      retry
    end
    self.class.class_variable_get(:@@inputmethods)[method] = method_body
    send(method,*args)
  end
end

I've been away from e-mail for a few days. I haven't looked at anyone
elses solution yet.

Here's my initial solution which didn't take long. I expect to refine
this with more function in the next few days.

I split out the function into a separate class
MethodPrompter::Interactor, so as not to polute the "class under
training" with extra methods while allowing for modular extension of
the function.

=== method_prompter.rb ===
module MethodPrompter

        class Interactor

                def initialize(target_object)
                        @target_object = target_object
                end

                def prompt_for_method(symbol, *args)
                        puts "#{symbol} is undefined"
                        puts "Please define what I should do (end with
a newline):"
                        @method_body = []
                        print ">> "
                        while line = gets
                                break if line == "\n"
                                puts "#{line.empty?} >>#{line}<<"
                                @method_body << line
                                print ">> "
                        end
                end

                def parms_from(*args)
                        ""
                end

                def make_method(symbol, *args)
                        method_string = "def #{symbol.to_s}
#{parms_from(args)}\n" <<
                                        @method_body.join("\n") <<
                                        'end'
                        @target_object.class.module_eval(method_string,
                                       'line',
                                       -1)
                end
        end

        def method_missing(symbol, *args)
                interactor = Interactor.new(self)
                interactor.prompt_for_method(symbol)
                interactor.make_method(symbol, args)
        end

end

···

--
Rick DeNatale

My blog on Ruby
http://talklikeaduck.denhaven2.com/

Not as nice as Tim Hollingsworth's use of the method signature, but oh well.

-Brent

# Example Use

irb(main):001:0> include Friendly
=> Object
irb(main):002:0> foo = bar * z
It appears that bar is undefined.
Please define what I should do (end with a blankline):
12

It appears that z is undefined.
Please define what I should do (end with a blankline):
4

=> 48
irb(main):003:0> foo
=> 48
irb(main):004:0> bar
=> 12
irb(main):005:0> z
=> 4
irb(main):006:0> added_methods
=> [:z, :bar]
irb(main):007:0> puts added_method_definitions.join("\n\n")
def z
   4
end

def bar
   12
end
=> nil
irb(main):008:0>

# Solution

module Friendly

   def method_missing name
     @_new_methods ||= Hash.new
     unless @_new_methods.has_key? name
       prompt_for_definition name
     end
     eval @_new_methods[name]
   end

   def prompt_for_definition name
     puts "It appears that #{name} is undefined."
     puts "Please define what I should do (end with a blankline):"
     @_new_methods[name] = ""
     while $stdin.gets !~ /^\s*$/
       @_new_methods[name] << $_
     end
   end

   def added_methods
     @_new_methods.keys
   end

   def added_method_definitions
     @_new_methods.map {|k,v|
       s = "def #{k}\n "
       v.rstrip!
       s << v.gsub("\n", "\n ")
       s << "\nend"
     }
   end

end

···

--

@Darren:

Actually, you can declare an attr in the module and it will work as an
instance variable in the class it's mixed in. For example:

module QAPrototype
  attr :_methods_added
end

That's what I did.

Also, print does work with one liners, but you need to flush out the
buffer each time you print the one line with
<code>$stdout.flush</code>. (Thanks to to cool folks in #ruby-lang for
that info.)

I'm still working on my solution (I've had quite a busy weekend) but
I'm having some trouble figuring out how to remove the methods I've
added... It's odd: method_missing is called for things like
#class_eval and #remove_method. O_O So, I might be doing something
wrong just adding the methods in #instance_eval.

I'll work on it some more and post it later today.

M.T.

Here's solution 2...

It now handles args, though somewhat poorly. If you know what you are doing
you can get around this but it will win no points for user-friendlyness. Also
got rid of the globals, I had to put @imethods within 'method_missing'
itself, whether or not I used :attr_accessor

I was wrong about not printing one-liners, as I have found [1..array.size-2]
will return the middle item of a three item array, so it works just fine.
Ruby is smart(er than me)...

Here is irb session showing usage:

···

############################

load 'quiz91.rb'
class Test; include MethodMaker; end
test = Test.new

=> #<Test:0xa7ae1f80>

test.print_address(:number, :street)

No such method: print_address
Care to define print_address? (y/n) y
Enter method definition ([ctrl-d] when done):
puts "#{number} #{street}"
=> nil

test.print_address("Four", "Pennsylvania Plaza")

Four Pennsylvania Plaza
=> nil

test.print_address(350, "Fifth Avenue")

350 Fifth Avenue
=> nil

test.print_name("Joe", "Ruby")

No such method: print_name
Care to define print_name? (y/n) n
# bail out, or else 'Joe' and 'Ruby' will be the arg names
NoMethodError: print_name
        from ./quiz91.rb:14:in `method_missing'
        from (irb):10

test.print_name(:first,:last)

No such method: print_name
Care to define print_name? (y/n) y
Enter method definition ([ctrl-d] when done):
puts "#{first} #{last}"
=> nil

test.print_name("Joe","Ruby")

Joe Ruby
...

Code:

module MethodMaker

  def method_missing(method_name, *args)
    @imethods = {}
    puts "No such method: #{method_name}"
    # It might be a simple typo...
    # so give a chance to bail out.
    print "Care to define #{method_name}? (y/n) "
    if $stdin.gets.chomp == "y" # 'y'
      prompt_method(method_name, args)
    else
      raise NoMethodError, "#{method_name}"
    end
  end

  def prompt_method(name, args=nil)
    puts "Enter method definition ([ctrl-d] when done):"
    meth = "def #{name}(#{args ? args.join(", ") : ""})"
    while $stdin.gets
      meth += $_
    end
    meth += "end"
    meth = meth.gsub("\n",";")
    @imethods["#{name}"] = meth
    eval meth
  end

  def print_method(name)
    meth_Array = @imethods[name].split(";")
    puts meth_Array[0]
    meth_Array[1..meth_Array.size-2].each { |line| puts " #{line}" }
    puts meth_Array[-1]
  end
end
#################################
-d
--
darren kirby :: Part of the problem since 1976 :: http://badcomputer.org
"...the number of UNIX installations has grown to 10, with more expected..."
- Dennis Ritchie and Ken Thompson, June 1972

My solution intercepts the missing method, generates the method signature (including named arguments) and spits it back at the user. The user fills in the body and ends with an 'end'.

The method is built as a string and simply eval'd against the instance, producing a singleton method. The method string is also stored in an array for later printing.

It uses the class of the given parameters as names for the method arguments. This works up to the point where you have multiple arguments of the same type.

example:
irb(main):004:0> o.foobar("hello", 3)
def foobar(string, fixnum)
fixnum.times {puts string}
end
=> nil
irb(main):005:0> o.foobar("hello", 3)
hello
=> 3
irb(main):006:0> o.moobar("goodbye")
def moobar(string)
puts string.upcase
end
=> nil
irb(main):007:0> o.moobar("goodbye")
GOODBYE
=> nil
irb(main):008:0> o.qaprint
def foobar(string, fixnum)
   fixnum.times {puts string}
end

def moobar(string)
   puts string.upcase
end
=> nil
irb(main):009:0>

code:

module QAPrototype

   def method_missing(meth, *args)
     arg_list = args.map {|arg| arg.class.to_s.downcase}.join(", ")
     meth_def = "def #{meth}(#{arg_list})\n"
     puts meth_def
     while (line = gets) != "end\n"
       meth_def += " " + line
     end
     meth_def += "end\n"

     eval meth_def

     @qamethods ||= []
     @qamethods << meth_def
     nil
   end

   def qaprint
     puts @qamethods.join("\n") if @qamethods
   end
end

quoth the Matt Todd:

@Darren:

Actually, you can declare an attr in the module and it will work as an
instance variable in the class it's mixed in. For example:

module QAPrototype
  attr :_methods_added
end

That's what I did.

Thanks for the tip. I was reading the section on mixins in the pickaxe book,
and tried doing it with an instance variable, but the code examples led me to
believe that the attribute could only be set/read within the including class
itself. Coupled with the fact that I was getting "No method '' for
Nil:Nilclass" (which I gather now was unrelated) I settled on the global
solution...

Also, print does work with one liners, but you need to flush out the
buffer each time you print the one line with
<code>$stdout.flush</code>. (Thanks to to cool folks in #ruby-lang for
that info.)

What I mean here is that after I split the string which describes the method
to an array for pretty printing my subscripting only works if the method
definition is 2+ lines of code (the 'def foo' and 'end' lines are freebies),
to make a total of 4+...

An artificial limitation I know, I will fix it when I add consideration for
method args. After reading a couple other solutions it seems much easier to
add this than I had imagined.

As for #ruby-lang, I have to agree. Someone in there saved my bacon too...
Coming from more of a Python background, my first instinct to tackle this
problem was to overload/redefine the "NoMethodError" exception to get it to
run my code. This, of course, didn't get me far, and I was directed
to "method_missing".

I guess I should just read the built in/standard lib docs strait through so I
will get a sense of what all methods are available...

-d

···

--
darren kirby :: Part of the problem since 1976 :: http://badcomputer.org
"...the number of UNIX installations has grown to 10, with more expected..."
- Dennis Ritchie and Ken Thompson, June 1972

Here's a simple IRB session using mine:

$ irb -rqap

Call #start if you want to get a head start on QAPrototype...

irb(main):001:0> start

irb(prep):001:0> @f = Foo.new
irb(prep):002:0> @f.bar

For your convenience in testing, I've created a class called
Foo and have already mixed in QAPrototype! Aren't you glad?
And while I was at it, I went ahead and created an instance
of Foo and put it into @f. Now we can all shout for joy!
Heck, we even started up the conversation with method_missing!

bar is undefined.
Please define what I should do, starting with arguments
this method should accept (skip and end with newline):

def bar
  "Hello!"

end

Okay, I got it.

Calling the method now!

=> "Hello!"
irb(main):002:0> @f.add 10, 12

add is undefined.
Please define what I should do, starting with arguments
this method should accept (skip and end with newline):

def add a, b
  a - b

end

Okay, I got it.

Calling the method now!

=> -2
irb(main):003:0> @f.undo

add is now gone from this class.

=> nil
irb(main):004:0> @f.add 10, 12

add is undefined.
Please define what I should do, starting with arguments
this method should accept (skip and end with newline):

def add a, b
  a + b

end

Okay, I got it.

Calling the method now!

=> 22
irb(main):005:0> @f.dump
class Foo
  def bar
    "Hello!"
  end
  def add a, b
    a + b
  end
end
=> nil
irb(main):006:0> @f.dump "/tmp/foo.rb"

Class was written to /tmp/foo.rb successfully!

=> nil
irb(main):007:0> exit
$

There you have it. Check out the file attached for my solution.

Of particular note to my solution is an #undo method that will remove
the last method added. As you can tell, it supports arguments.
However, I was not able to fully coax blocks to work properly, at
least on creation (when it calls it after you've created it). That's
negligible, though, and I'll probably iron that out a bit.

Cheers,

M.T.

P.S. - No, I didn't go into another IRB session for those two lines...
I just printed that out to give the general idea of what just happened
to get things rolling.

qap.rb (3.95 KB)