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.erikveen.dds.nl/
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
----------------------------------------------------------------