[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

[QUIZ] QAPrototype (#91

James Gray

8/18/2006 2:03:00 PM

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

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]

8 Answers

darren kirby

8/20/2006 12:34:00 PM

0

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://badco...
"...the number of UNIX installations has grown to 10, with more expected..."
- Dennis Ritchie and Ken Thompson, June 1972

Sander Land

8/20/2006 2:28:00 PM

0

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

Pastie: http://pastie.cab...
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

Matt Todd

8/20/2006 3:19:00 PM

0

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

Tim Hollingsworth

8/20/2006 4:14:00 PM

0

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

Rick DeNatale

8/20/2006 6:40:00 PM

0

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

Brent Fitzgerald

8/20/2006 7:00:00 PM

0

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

--
http://brentfitzg...



darren kirby

8/20/2006 10:01:00 PM

0

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://badco...
"...the number of UNIX installations has grown to 10, with more expected..."
- Dennis Ritchie and Ken Thompson, June 1972

darren kirby

8/21/2006 4:18:00 AM

0

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
irb> 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://badco...
"...the number of UNIX installations has grown to 10, with more expected..."
- Dennis Ritchie and Ken Thompson, June 1972