[lnkForumImage]
TotalShareware - Download Free Software

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


 

Forums >

comp.lang.ruby

Meta-Meta-Programming

Erik Veenstra

2/7/2006 10:18:00 PM


I had a discussion with a friend. A Java guy. He wants the
arguments of a method call being checked. "I want the first one
to be an Integer. And the second one is a String. Period." No
discussion. I explained our duck-typing paradigm. He's not
convinced. He thinks Java. So, he gets Java.

Lets check the types of the arguments of a method call!

(This post is not about type checking at all. It's about how to
implement such a type checker. Or, more general, it's about
monitoring-functions.)

I wanted to do this with a nice and clean implementation, with
the real magic pushed down to a place I would never come again
("write once, read never"). I wanted something like this (focus
on line 7):

1 class Foo
2 def bar(x, y, z)
3 # x should be Numeric
4 # y should be a String
5 # z should respond to :to_s
6 end
7 typed :bar, Numeric, String, :to_s # !!!!!
8 end

Focus on line 7, once again. Make it three times. It's all
about line 7.

That was good enough for him. "But you can't do this. You
simply can't. That's magic." I laughed at him, turned around
and did it...

That's where this story is all about...

First, I'll give you a piece of code which doesn't do anything,
except that it seems to wrap the original method in another
method (focus on line 12):

1 class Module
2 def just_wrap(method_name)
3 wrap_method(method_name) do |org_method, args, block|
4 org_method.call(*args, &block)
5 end
6 end
7 end
8 class Foo
9 def bar(x, y, z)
10 p [x, y, z]
11 end
12 just_wrap :bar # !!!!!
13 end
14 Foo.new.bar("a", "b", "c") # ===> ["a", "b", "c"]

You can find the implementation of wrap_method below. This
thread is all about that very one method. It's the big trick.
You don't need to understand its implementation. Knowing how to
use it is good enough.

Line 3 retrieves the original method and yields the given block
with this method, as well as with its arguments and block. Not
*args, not &block. Just args and block. Blocks don't get
blocks, you know. (Although it's introduced in Ruby 1.9.)

Within the given block, we can do whatever we want to. That's
where the real stuff goes.

But, someday, we have to call the original method with the
original parameters and the original block. That's what we do
on line 4.

That's about it. That's the whole story. There's nothing more
to say.

Except for an example or two...

Here's a simple example. It "upcases" every argument. It must
be silly to "upcase" every argument like this, but we'll do it
anyway. Introducing line 4:

1 class Module
2 def big_arguments(method_name)
3 wrap_method(method_name) do |org_method, args, block|
4 args = args.collect{|x| x.to_s.upcase}
5 org_method.call(*args, &block)
6 end
7 end
8 end
9 class Foo
10 def bar(x, y, z)
11 [x, y, z]
12 end
13 big_arguments :bar
14 end
15 Foo.new.bar("a", "b", "c") # ===> ["A", "B", "C"]

Here's another example. Lines 4, 5 and 6. They inform you about
nil things.

1 class Module
2 def find_nil(method_name)
3 wrap_method(method_name) do |org_method, args, block|
4 if args.include?(nil)
5 $stderr.puts "Found a nil when called from #{caller[1..-1].inspect}."
6 end
7 org_method.call(*args, &block)
8 end
9 end
10 end
11 class Foo
12 def bar(x, y, z)
13 end
14 find_nil :bar
15 end
16 Foo.new.bar("a", "b", "c") # ===>
17 Foo.new.bar("a", "b", nil) # ===> Found a nil when called from from ["test.rb:17"].
18 Foo.new.bar("a", "b", "c") # ===>

I call "typed", "just_wrap", "big_arguments" and "find_nil":
monitor-functions. I don't know exactly how this term got into
my head, but it does sound good: monitor-functions. It's
definitely better than wrap-method-functions. (You can build
non-monitor-functions as well. But that's really stupid:
monitor-and-non-monitor-functions.)

Meanwhile, I played with a couple of monitor-functions:
debugging, logging, synchronization, statistics, benchmarking,
roles (like on WebSphere). Ideas? It's easy to create them. Try
it. Let me know.

Forget about the implementation of "wrap_method". It's just
sitting there, waiting to be used to implement a
monitor-function. It's easy to implement a monitor-function.
And it's very, very easy to use it. Those where my goals.

Oh, by the way, if such a monitor-function is kind of
meta-programming (it's a buzz-word, I know, but it is, isn't
it?), how would you call "wrap_method"? Meta-meta-programming?

It was just an idea. Just wanted to tell you. Couldn't sleep.
Feel much better now. Maybe I can sleep...

Thanks for listening.

gegroet,
Erik V. - http://www.erikve...

PS: Sorry for this rather lengthy post. It just got a bit
lengthier than I planned. It just happened. No control.

----------------------------------------------------------------

class Module

# With this, we can create monitoring functions.
# It might not be clearly readable,
# but it's written only once.
# Write once, read never.
# Forget about the internals.
# Just use it.
# It should be part of Ruby itself, anyway... :)

def wrap_method(method_name, *args1, &block1)
@_wrap_method_count_ ||= 0
@_wrap_method_count_ += 1

prefix = "_wrap_method_#{@_wrap_method_count_}"

module_eval <<-EOF
alias :#{prefix}_org :#{method_name} # Store the original method for later use.

define_method(:#{prefix}_args) {args1} # Store the arguments of the call to Module#wrap_method. (Not used.)
define_method(:#{prefix}_block) {block1} # Store the block of the call to Module#wrap_method.

def #{method_name}(*args2, &block2)
#{prefix}_block.call(method(:#{prefix}_org), args2, block2) # Note that this is not *args2 and not &block2!
end
EOF
end

end

----------------------------------------------------------------

29 Answers

Erik Veenstra

2/7/2006 10:28:00 PM

0


I forgot to show you the implementation of this "typed".

Well, here it is...

gegroet,
Erik V. - http://www.erikve...

----------------------------------------------------------------

# IMPLEMENTATION

class Module
def typed(method_name, *types)
wrap_method(method_name) do |org_method, args, block|
args.each_with_index do |args, n|
[types[n]].flatten.each do |typ|
if typ.kind_of?(Module)
unless arg.kind_of?(typ)
raise ArgumentError, "Wrong argument type (#{arg.class} instead of #{typ}, argument #{n+1})."
end
elsif typ.kind_of?(Symbol)
unless arg.respond_to?(typ)
raise ArgumentError, "#{arg} doesn't respond to :#{typ} (argument #{n+1})."
end
else
raise ArgumentError, "Wrong type in types (#{typ}, argument #{n+1})"
end
end
end

org_method.call(*args, &block)
end
end
end

----------------------------------------------------------------

# TEST SCRIPT

class Foo
def bar(x, y, z)
# x should be Numeric
# y should be a String
# z should respond to :gsub and :to_s
:good
end

typed :bar, Numeric, String, [:gsub, :to_s]
end

def test(*args)
begin
puts "#{args.inspect} : OK : #{Foo.new.bar(*args).inspect}"
rescue Exception => e
puts "#{args.inspect} : NOK : #{e.message}"
end
end

puts
puts File.open(__FILE__){|f| f.readlines}.select{|x| x =~ /^\s*typed\b/}.join("\n")
puts

test(7)
test(7, 8, 9)
test(7, 8, "9")
test(7, "8", 9)
test(7, "8", "9")

----------------------------------------------------------------

jgbailey

2/7/2006 10:52:00 PM

0

Slick!

Erik Veenstra wrote:
> I had a discussion with a friend. A Java guy. He wants the
> arguments of a method call being checked. "I want the first one
> to be an Integer. And the second one is a String. Period." No
> discussion. I explained our duck-typing paradigm. He's not
> convinced. He thinks Java. So, he gets Java.
>
> Lets check the types of the arguments of a method call!
>
> (This post is not about type checking at all. It's about how to
> implement such a type checker. Or, more general, it's about
> monitoring-functions.)
>
> I wanted to do this with a nice and clean implementation, with
> the real magic pushed down to a place I would never come again
> ("write once, read never"). I wanted something like this (focus
> on line 7):
>
> 1 class Foo
> 2 def bar(x, y, z)
> 3 # x should be Numeric
> 4 # y should be a String
> 5 # z should respond to :to_s
> 6 end
> 7 typed :bar, Numeric, String, :to_s # !!!!!
> 8 end
>
> Focus on line 7, once again. Make it three times. It's all
> about line 7.
>
> That was good enough for him. "But you can't do this. You
> simply can't. That's magic." I laughed at him, turned around
> and did it...
>
> That's where this story is all about...
>
> First, I'll give you a piece of code which doesn't do anything,
> except that it seems to wrap the original method in another
> method (focus on line 12):
>
> 1 class Module
> 2 def just_wrap(method_name)
> 3 wrap_method(method_name) do |org_method, args, block|
> 4 org_method.call(*args, &block)
> 5 end
> 6 end
> 7 end
> 8 class Foo
> 9 def bar(x, y, z)
> 10 p [x, y, z]
> 11 end
> 12 just_wrap :bar # !!!!!
> 13 end
> 14 Foo.new.bar("a", "b", "c") # ===> ["a", "b", "c"]
>
> You can find the implementation of wrap_method below. This
> thread is all about that very one method. It's the big trick.
> You don't need to understand its implementation. Knowing how to
> use it is good enough.
>
> Line 3 retrieves the original method and yields the given block
> with this method, as well as with its arguments and block. Not
> *args, not &block. Just args and block. Blocks don't get
> blocks, you know. (Although it's introduced in Ruby 1.9.)
>
> Within the given block, we can do whatever we want to. That's
> where the real stuff goes.
>
> But, someday, we have to call the original method with the
> original parameters and the original block. That's what we do
> on line 4.
>
> That's about it. That's the whole story. There's nothing more
> to say.
>
> Except for an example or two...
>
> Here's a simple example. It "upcases" every argument. It must
> be silly to "upcase" every argument like this, but we'll do it
> anyway. Introducing line 4:
>
> 1 class Module
> 2 def big_arguments(method_name)
> 3 wrap_method(method_name) do |org_method, args, block|
> 4 args = args.collect{|x| x.to_s.upcase}
> 5 org_method.call(*args, &block)
> 6 end
> 7 end
> 8 end
> 9 class Foo
> 10 def bar(x, y, z)
> 11 [x, y, z]
> 12 end
> 13 big_arguments :bar
> 14 end
> 15 Foo.new.bar("a", "b", "c") # ===> ["A", "B", "C"]
>
> Here's another example. Lines 4, 5 and 6. They inform you about
> nil things.
>
> 1 class Module
> 2 def find_nil(method_name)
> 3 wrap_method(method_name) do |org_method, args, block|
> 4 if args.include?(nil)
> 5 $stderr.puts "Found a nil when called from #{caller[1..-1].inspect}."
> 6 end
> 7 org_method.call(*args, &block)
> 8 end
> 9 end
> 10 end
> 11 class Foo
> 12 def bar(x, y, z)
> 13 end
> 14 find_nil :bar
> 15 end
> 16 Foo.new.bar("a", "b", "c") # ===>
> 17 Foo.new.bar("a", "b", nil) # ===> Found a nil when called from from ["test.rb:17"].
> 18 Foo.new.bar("a", "b", "c") # ===>
>
> I call "typed", "just_wrap", "big_arguments" and "find_nil":
> monitor-functions. I don't know exactly how this term got into
> my head, but it does sound good: monitor-functions. It's
> definitely better than wrap-method-functions. (You can build
> non-monitor-functions as well. But that's really stupid:
> monitor-and-non-monitor-functions.)
>
> Meanwhile, I played with a couple of monitor-functions:
> debugging, logging, synchronization, statistics, benchmarking,
> roles (like on WebSphere). Ideas? It's easy to create them. Try
> it. Let me know.
>
> Forget about the implementation of "wrap_method". It's just
> sitting there, waiting to be used to implement a
> monitor-function. It's easy to implement a monitor-function.
> And it's very, very easy to use it. Those where my goals.
>
> Oh, by the way, if such a monitor-function is kind of
> meta-programming (it's a buzz-word, I know, but it is, isn't
> it?), how would you call "wrap_method"? Meta-meta-programming?
>
> It was just an idea. Just wanted to tell you. Couldn't sleep.
> Feel much better now. Maybe I can sleep...
>
> Thanks for listening.
>
> gegroet,
> Erik V. - http://www.erikve...
>
> PS: Sorry for this rather lengthy post. It just got a bit
> lengthier than I planned. It just happened. No control.
>
> ----------------------------------------------------------------
>
> class Module
>
> # With this, we can create monitoring functions.
> # It might not be clearly readable,
> # but it's written only once.
> # Write once, read never.
> # Forget about the internals.
> # Just use it.
> # It should be part of Ruby itself, anyway... :)
>
> def wrap_method(method_name, *args1, &block1)
> @_wrap_method_count_ ||= 0
> @_wrap_method_count_ += 1
>
> prefix = "_wrap_method_#{@_wrap_method_count_}"
>
> module_eval <<-EOF
> alias :#{prefix}_org :#{method_name} # Store the original method for later use.
>
> define_method(:#{prefix}_args) {args1} # Store the arguments of the call to Module#wrap_method. (Not used.)
> define_method(:#{prefix}_block) {block1} # Store the block of the call to Module#wrap_method.
>
> def #{method_name}(*args2, &block2)
> #{prefix}_block.call(method(:#{prefix}_org), args2, block2) # Note that this is not *args2 and not &block2!
> end
> EOF
> end
>
> end
>
> ----------------------------------------------------------------

Eric Hodel

2/7/2006 11:43:00 PM

0

On Feb 7, 2006, at 2:18 PM, Erik Veenstra wrote:

>
> I had a discussion with a friend. A Java guy. He wants the
> arguments of a method call being checked. "I want the first one
> to be an Integer. And the second one is a String. Period." No
> discussion. I explained our duck-typing paradigm. He's not
> convinced. He thinks Java. So, he gets Java.
>
> Lets check the types of the arguments of a method call!
>
> (This post is not about type checking at all. It's about how to
> implement such a type checker. Or, more general, it's about
> monitoring-functions.)
>
> I wanted to do this with a nice and clean implementation, with
> the real magic pushed down to a place I would never come again
> ("write once, read never"). I wanted something like this (focus
> on line 7):
>
> 1 class Foo
> 2 def bar(x, y, z)
> 3 # x should be Numeric
> 4 # y should be a String
> 5 # z should respond to :to_s
> 6 end
> 7 typed :bar, Numeric, String, :to_s # !!!!!
> 8 end
>
> Focus on line 7, once again. Make it three times. It's all
> about line 7.
>
> That was good enough for him. "But you can't do this. You
> simply can't. That's magic." I laughed at him, turned around
> and did it...

For bonus points, record stats for every time your assertion fails
and you generate a "type error" compared with every time it does
nothing. Hopefully you can show your coworker how useless the code
really is.

--
Eric Hodel - drbrain@segment7.net - http://se...
This implementation is HODEL-HASH-9600 compliant

http://trackmap.rob...




Lionel Thiry

2/7/2006 11:46:00 PM

0

http://www.rcrchive.net/rc...

Erik Veenstra a écrit :
>
> 1 class Module
> 2 def just_wrap(method_name)
> 3 wrap_method(method_name) do |org_method, args, block|
> 4 org_method.call(*args, &block)
> 5 end
> 6 end
> 7 end
> 8 class Foo
> 9 def bar(x, y, z)
> 10 p [x, y, z]
> 11 end
> 12 just_wrap :bar # !!!!!
> 13 end
> 14 Foo.new.bar("a", "b", "c") # ===> ["a", "b", "c"]
>

class Foo
def bar(x, y, z)
p [x, y, z]
end
end

cut JustWrap < Foo
def bar
super
end
end

>
> 1 class Module
> 2 def big_arguments(method_name)
> 3 wrap_method(method_name) do |org_method, args, block|
> 4 args = args.collect{|x| x.to_s.upcase}
> 5 org_method.call(*args, &block)
> 6 end
> 7 end
> 8 end
> 9 class Foo
> 10 def bar(x, y, z)
> 11 [x, y, z]
> 12 end
> 13 big_arguments :bar
> 14 end
> 15 Foo.new.bar("a", "b", "c") # ===> ["A", "B", "C"]
>

class Foo
def bar(x, y, z)
p [x, y, z]
end
end

cut BigArguments < Foo
def bar(*args)
super(*args.collect{|x| x.to_s.upcase})
end
end

>
> 1 class Module
> 2 def find_nil(method_name)
> 3 wrap_method(method_name) do |org_method, args, block|
> 4 if args.include?(nil)
> 5 $stderr.puts "Found a nil when called from #{caller[1..-1].inspect}."
> 6 end
> 7 org_method.call(*args, &block)
> 8 end
> 9 end
> 10 end
> 11 class Foo
> 12 def bar(x, y, z)
> 13 end
> 14 find_nil :bar
> 15 end
> 16 Foo.new.bar("a", "b", "c") # ===>
> 17 Foo.new.bar("a", "b", nil) # ===> Found a nil when called from from ["test.rb:17"].
> 18 Foo.new.bar("a", "b", "c") # ===>
>

class Foo
def bar(x, y, z)
end
end

cut FindNil < Foo
def bar(*args)
if args.include?(nil)
$stderr.puts "Found a nil when called from #{caller[1..-1].inspect}."
end
super(*args)
end
end

--
Lionel Thiry

Personal web site: http://users.skynet....

Erik Veenstra

2/8/2006 12:02:00 AM

0

> class Foo
> def bar(x, y, z)
> p [x, y, z]
> end
> end
>
> cut JustWrap < Foo
> def bar
> super
> end
> end

So I have to replace every Foo.new to JustWrap.new, just to
activate the debugging? Kidding?

gegroet,
Erik V. - http://www.erikve...

vanekl

2/8/2006 12:22:00 AM

0

Trans

2/8/2006 12:33:00 AM

0

> So I have to replace every Foo.new to JustWrap.new, just to
> activate the debugging? Kidding?

No you do not. A cut is a _transparent_ class. You would still use
Foo.new.

T.

Maurice Codik

2/8/2006 12:54:00 AM

0

Yet another suggestion-- I wrote a little library that lets you define
simple contracts for function inputs... useful if you dont want to repeat
the same duck-type assertions over and over again.

here's an example of how it works:

require "contracts"
class TestContracts
extend Contracts

define_data :writable => lambda {|x| x.respond_to?("write") and
x.respond_to?("closed?") and not x.closed? },
:positive => lambda {|x| x >= 0 }

contract :hello, [:positive, :string, :writable]

def hello(n, s, f)
n.times { f.write "hello #{s}!\n" }
end
end

tc = TestContracts.new
tc.hello(2, "world", $stdout)
# -> hello world!
# -> hello world!

# tc.hello(2, 3, $stdout)
# -> test-contracts.rb:22: argument 2 of method 'hello' must satisfy
the 'string' contract (Contracts::ContractViolation)

You can download it at: : http://mauricecodik.com/projects/ruby/co...

Maurice

On 2/7/06, Trans <transfire@gmail.com> wrote:
>
> > So I have to replace every Foo.new to JustWrap.new, just to
> > activate the debugging? Kidding?
>
> No you do not. A cut is a _transparent_ class. You would still use
> Foo.new.
>
> T.
>
>
>

Trans

2/8/2006 2:03:00 AM

0

But this is an example of why you wouldn't really want this
functionality in Ruby right? We all know there are times we need to
contrain arguments, but that should be exception, not the norm. Hence
the beauty of ducktyping.

T.

Vidar Hokstad

2/8/2006 11:34:00 AM

0

Trans wrote:
> But this is an example of why you wouldn't really want this
> functionality in Ruby right? We all know there are times we need to
> contrain arguments, but that should be exception, not the norm. Hence
> the beauty of ducktyping.

That's somewhat short-sighted. Any method in a language like Ruby will
either not be fully defined (that is, it will fail for a subset of
possible inputs), or will be full of explicit type checking (kind_of?
etc). For the most part, the former is chosen. For the most part it
"works", sort of.

But it works because most of the time people have reasonable
expectations of what a method will expect, or you read the
documentation (and it is up to date enough) and you test.

However this is nothing more than making Ruby enforce a contract: If
your method DOES need #to_s to be present for one of the arguments for
the method to be well defined, I for one would prefer to find out as
early as possible rather than have it suddenly break on me because
things just happens to work without it "most of the time".

Properly written preconditions both reduces the test cases - the set of
different classes of input can be constrained significantly - and helps
document the code _and_ ensure that this documentation is likely to
stay in sync with the code, unlike documentation that has no effect on
your tests.

There's nothing contradictory between this method and ducktyping -
ducktyping is about not relying on name tags but about actual features
(that is, it's having the #to_s method that is important, not having
been labeled as implementing a hypotetical "CanConvertToString"
interface), and this method can be used to check for that.

It can of course also be abused to make type checks that are far too
generic, and I can to a certain extent agree with you that using it to
constrain arguments to a specific class may be undesirable most of the
time (... after having seen full well how annoying badly done type
checking is from dealing with REXML...)

Consider it inline documentation and an additional testing and
debugging tool - if runtime performance is affected too much you could
always add a switch to make it only actually wrap the methods if $DEBUG
is set and otherwise leave them alone.

Vidar