Ruby Monitor-Functions
Or Meta-Meta-Programming in Ruby
Tue Aug 14 13:09:14 UTC 2007
Erik Veenstra <rubyscript2exe@erikveen.dds.nl>
1. Rationale
2. Wrapping an Instance Method
3. Wrapping a Module Method or Class Method
4. Module#pre_condition and Module#post_condition
5. Implementation
5.1. Order of Execution of Recursively Wrapped Methods
5.2. The Real Stuff
5.3. Unit Tests
6. Real-World Examples
6.1. Type-Checking
6.2. Value-Checking
6.3. Once
6.4. Singleton
6.5. Abstract Methods
1. Rationale
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!
(Well, this article 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 8):
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
8 check_types :bar, Numeric, String, :to_s
9 end
(Focus on line 8, once again. Make it three times. It's all about line 8.)
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 what this story is all about. To be more accurate: It's about how it's done, about monitor-functions and method-wrapping, not about type-checking.
2. Wrapping an Instance Method
First, here's a piece of code which doesn't do anything, except that it seems to wrap the original method with a block (focus on line 14):
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
9 class Foo
10 def bar(x, y, z)
11 p [x, y, z]
12 end
13
14 just_wrap :bar
15 end
16
17 Foo.new.bar("a", "b", "c") # ===> ["a", "b", "c"]
You can find the implementation of Module#wrap_method in Implementation. This article 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 its 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 (near line 4), 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
6 org_method.call(*args, &block)
7 end
8 end
9 end
10
11 class Foo
12 def bar(x, y, z)
13 [x, y, z]
14 end
15
16 big_arguments :bar
17 end
18
19 Foo.new.bar("a", "b", "c") # ===> ["A", "B", "C"]
Here's another example. Lines 4, 5, 6 and 7. 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 \
6 from #{caller[1..-1].inspect}."
7 end
8
9 org_method.call(*args, &block)
10 end
11 end
12 end
13
14 class Foo
15 def bar(x, y, z)
16 end
17
18 find_nil :bar
19 end
20
21 Foo.new.bar("a", "b", "c") # ===>
22 Foo.new.bar("a", "b", nil) # ===> Found a nil when
23 called from from
24 ["test.rb:22"].
25 Foo.new.bar("a", "b", "c") # ===>
I call check_types, 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.)
Did I mention that it is possible to double-wrap a method with two or more monitor-functions? The order in which they are executed is, of course, bottom-up:
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 and :gsub
6 end
7
8 check_types :bar, Numeric, String, [:to_s, :gsub]
9 log_args :bar
10 end
Meanwhile, I played with a couple of monitor-functions: debugging, logging, synchronization, statistics, benchmarking, roles (like on WebSphere), #typechecking type checking, even value checking and range checking. Ideas? It's easy to create them. Try it. Let me know.
Forget about the implementation of Module#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?
3. Wrapping a Module Method or Class Method
Now, we have this Module#wrap_method for wrapping instance methods. But what about wrapping module methods, like Module#wrap_module_method?
(Module#wrap_module_method is deprecated. Use aModule.metaclass.wrap_method instead.)
We are going to take this dangerous type checking thing to the next level. Just as an example. (I'm not promoting this technique; it's just an example!)
Imagine, we want the type definition to be positioned before the method instead of after the method. Just for better readability:
1 class Foo
2 def_types Numeric, String, [:to_s, :gsub]
3 def bar(x, y, z)
4 # x should be Numeric
5 # y should be a String
6 # z should respond to :to_s and :gsub
7 # Very long method...
8 end
9 end
... instead of:
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 and :gsub
6 # Very long method...
7 end
8
9 check_types :bar, Numeric, String, [:to_s, :gsub]
10 end
We could do this by storing the types in def_types and overwriting Foo::method_added. But what about the old functionality in Foo::method_added? You could alias to another method and than use this alias. That's the common way to work around this problem. (I've never liked it...) But, again, we can use wrap_method to add the new functionality to the original method. A module is an instance itself after all, isn't it? Introducing metaclass.wrap_method:
1 class Module
2 def def_types(*types)
3 metaclass.wrap_method(:method_added) do |org_method, args, block|
4 if types
5 method_name = args[0]
6 t = types
7 types = nil # Avoid looping
8
9 check_types(method_name, *t)
10 end
11
12 org_method.call(*args, &block) if org_method
13 end
14 end
15 end
But, be careful, there's a hidden loop. def_types wraps method_added. From now on, every time method_added is called, it calls check_types (line 9, but only if we removed if types on line 4), which calls wrap_method, which adds methods and thus triggers method_added. And so on. This types = nil (line 7) avoids this looping. It also applies the wrapping to only the next method defined in Foo (only to Foo#bar1 and not to Foo#bar2). You only have to add this mechanism when wrapping Module#method_added, not for other module methods.
Do you see that this metaclass.wrap_method looks like wrap_method? They should look the same. They are brother and sister.
Once again, it should be possible to wrap the wrapper:
1 class Foo
2 def_types Numeric, String, [:to_s, :gsub]
3 def_stat "/tmp/stats.log"
4
5 def bar1(x, y, z)
6 end
7
8 def bar2(x, y, z) # bar2 is neither logged, nor checked.
9 end
10 end
4. Module#pre_condition and Module#post_condition
Since doing checks on arguments before executing an instance method is very common, I've made Module#pre_condition. You give it the names of the instance methods you want to wrap (as symbols or as strings) and you give it a block. This block is executed in the original context and receives |obj, method_name, args, block|.
There's also a Module#post_condition.
Example:
1 class Foo
2 def bar(*args, &block)
3 p [:in_method, args, block]
4
5 yield(*args)
6 end
7
8 pre_condition(:bar) do |obj, method_name, args, block|
9 p [:in_pre_condition, args]
10 end
11
12 post_condition(:bar) do |obj, method_name, args, block|
13 p [:in_post_condition, args]
14 end
15 end
16
17 foo = Foo.new
18
19 foo.bar(1, 2, 3, 4) do |*args|
20 p [:in_block, args]
21 end
Which results in:
[:in_pre_condition, [1, 2, 3, 4]] [:in_method, [1, 2, 3, 4], #<Proc:0xb7d7f868@.....:19>] [:in_block, [1, 2, 3, 4]] [:in_post_condition, [1, 2, 3, 4]]
Use metaclass as receiver of Module#pre_condition or Module#post_condition to wrap a module method instead of an instance method:
1 require "ostruct"
2
3 class Foo < Struct.new(:aa, :bbb)
4 metaclass.post_condition(:new) do |obj, method_name, args, block|
5 line = caller.select{|s| s.include?(__FILE__)}.shift.scan(/\d+/)[-1]
6
7 puts "An object of class #{self} has been created."
8 puts "The call looked like: #{self}.new(#{args.inspect})."
9 puts "It's done on line: #{line}."
10 puts
11 end
12 end
13
14 Foo.new(11, 111)
15 Foo.new(22, 222)
16 Foo.new(33, 333)
Which results in:
An object of class Foo has been created. The call looked like: Foo.new([11, 111]). It's done on line: 14. An object of class Foo has been created. The call looked like: Foo.new([22, 222]). It's done on line: 15. An object of class Foo has been created. The call looked like: Foo.new([33, 333]). It's done on line: 16.
Use obj.instance_eval to execute part the block in the context of the object, instead of in the original context:
1 require "ostruct"
2
3 class Foo < Struct.new(:aa, :bbb)
4 post_condition(:initialize) do |obj, method_name, args, block|
5 line = caller.select{|s| s.include?(__FILE__)}.shift.scan(/\d+/)[-1]
6
7 obj.instance_eval do
8 puts "An object of class #{self.class} has been created."
9 puts "The arguments are: @aa=#{aa} and @bbb=#{bbb}."
10 puts "The object has id: #{__id__}."
11 puts "It's done on line: #{line}."
12 puts
13 end
14 end
15 end
16
17 Foo.new(11, 111)
18 Foo.new(22, 222)
19 Foo.new(33, 333)
Which results in:
An object of class Foo has been created. The arguments are: @aa=11 and @bbb=111. The object has id: -605321068. It's done on line: 17. An object of class Foo has been created. The arguments are: @aa=22 and @bbb=222. The object has id: -605321448. It's done on line: 18. An object of class Foo has been created. The arguments are: @aa=33 and @bbb=333. The object has id: -605321898. It's done on line: 19.
It is possible to wrap a non-existing method. And since the block is, well, uh, a block, and not a method definition, it captures the local variables.
1 class Foo
2 x = 7
3
4 post_condition(:bar) do
5 puts "Variable x is captured in the closure and has value #{x}."
6 end
7
8 x = 8
9 end
10
11 Foo.new.bar
Which results in:
Variable x is captured in the closure and has value 8.
(Although they're named *_condition, they're not checking anything. They should be named *_action. But pre_action is harder to remember than pre_condition. So I stick to the latter.)
5. Implementation
5.1. Order of Execution of Recursively Wrapped Methods
One more minute...
In normal Ruby, when a method of an object is called, it's searched for in the class of that object. If it's not found, Ruby tries to find it in the superclass, and so on.
When wrapping methods, it's basically the same.
At wrap-time, you can only wrap methods in the specified class, not methods in the superclass. At run-time, this chain of blocks is called in reversed order. At the end (beginning?) of the chain, the original method is called. If there's no original method, the superclass is searched for its method (or chain).
![[images/monitorfunctions1.dia.gif]](images/monitorfunctions1.dia.gif)
Imagine this code:
1 class Foo
2 def bar
3 p [Foo, :bar]
4 end
5
6 pre_condition(:bar) {p [Foo, :pre]}
7 post_condition(:bar) {p [Foo, :post]}
8 end
9
10 class Bar < Foo
11 pre_condition(:bar) {p [Bar, :pre]}
12 post_condition(:bar) {p [Bar, :post]}
13 end
14
15 class Baz < Foo
16 def bar
17 p [Baz, :bar, :before]
18 super
19 p [Baz, :bar, :after]
20 end
21
22 pre_condition(:bar) {p [Baz, :pre]}
23 post_condition(:bar) {p [Baz, :post]}
24 end
25
26 class Bam < Foo
27 def bar
28 p [Bam, :bar, :before]
29 # no super
30 p [Bam, :bar, :after]
31 end
32
33 pre_condition(:bar) {p [Bam, :pre]}
34 post_condition(:bar) {p [Bam, :post]}
35 end
36
37 class Foo
38 pre_condition(:bar) {p [Foo, :prepre]}
39 post_condition(:bar) {p [Foo, :postpost]}
40 end
41
42 puts
43 Foo.new.bar
44 puts
45 Bar.new.bar
46 puts
47 Baz.new.bar
48 puts
51 Bam.new.bar
Which results in:
[Foo, :prepre] [Foo, :pre] [#<Foo:0xb7d618b8>, :bar] [Foo, :post] [Foo, :postpost] [Bar, :pre] [Foo, :prepre] [Foo, :pre] [#<Bar:0xb7d61250>, :bar] [Foo, :post] [Foo, :postpost] [Bar, :post] [Baz, :pre] [#<Baz:0xb7d60878>, :bar, :before] [Foo, :prepre] [Foo, :pre] [#<Baz:0xb7d60878>, :bar] [Foo, :post] [Foo, :postpost] [#<Baz:0xb7d60878>, :bar, :after] [Baz, :post] [Bam, :pre] [#<Bam:0xb7d5ec18>, :bar, :before] [#<Bam:0xb7d5ec18>, :bar, :after] [Bam, :post]
5.2. The Real Stuff
Finally!
If you want this code to be available as a gem, please shout. If enough people want it, I might build one.
About the license: If you want to copy this code to your application, go ahead. Just don't sell this code as a standalone solution. That's not fair.
require "thread"
class Module
# Meta-Meta-Programming
# 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... :)
# This wrap_method is low-level stuff.
# If you just want to add code to a method, scroll
# down to pre_condition and post_condition.
# They're much easier to use.
def wrap_method(*method_names, &block1)
raise ArgumentError, "method_name is missing" if method_names.empty?
raise ArgumentError, "block is missing" unless block1
Thread.exclusive do
method_names.flatten.each do |method_name|
count =
Module.module_eval do
@_wm_count_ ||= 0
@_wm_count_ +=1
end
module_eval <<-EOF
# Get the method which is to be wrapped.
method = instance_method(:"#{method_name}") rescue nil
# But it shouldn't be defined in a super class...
if method.to_s != "#<UnboundMethod: " + self.to_s + "##{method_name}>"
method = nil
end
if method.nil? and ($VERBOSE or $DEBUG)
$stderr.puts \
"Wrapping a non-existing method ["+self.to_s+"##{method_name}]."
end
# Store the method-to-be-wrapped and the wrapping block.
define_method(:"_wm_previous_#{method_name}_#{count}_") do
[method, block1]
end
# Avoid this stupid "warning: method redefined".
unless :#{method_name} == :initialize
undef_method(:"#{method_name}") rescue nil
end
# Define __class__ and __kind_of__.
define_method(:__class__) \
{Object.instance_method(:class).bind(self).call}
define_method(:__kind_of__) \
{|s| Object.instance_method(:"kind_of?").bind(self).call(s)}
# Define the new method.
def #{method_name}(*args2, &block2)
if self.__kind_of__(Module)
context = metaclass
else
context = self.__class__
end
# Retrieve the previously stored method-to-be-wrapped (old),
# as well as the wrapping block (new).
# Note: An UnboundMethod of self.superclass.metaclass can't be
# bound to self.metaclass, so we "walk up" the class hierarchy.
previous = context.instance_method(
:"_wm_previous_#{method_name}_#{count}_")
begin
previous = previous.bind(zelf ||= self)
rescue TypeError => e
retry if zelf = zelf.superclass
end
old, new = previous.call
# If there's no method-to-be-wrapped in the current class, we
# should look for it in the superclass.
old ||=
context.superclass.instance_method(:"#{method_name}") rescue nil
# Since old is an unbound method, we should bind it.
# Note: An UnboundMethod of self.superclass.metaclass can't be
# bound to self.metaclass, so we "walk up" the class hierarchy.
begin
old &&= old.bind(zelf ||= self)
rescue TypeError => e
retry if zelf = zelf.superclass
end
# Finally...
new.call(old, args2, block2, self)
end
EOF
end
end
end
def wrap_module_method(*method_names, &block1) # Deprecated
if $VERBOSE or $DEBUG
$stderr.puts "Module#wrap_module_method is deprecated."
$stderr.puts "Use aModule.metaclass.wrap_method instead."
end
metaclass.wrap_method(*method_names, &block1)
end
# Since adding code at the beginning or at the
# end of an instance method is very common, we
# simplify this by providing the next methods.
# Althoug they're named *_condition, they're
# not checking anything. They should be named
# *_action. But pre_action is harder to remember
# than pre_condition. So I stick to the latter.
def pre_condition(*method_names, &block1)
pre_and_post_condition(true, false, *method_names, &block1)
end
def post_condition(*method_names, &block1)
pre_and_post_condition(false, true, *method_names, &block1)
end
def pre_and_post_condition(pre, post, *method_names, &block1)
method_names.flatten.each do |method_name|
wrap_method(method_name) do |org_method, args2, block2, obj2|
block1.call(obj2, method_name, args2, block2) if pre
res = org_method.call(*args2, &block2) if org_method
block1.call(obj2, method_name, args2, block2) if post
res
end
end
end
end
class Object
def metaclass
class << self
self
end
end
end
5.3. Unit Tests
Work in progress...
require "ev/metameta" # This is where I store my meta-meta-programming stuff.
require "test/unit"
class TestMetaMeta1 < Test::Unit::TestCase
TESTRESULT = []
class Foo
def bar
TESTRESULT << [Foo, :bar]
:foo_bar
end
pre_condition(:bar) {TESTRESULT << [Foo, :pre]}
post_condition(:bar) {TESTRESULT << [Foo, :post]}
end
class Bar < Foo
pre_condition(:bar) {TESTRESULT << [Bar, :pre]}
post_condition(:bar) {TESTRESULT << [Bar, :post]}
end
class Baz < Foo
def bar
TESTRESULT << [Baz, :bar, :before]
super
TESTRESULT << [Baz, :bar, :after]
:baz_bar
end
pre_condition(:bar) {TESTRESULT << [Baz, :pre]}
post_condition(:bar) {TESTRESULT << [Baz, :post]}
end
class Bam < Foo
def bar
TESTRESULT << [Bam, :bar, :before]
# no super
TESTRESULT << [Bam, :bar, :after]
:bam_bar
end
pre_condition(:bar) {TESTRESULT << [Bam, :pre]}
post_condition(:bar) {TESTRESULT << [Bam, :post]}
end
class Foo
pre_condition(:bar) {TESTRESULT << [Foo, :prepre]}
post_condition(:bar) {TESTRESULT << [Foo, :postpost]}
end
def setup
TESTRESULT.clear
end
def test_foo
res = Foo.new.bar
assert_equal(:foo_bar, res)
assert_equal([ [Foo, :prepre],
[Foo, :pre],
[Foo, :bar],
[Foo, :post],
[Foo, :postpost]],
TESTRESULT)
end
def test_bar
res = Bar.new.bar
assert_equal(:foo_bar, res)
assert_equal([ [Bar, :pre],
[Foo, :prepre],
[Foo, :pre],
[Foo, :bar],
[Foo, :post],
[Foo, :postpost],
[Bar, :post]],
TESTRESULT)
end
def test_baz
res = Baz.new.bar
assert_equal(:baz_bar, res)
assert_equal([ [Baz, :pre],
[Baz, :bar, :before],
[Foo, :prepre],
[Foo, :pre],
[Foo, :bar],
[Foo, :post],
[Foo, :postpost],
[Baz, :bar, :after],
[Baz, :post]],
TESTRESULT)
end
def test_bam
res = Bam.new.bar
assert_equal(:bam_bar, res)
assert_equal([ [Bam, :pre],
[Bam, :bar, :before],
[Bam, :bar, :after],
[Bam, :post]],
TESTRESULT)
end
end
class TestMetaMeta2 < Test::Unit::TestCase
TESTRESULT = []
def setup
TESTRESULT.clear
end
class Foo
def self.xyz
TESTRESULT << :xyz2
:foo_xyz
end
metaclass.pre_condition(:xyz) do |o, m, a, b|
TESTRESULT << :xyz1
end
end
class Bar < Foo
end
class Bam < Foo
metaclass.post_condition(:xyz) do |o, m, a, b|
TESTRESULT << :xyz3
end
end
def test_xyz_bar
res = Bar.xyz
assert_equal(:foo_xyz, res)
assert_equal([:xyz1, :xyz2], TESTRESULT)
end
def test_xyz_bam
res = Bam.xyz
assert_equal(nil, res)
assert_equal([:xyz3], TESTRESULT)
end
end
6. Real-World Examples
6.1. Type-Checking
In this example, we implement a type-checking mechanism with wrap_method.
require "ev/metameta" # This is where I store my meta-meta-programming stuff.
class Module
# Type checking.
# Or duck-type checking.
# Example:
# class Foo
# def_types String, Numeric, [:to_s, :gsub]
# def :bar(x, y, x)
# # x should be Numeric
# # y should be a String
# # z should respond to :to_s and :gsub
# end
# end
def def_types(*types)
metaclass.pre_condition(:method_added) do |obj, method_name, args, block|
if types
method_name = args[0]
t = types
types = nil # Avoid looping
check_types(method_name, *t)
end
end
end
# Example:
# class Foo
# def :bar(x, y, x)
# # x should be Numeric
# # y should be a String
# # z should respond to :to_s and :gsub
# end
# check_types :bar, String, Numeric, [:to_s, :gsub]
# end
def check_types(method_names, *types)
[method_names].flatten.each do |method_name|
pre_condition(method_name) do |obj, method_name, args, block|
args.each_with_index do |arg, 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
end
end
end
end
And this is a little test script:
require "ev/types"
require "test/unit"
class TestTypes < Test::Unit::TestCase
class Thing
def_types Numeric, String, [:gsub, :to_s]
def go(x, y, z)
# x should be Numeric
# y should be a String
# z should respond to :gsub and :to_s
throw "BOOM" if x == 0
[x, y, z]
end
end
def test_ok
assert_equal([7, "8", "9"], Thing.new.go(7, "8", "9"))
end
def test_first_parm_not_numeric
assert_raise(ArgumentError) {Thing.new.go("7", "8" "9")}
end
def test_second_parm_not_string
assert_raise(ArgumentError) {Thing.new.go(7, 8, "9")}
end
def test_third_parm_not_responding
assert_raise(ArgumentError) {Thing.new.go(7, "8", 9)}
end
def test_exception_comes_through
assert_raise(NameError) {Thing.new.go(0, "8", "9")}
end
end
6.2. Value-Checking
This example checks an argument of a call for certain values. It accepts arrays of values or ranges (everything that responds to :include?). nil means: no check.
require "ev/metameta" # This is where I store my meta-meta-programming stuff.
class Module
# Example:
# class Foo
# def :bar(x, y, z)
# # z should be :abc or :xyz
# end
# check_values :bar, nil, nil, [:abc, :xyz]
# end
def check_values(method_names, *values)
[method_names].flatten.each do |method_name|
pre_condition(method_name) do |obj, method_name, args, block|
args.each_with_index do |arg, n|
unless values[n].nil? or values[n].include?(args[n])
argss = args.collect{|s| s.inspect}.join(", ")
call = "%s#%s(%s)" % [self, method_name, argss]
raise ArgumentError, "value of argmument #{n+1} is invalid [#{call}]"
end
end
end
end
end
end
And this is a little test script:
require "ev/values"
require "test/unit"
class TestValues < Test::Unit::TestCase
class Thing
def go(x, y, z)
# x can have any value
# y should be in the range 20..29
# z should be :abc or :xyz
throw "BOOM" if x == 0
[x, y, z]
end
check_values :go, nil, 20..29, [:abc, :xyz]
# ^^^ ^^^ ^^^^^^ ^^^^^^^^^^^^
# method x y x
end
def test_ok
assert_equal([11, 22, :abc], Thing.new.go(11, 22, :abc))
end
def test_second_parm_not_correct
assert_raise(ArgumentError) {Thing.new.go(11, 33, :abc)}
end
def test_third_parm_not_correct
assert_raise(ArgumentError) {Thing.new.go(11, 22, :def)}
end
def test_exception_comes_through
assert_raise(NameError) {Thing.new.go(0, 22, :abc)}
end
end
6.3. Once
In this example, we implement a caching mechanism with wrap_method. The given method will be executed only once. The result is cached. The next time the method is called, the result is returned immediately. The drawback is that the object itself should be frozen to enforce some integrity. For the same reason, blocks are prohibited. You can pass true to freeze the object on the fly, or pass false to skip this check.
require "ev/metameta" # This is where I store my meta-meta-programming stuff.
require "thread"
class Module
def once(*method_names)
forced_freeze = method_names.include?(true)
ignore_frozen = method_names.include?(false)
method_names = method_names.select{|x| x.kind_of?(Symbol) or
x.kind_of?(String)}
Thread.exclusive do
pre_condition(:freeze) do |obj, method_name, args, block|
Thread.exclusive do
unless obj.instance_variable_get("@_once_results_")
obj.instance_variable_set("@_once_results_", {})
end
end
end
end
[method_names].flatten.each do |method_name|
wrap_method(method_name) do |org_method, args, block, obj|
obj.freeze if forced_freeze and not obj.frozen?
raise "object must be frozen" unless obj.frozen? or ignore_frozen
raise "block handling is not (yet) implemented" if block
results = nil
if obj.frozen?
results = obj.instance_variable_get("@_once_results_")
else
Thread.exclusive do
unless results = obj.instance_variable_get("@_once_results_")
results = obj.instance_variable_set("@_once_results_", {})
end
end
end
results[[method_name, args]] ||= org_method.call(*args) if org_method
end
end
end
end
And this is a little test script:
require "ev/once_method"
require "test/unit"
class TestOnceMethod < Test::Unit::TestCase
TESTRESULT = []
class Foo
def initialize
freeze
end
def bar3(n)
TESTRESULT << [:bar3, n]
fail "BOOM!" if n == 0
3*n
end
def bar4(n)
TESTRESULT << [:bar4, n]
4*n
end
once :bar3
end
def setup
TESTRESULT.clear
end
def test_ok
foo = Foo.new
assert_equal(18, foo.bar3(6))
assert_equal(18, foo.bar3(6))
assert_equal(21, foo.bar3(7))
assert_equal(21, foo.bar3(7))
assert_equal(32, foo.bar4(8))
assert_equal(32, foo.bar4(8))
assert_equal(36, foo.bar4(9))
assert_equal(36, foo.bar4(9))
assert_equal([ [:bar3, 6],
[:bar3, 7],
[:bar4, 8],
[:bar4, 8],
[:bar4, 9],
[:bar4, 9]], TESTRESULT)
end
def test_block_not_supported
assert_raise(RuntimeError) {Foo.new.bar3(1){2}}
end
def test_exception_comes_through
assert_raise(RuntimeError) {Foo.new.bar3(0)}
end
end
6.4. Singleton
These are implementations of singleton. (Don't use them! Stick to the original!)
require "ev/metameta" # This is where I store my meta-meta-programming stuff.
require "thread"
class Module
def singleton1
metaclass.wrap_method(:new) do |org_method, args, block, obj|
Thread.exclusive do
@_instance ||= org_method.call(*args, &block)
end
end
end
end
Or better:
require "ev/metameta" # This is where I store my meta-meta-programming stuff.
require "thread"
class Module
def singleton2
metaclass.wrap_method(:new) do |org_method, args, block, obj|
Thread.exclusive do
unless @_instance
@_instance = org_method.call(*args, &block)
metaclass.wrap_method(:new) do # Use define_method for pure speed...
@_instance
end
end
end
@_instance
end
end
end
We could even use once.
require "ev/once_method"
class Module
def singleton3
metaclass.once :new, true
end
end
And this is a little test script:
require "ev/singleton_by_wrap_1"
require "ev/singleton_by_wrap_2"
require "ev/singleton_by_wrap_3"
require "test/unit"
class TestSingleton123 < Test::Unit::TestCase
class Foo1
singleton1
end
class Foo2
singleton2
end
class Foo3
singleton3
end
def test_singleton1
id1 = Foo1.new.object_id
id2 = Foo1.new.object_id
id3 = Foo1.new.object_id
assert_equal(id1, id2)
assert_equal(id1, id3)
end
def test_singleton2
id1 = Foo2.new.object_id
id2 = Foo2.new.object_id
id3 = Foo2.new.object_id
assert_equal(id1, id2)
assert_equal(id1, id3)
end
def test_singleton3
id1 = Foo3.new.object_id
id2 = Foo3.new.object_id
id3 = Foo3.new.object_id
assert_equal(id1, id2)
assert_equal(id1, id3)
end
end
6.5. Abstract Methods
This enforces subclasses to implement certain methods.
(This really slows down your application. You might want to use it only in debug and test environments.)
require "ev/metameta"
class Class
def abstract_method(*method_names)
metaclass.pre_condition(:new) do |obj, method_name, args, block|
method_names.each do |method_name|
obj.instance_eval do
unless instance_methods.include?(method_name.to_s)
raise NotImplementedError,
"Class #{self} doesn't implement method #{method_name}."
end
end
end
end
end
end
And this is a little test script:
require "ev/abstract"
require "test/unit"
class TestAbstractMethods < Test::Unit::TestCase
class Foo
abstract_method :bam # if `hostname` == ...
end
class Bar < Foo
end
class Baz < Foo
def bam
:ok
end
end
def test_foo
assert_raise(NotImplementedError){Foo.new}
end
def test_bar
assert_raise(NotImplementedError){Bar.new}
end
def test_baz
assert_nothing_raised{Baz.new}
end
end
aior all allinone allinoneruby aop applications archive aspect aspectorientedprogramming bin browser by code codesnippet codesnippets compile compiler computer computerlanguage contract design designbycontract dialog dialogs distribute distributing distributingrubyapplications distribution eee eee-file eeefile erik erikveen erikveenstra exe executable exerb file graphical graphicaluserinterface gui gz html http httpserver iloveruby interface jar jit just justintime lang language meta metametaprogramming metaprogramming one oriented pack package packaging packing packingrubyapplications programming programminglanguage rar rb rb2bin rb2exe rba rbarchive rbtobin rbtoexe rbw ruby ruby2bin ruby2exe rubyapplications rubyarchive rubycompiler rubyscript rubyscript2 rubyscript2exe rubyscripts rubyscripttoexe rubytobin rubytoexe rubyweb rubywebdialog rubywebdialogs script scripts server snippet snippets t2rb t2rs tar tar2rb tar2rbscript tar2rs tar2rscript time ui user userinterface veenstra web webbrowser webdialog webdialogs window windowinbrowser windows wrap wrapper wxruby zip