From: Erik Veenstra Date: 2006-02-08T07:18:23+09:00 Subject: Meta-Meta-Programming 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 ----------------------------------------------------------------