chainable 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +90 -5
- data/benchmark/chainable.rb +54 -35
- data/lib/chainable.rb +77 -12
- data/spec/chainable/auto_chain_spec.rb +4 -4
- data/spec/chainable/chain_method_spec.rb +2 -2
- data/spec/chainable/chain_method_try_merge_spec.rb +72 -0
- data/spec/chainable/merge_method_spec.rb +39 -0
- metadata +5 -1
data/README.rdoc
CHANGED
@@ -2,12 +2,16 @@
|
|
2
2
|
|
3
3
|
This is heavy ruby abuse. It even got the Evil of the Day Award™ from zenspider.
|
4
4
|
|
5
|
+
Forks are welcome!
|
6
|
+
|
5
7
|
== Thou shalt not use alias_method_chain!
|
6
8
|
- http://yehudakatz.com/2009/03/06/alias_method_chain-in-models
|
7
9
|
- http://yehudakatz.com/2009/01/18/other-ways-to-wrap-a-method
|
8
10
|
- http://www.codefluency.com/articles/2009/01/03/wrapping-a-method-in-ruby
|
9
11
|
|
10
12
|
== What it does
|
13
|
+
|
14
|
+
=== Chaining Methods
|
11
15
|
Chainable is an alternative to alias_method_chain, that uses inheritance, rather
|
12
16
|
than aliasing. It does the following when "chaining" a method:
|
13
17
|
|
@@ -74,15 +78,96 @@ Of course you can do this with any class (or module):
|
|
74
78
|
|
75
79
|
Note that there is a speed advantage when using chain_method without a block
|
76
80
|
and doing a "def", since chain_method will use define_method if a block is
|
77
|
-
given, which produces slower methods
|
81
|
+
given, which produces slower methods.
|
82
|
+
|
83
|
+
=== Merging Methods
|
84
|
+
|
85
|
+
But wait, there is more:
|
86
|
+
|
87
|
+
class Foo
|
88
|
+
def foo
|
89
|
+
10
|
90
|
+
end
|
91
|
+
merge_method :foo do
|
92
|
+
super * 3
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
puts Ruby2Ruby.translate Foo, :foo
|
97
|
+
|
98
|
+
The output:
|
99
|
+
|
100
|
+
def foo
|
101
|
+
(10) * 3
|
102
|
+
end
|
103
|
+
|
104
|
+
<b>Before you yell at me about how insane I am, read on!</b>
|
105
|
+
|
106
|
+
The library will only allow merging, if it thinks, it is possible:
|
107
|
+
|
108
|
+
class Foo
|
109
|
+
def foo
|
110
|
+
x = 10
|
111
|
+
end
|
112
|
+
merge_method :foo do
|
113
|
+
x = 20
|
114
|
+
super
|
115
|
+
puts x
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
Will give you:
|
120
|
+
|
121
|
+
ArgumentError: cannot merge foo.
|
122
|
+
|
123
|
+
Same goes for this one:
|
124
|
+
|
125
|
+
class Foo
|
126
|
+
def foo x
|
127
|
+
puts x
|
128
|
+
end
|
129
|
+
merge_method :foo do
|
130
|
+
super
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# => ArgumentError: cannot merge foo.
|
135
|
+
|
136
|
+
But where is the fun in that one? You probably don't want your ruby script
|
137
|
+
throwing such errors at you.
|
138
|
+
|
139
|
+
Enter "try_merge":
|
140
|
+
|
141
|
+
SomeEvilClassWithoutHooks.class_eval do
|
142
|
+
chain_method instance_methods(false), :try_merge => true do
|
143
|
+
old_value = self.value.dup
|
144
|
+
super.tap { observer.notify if old_value != value }
|
145
|
+
end
|
146
|
+
attr_accessor :observer
|
147
|
+
end
|
148
|
+
|
149
|
+
some_evil_instance.observer = MyObserver.new
|
150
|
+
|
151
|
+
== When to use it?
|
152
|
+
|
153
|
+
As with alias_method_chain, you should use this as seldom as possible. Prefer
|
154
|
+
clean inheritance over evil hacks. There actually is only one case one may use
|
155
|
+
chainable (or alias_method_chain, for that matter): If there is a class you
|
156
|
+
need to modify that is not part of your own code and the instances you deal with
|
157
|
+
may already exists when you modify the class. In case you can modify the class
|
158
|
+
before instance creation, just create another class inheriting from the first
|
159
|
+
one and overwrite new to return instances of the latter.
|
78
160
|
|
79
161
|
== Benchmark
|
80
162
|
chain_method tends do produce slightly faster methods than alias_method_chain:
|
163
|
+
$ rake benchmark
|
81
164
|
user system total real
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
165
|
+
no wrappers 0.000000 0.000000 0.000000 ( 0.004887)
|
166
|
+
merge_method 0.000000 0.000000 0.000000 ( 0.004830)
|
167
|
+
chain_method (def) 1.040000 0.350000 1.390000 ( 1.392329)
|
168
|
+
chain_method (define_method) 1.150000 0.240000 1.390000 ( 1.396007)
|
169
|
+
alias_method_chain (def) 1.210000 0.260000 1.470000 ( 1.472633)
|
170
|
+
alias_method_chain (define_method) 3.470000 0.590000 4.060000 ( 4.096245)
|
86
171
|
|
87
172
|
== Installation
|
88
173
|
Add github gems, if you haven't already:
|
data/benchmark/chainable.rb
CHANGED
@@ -2,50 +2,69 @@ require "benchmark"
|
|
2
2
|
require "lib/chainable"
|
3
3
|
require "active_support"
|
4
4
|
|
5
|
-
class
|
6
|
-
|
7
|
-
CALL_TIMES =
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
5
|
+
class BenchMe
|
6
|
+
|
7
|
+
CALL_TIMES = 5000
|
8
|
+
DEF_TIMES = 500
|
9
|
+
BENCH_ME = []
|
10
|
+
|
11
|
+
def self.report
|
12
|
+
@report || "unknown"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.inherited klass
|
16
|
+
BENCH_ME << klass
|
17
|
+
klass.class_eval "def a_method; nil; end"
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.run
|
21
|
+
Benchmark.bmbm do |x|
|
22
|
+
BENCH_ME.each do |klass|
|
23
|
+
object = klass.new
|
24
|
+
x.report(klass.report) { CALL_TIMES.times { object.a_method } }
|
25
|
+
end
|
16
26
|
end
|
17
27
|
end
|
18
|
-
|
19
|
-
def bm2; end
|
28
|
+
|
20
29
|
end
|
21
30
|
|
22
|
-
class
|
23
|
-
@
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
31
|
+
class NoWrappers < BenchMe
|
32
|
+
@report = "no wrappers"
|
33
|
+
end
|
34
|
+
|
35
|
+
class MergeMethod < BenchMe
|
36
|
+
@report = "merge_method"
|
37
|
+
DEF_TIMES.times { merge_method(:a_method) { super } }
|
38
|
+
end
|
39
|
+
|
40
|
+
class ChainMethodDef < BenchMe
|
41
|
+
@report = "chain_method (def)"
|
42
|
+
DEF_TIMES.times do
|
43
|
+
chain_method(:a_method)
|
44
|
+
def a_method; super; end
|
28
45
|
end
|
29
46
|
end
|
30
47
|
|
31
|
-
class
|
32
|
-
@
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
48
|
+
class ChainMethod < BenchMe
|
49
|
+
@report = "chain_method (define_method)"
|
50
|
+
DEF_TIMES.times { chain_method(:a_method) { super } }
|
51
|
+
end
|
52
|
+
|
53
|
+
class AliasMethodChainDef < BenchMe
|
54
|
+
@report = "alias_method_chain (def)"
|
55
|
+
DEF_TIMES.times do |i|
|
56
|
+
eval "def a_method_with_#{i}; a_method_without_#{i}; end"
|
57
|
+
alias_method_chain :a_method, i
|
39
58
|
end
|
40
59
|
end
|
41
60
|
|
42
|
-
|
43
|
-
|
61
|
+
class AliasMethodChain < BenchMe
|
62
|
+
@report = "alias_method_chain (define_method)"
|
63
|
+
DEF_TIMES.times do |i|
|
64
|
+
without = "a_method_without_#{i}".to_sym
|
65
|
+
define_method("a_method_with_#{i}") { send without }
|
66
|
+
alias_method_chain :a_method, i
|
67
|
+
end
|
44
68
|
end
|
45
69
|
|
46
|
-
|
47
|
-
BenchmarkChainable.bm1(x)
|
48
|
-
BenchmarkAliasMethodChain.bm1(x)
|
49
|
-
BenchmarkChainable.bm2(x)
|
50
|
-
BenchmarkAliasMethodChain.bm2(x)
|
51
|
-
end
|
70
|
+
BenchMe.run
|
data/lib/chainable.rb
CHANGED
@@ -9,26 +9,91 @@ module Chainable
|
|
9
9
|
@auto_chain = false
|
10
10
|
end
|
11
11
|
|
12
|
+
def self.wrapped_source klass, name, wrapper
|
13
|
+
begin
|
14
|
+
inner = unifier.process(parse_tree.parse_tree_for_method(klass, name))
|
15
|
+
outer = unifier.process(parse_tree.parse_tree_for_proc(wrapper))
|
16
|
+
rescue Exception
|
17
|
+
raise ArgumentError, "cannot merge #{name}"
|
18
|
+
end
|
19
|
+
raise ArgumentError, "cannot merge #{name}" if inner[2] != s(:args) or outer[2]
|
20
|
+
inner_locals = []
|
21
|
+
sexp_walk(inner) do |e|
|
22
|
+
raise ArgumentError, "cannot merge #{name}" if [:zsuper, :super].include? e[0]
|
23
|
+
inner_locals << e if e[0] == :lvar
|
24
|
+
end
|
25
|
+
sexp_walk(outer) do |e|
|
26
|
+
if inner_locals.include? e or (e[0] == :super and e.length > 1)
|
27
|
+
raise ArgumentError, "cannot merge #{name}"
|
28
|
+
end
|
29
|
+
e.replace inner[3][1] if [:zsuper, :super].include? e[0]
|
30
|
+
end
|
31
|
+
src = Ruby2Ruby.new.process s(:defn, name, s(:args), s(:scope, s(:block, outer[3])))
|
32
|
+
src.gsub "# do nothing", "nil"
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.unifier
|
36
|
+
return @unifier if @unifier
|
37
|
+
@unifier = Unifier.new
|
38
|
+
# HACK (stolen from ruby2ruby)
|
39
|
+
@unifier.processors.each { |p| p.unsupported.delete :cfunc }
|
40
|
+
@unifier
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.parse_tree
|
44
|
+
return @parse_tree if @parse_tree
|
45
|
+
require "parse_tree"
|
46
|
+
@parse_tree = ParseTree.new
|
47
|
+
end
|
48
|
+
|
12
49
|
# This will "chain" a method (read: push it to a module and include it).
|
13
50
|
# If a block is given, it will do a define_method(name, &block).
|
14
51
|
# Maybe that is not what you want, as methods defined by def tend to be
|
15
52
|
# faster. If that is the case, simply don't pass the block and call def
|
16
53
|
# after chain_method instead.
|
17
54
|
def chain_method(*names, &block)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
55
|
+
options = names.grep(Hash).inject({}) { |a, b| a.merge names.delete(b) }
|
56
|
+
if options[:try_merge]
|
57
|
+
names.reject! do |name|
|
58
|
+
begin
|
59
|
+
merge_method(name, &block)
|
60
|
+
true
|
61
|
+
rescue ArgumentError
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
names.each do |name|
|
67
|
+
name = name.to_s
|
68
|
+
if instance_methods(false).include? name
|
69
|
+
begin
|
70
|
+
code = Ruby2Ruby.translate self, name
|
71
|
+
include Module.new { eval code }
|
72
|
+
rescue NameError
|
73
|
+
m = instance_method name
|
74
|
+
include Module.new { define_method(name) { |*a, &b| m.bind(self).call(*a, &b) } }
|
75
|
+
end
|
27
76
|
end
|
77
|
+
block ||= Proc.new { super }
|
78
|
+
define_method(name, &block)
|
28
79
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
80
|
+
end
|
81
|
+
|
82
|
+
def merge_method(*names, &block)
|
83
|
+
names.each do |name|
|
84
|
+
name = name.to_s
|
85
|
+
unless instance_methods(false).include? name
|
86
|
+
define_method(name, &block)
|
87
|
+
next
|
88
|
+
end
|
89
|
+
class_eval Chainable.wrapped_source(self, name, block)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.sexp_walk sexp, &block
|
94
|
+
return unless sexp.is_a? Sexp
|
95
|
+
yield sexp
|
96
|
+
sexp.each { |e| sexp_walk e, &block }
|
32
97
|
end
|
33
98
|
|
34
99
|
# If you define a method inside a block passed to auto_chain, chain_method
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require "lib/chainable"
|
2
2
|
|
3
|
-
describe Chainable do
|
3
|
+
describe "Chainable#auto_chain" do
|
4
4
|
|
5
|
-
it "should chain all methods defined inside
|
5
|
+
it "should chain all methods defined inside given block" do
|
6
6
|
a_class = Class.new do
|
7
7
|
auto_chain do
|
8
8
|
define_method(:foo) { 100 }
|
@@ -13,7 +13,7 @@ describe Chainable do
|
|
13
13
|
a_class.new.foo.should == 222
|
14
14
|
end
|
15
15
|
|
16
|
-
it "should allow defining methods both inside and outside of
|
16
|
+
it "should allow defining methods both inside and outside of the block" do
|
17
17
|
a_class = Class.new do
|
18
18
|
define_method(:foo) { 100 }
|
19
19
|
chain_method :foo
|
@@ -23,7 +23,7 @@ describe Chainable do
|
|
23
23
|
a_class.new.foo.should == 222
|
24
24
|
end
|
25
25
|
|
26
|
-
it "should
|
26
|
+
it "should work with core functions" do
|
27
27
|
# We screw with String#reverse, this could mess up other specs.
|
28
28
|
String.class_eval do
|
29
29
|
chain_method :reverse # or we would overwrite the original
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require "lib/chainable"
|
2
2
|
|
3
|
-
describe Chainable do
|
3
|
+
describe "Chainable#chain_method" do
|
4
4
|
|
5
5
|
before :each do
|
6
6
|
@a_class = Class.new do
|
@@ -60,7 +60,7 @@ describe Chainable do
|
|
60
60
|
@an_instance.to_i.should == @original_results["to_i"] + 20
|
61
61
|
end
|
62
62
|
|
63
|
-
it "should allow passing multiple method names
|
63
|
+
it "should allow passing multiple method names" do
|
64
64
|
@a_class.class_eval do
|
65
65
|
chain_method :random, :foo2
|
66
66
|
chain_method(:foo, :to_i) { super.to_s }
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "lib/chainable"
|
2
|
+
|
3
|
+
describe "Chainable#chain_method(..., :try_merge => true)" do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@a_class = Class.new do
|
7
|
+
def foo
|
8
|
+
:foo
|
9
|
+
end
|
10
|
+
def foo2
|
11
|
+
foo.to_s.upcase
|
12
|
+
end
|
13
|
+
def to_i
|
14
|
+
42
|
15
|
+
end
|
16
|
+
def inspect
|
17
|
+
random
|
18
|
+
super
|
19
|
+
end
|
20
|
+
define_method(:random) { @some_value ||= rand(1000) }
|
21
|
+
end
|
22
|
+
@an_instance = @a_class.new
|
23
|
+
@original_results = @a_class.instance_methods(false).inject({}) do |h, m|
|
24
|
+
h.merge m => @an_instance.send(m)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should not change the behaviour of the original methods" do
|
29
|
+
5.times do
|
30
|
+
@original_results.each do |method_name, method_result|
|
31
|
+
@a_class.class_eval { chain_method method_name, :try_merge => true }
|
32
|
+
@an_instance.send(method_name).should == method_result
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should work for core methods" do
|
38
|
+
String.class_eval do
|
39
|
+
chain_method(:upcase, :try_merge => true) do |*x|
|
40
|
+
return super if x.empty?
|
41
|
+
x.first
|
42
|
+
end
|
43
|
+
end
|
44
|
+
"foo".upcase("bar").should == "bar"
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should define a new method, when block given" do
|
48
|
+
@a_class.class_eval do
|
49
|
+
chain_method(:to_i, :try_merge => true) { super - 5 }
|
50
|
+
chain_method(:foo2, :try_merge => true) { "foo2" }
|
51
|
+
end
|
52
|
+
@an_instance.to_i.should == @original_results["to_i"] - 5
|
53
|
+
@an_instance.foo2.should == "foo2"
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should allow passing multiple method names" do
|
57
|
+
@a_class.class_eval do
|
58
|
+
chain_method(:foo, :to_i, :try_merge => true) { super.to_s }
|
59
|
+
end
|
60
|
+
@an_instance.foo.should == @original_results["foo"].to_s
|
61
|
+
@an_instance.to_i.should == @original_results["to_i"].to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should merge, if possible" do
|
65
|
+
old_ancestors = @a_class.ancestors
|
66
|
+
@a_class.class_eval do
|
67
|
+
chain_method(:to_i, :foo, :try_merge => true) { super.to_s }
|
68
|
+
end
|
69
|
+
@a_class.ancestors.should == old_ancestors
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "lib/chainable"
|
2
|
+
|
3
|
+
describe "Chainable#merge_method" do
|
4
|
+
|
5
|
+
it "should be able to merge \"simple\" methods" do
|
6
|
+
Class.new do
|
7
|
+
old_ancestors = ancestors
|
8
|
+
define_method(:simple) { 100 + 100 }
|
9
|
+
new.simple.should == 200
|
10
|
+
merge_method(:simple) { super - 100 }
|
11
|
+
new.simple.should == 100
|
12
|
+
ancestors.should == old_ancestors
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should refuse merging methods, if the merge would cause harm" do
|
17
|
+
forbidden = []
|
18
|
+
forbidden << lambda do
|
19
|
+
Class.new do
|
20
|
+
define_method(:same_lvars) { x = 10; x * 2 }
|
21
|
+
merge_method(:same_lvars) { x = 5; super; puts x }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
forbidden << lambda do
|
25
|
+
Class.new do
|
26
|
+
define_method(:args) { |x| x }
|
27
|
+
merge_method(:args) { super }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
forbidden << lambda do
|
31
|
+
Class.new do
|
32
|
+
define_method(:args) { |x| x = 0 }
|
33
|
+
merge_method(:args) { |x| x.to_s; super; puts x }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
forbidden.each { |block| block.should raise_error(ArgumentError) }
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chainable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Konstantin Haase
|
@@ -33,7 +33,9 @@ extra_rdoc_files:
|
|
33
33
|
- LICENSE
|
34
34
|
- lib/chainable.rb
|
35
35
|
- spec/chainable/auto_chain_spec.rb
|
36
|
+
- spec/chainable/merge_method_spec.rb
|
36
37
|
- spec/chainable/chain_method_spec.rb
|
38
|
+
- spec/chainable/chain_method_try_merge_spec.rb
|
37
39
|
- benchmark/chainable.rb
|
38
40
|
files:
|
39
41
|
- LICENSE
|
@@ -41,7 +43,9 @@ files:
|
|
41
43
|
- README.rdoc
|
42
44
|
- lib/chainable.rb
|
43
45
|
- spec/chainable/auto_chain_spec.rb
|
46
|
+
- spec/chainable/merge_method_spec.rb
|
44
47
|
- spec/chainable/chain_method_spec.rb
|
48
|
+
- spec/chainable/chain_method_try_merge_spec.rb
|
45
49
|
- benchmark/chainable.rb
|
46
50
|
has_rdoc: true
|
47
51
|
homepage: http://rkh.github.com/chainable
|