tco_method 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +86 -14
- data/lib/tco_method/mixin.rb +6 -7
- data/lib/tco_method/version.rb +1 -1
- data/lib/tco_method.rb +2 -1
- data/test/unit/mixin_test.rb +50 -20
- data/test/unit/tco_method_test.rb +96 -53
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa6f5ef52346f81e6dda4617f58c78d8ea6268fd
|
4
|
+
data.tar.gz: 3d38f3813816bc489c5329fe006e679f67041432
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3ab604141152bb7c7fc76846a4ecb7edd216b73dd4d134bb07faa13bda12901aa780a5dc34262ec0259d04ab9123a2b486228980dca9684440b3443c16a057e7
|
7
|
+
data.tar.gz: 7f1ced9785c6aebf644253d52a2a1c0973d94a1efe2fcc84e6aef3ca099c57e54bbc72c5767b0ea61ffb5b33dcc9d34e85b154843ff0a664881097d22fc05281
|
data/README.md
CHANGED
@@ -8,15 +8,15 @@
|
|
8
8
|
|
9
9
|
Provides `TCOMethod::Mixin` for extending Classes and Modules with helper methods
|
10
10
|
to facilitate evaluating code and some types of methods with tail call
|
11
|
-
optimization enabled. Also provides `TCOMethod.tco_eval` providing
|
12
|
-
to evaluate code strings with tail call optimization enabled.
|
11
|
+
optimization enabled. Also provides `TCOMethod.tco_eval` providing a direct and
|
12
|
+
easy means to evaluate code strings with tail call optimization enabled.
|
13
13
|
|
14
14
|
## Installation
|
15
15
|
|
16
16
|
Add this line to your application's Gemfile:
|
17
17
|
|
18
|
-
```
|
19
|
-
gem
|
18
|
+
```ruby
|
19
|
+
gem "tco_method"
|
20
20
|
```
|
21
21
|
|
22
22
|
And then execute:
|
@@ -33,13 +33,18 @@ $ gem install tco_method
|
|
33
33
|
|
34
34
|
## Usage
|
35
35
|
|
36
|
-
Require the `TCOMethod`
|
36
|
+
Require the [`TCOMethod`](http://www.rubydoc.info/gems/tco_method/TCOMethod)
|
37
|
+
library:
|
37
38
|
|
38
39
|
```ruby
|
39
40
|
require "tco_method"
|
40
41
|
```
|
41
42
|
|
42
|
-
Extend a class with the `TCOMethod::Mixin`
|
43
|
+
Extend a class with the [`TCOMethod::Mixin`](http://www.rubydoc.info/gems/tco_method/TCOMethod/Mixin)
|
44
|
+
and let the fun begin!
|
45
|
+
|
46
|
+
To redefine an instance method with tail call optimization enabled, use
|
47
|
+
[`tco_method`](http://www.rubydoc.info/gems/tco_method/TCOMethod/Mixin:tco_method):
|
43
48
|
|
44
49
|
```ruby
|
45
50
|
class MyClass
|
@@ -51,12 +56,31 @@ class MyClass
|
|
51
56
|
tco_method :factorial
|
52
57
|
end
|
53
58
|
|
54
|
-
MyClass.new.factorial(10_000).to_s.length
|
59
|
+
puts MyClass.new.factorial(10_000).to_s.length
|
55
60
|
# => 35660
|
56
61
|
```
|
57
62
|
|
58
|
-
Or, use `
|
59
|
-
|
63
|
+
Or alternatively, use [`tco_module_method`](http://www.rubydoc.info/gems/tco_method/TCOMethod/Mixin:tco_module_method)
|
64
|
+
or [`tco_class_method`](http://www.rubydoc.info/gems/tco_method/TCOMethod/Mixin:tco_module_method)
|
65
|
+
for a Module or Class method:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
module MyFibonacci
|
69
|
+
extend TCOMethod::Mixin
|
70
|
+
|
71
|
+
def self.fibonacci(index, back_one = 1, back_two = 0)
|
72
|
+
index < 1 ? back_two : fibonacci(index - 1, back_one + back_two, back_one)
|
73
|
+
end
|
74
|
+
tco_module_method :fibonacci
|
75
|
+
end
|
76
|
+
|
77
|
+
puts MyFibonacci.fibonacci(10_000).to_s.length
|
78
|
+
# => 2090
|
79
|
+
```
|
80
|
+
|
81
|
+
Or, for more power and flexibility (at the cost of stringified code blocks) use
|
82
|
+
[`TCOMethod.tco_eval`](http://www.rubydoc.info/gems/tco_method/TCOMethod/Mixin:tco_eval)
|
83
|
+
directly:
|
60
84
|
|
61
85
|
```ruby
|
62
86
|
TCOMethod.tco_eval(<<-CODE)
|
@@ -71,15 +95,63 @@ MyClass.new.factorial(10_000).to_s.length
|
|
71
95
|
# => 35660
|
72
96
|
```
|
73
97
|
|
74
|
-
|
98
|
+
You can kind of get around this by dynamically reading the code you want to
|
99
|
+
compile with tail call optimization, but this approach also has downsides in
|
100
|
+
that it goes around the standard Ruby `require` model. For example, consider the
|
101
|
+
Fibonacci example broken across two scripts, one script serving as a loader and
|
102
|
+
the other script acting as a more standard library:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
# loader.rb
|
106
|
+
|
107
|
+
require "tco_method"
|
108
|
+
fibonacci_lib = File.read(File.expand_path("../fibonacci.rb", __FILE__))
|
109
|
+
TCOMethod.tco_eval(fibonacci_lib)
|
75
110
|
|
76
|
-
|
111
|
+
puts MyFibonacci.fibonacci(10_000).to_s.length
|
112
|
+
# => 2090
|
77
113
|
|
78
|
-
|
79
|
-
|
114
|
+
|
115
|
+
# fibonacci.rb
|
116
|
+
|
117
|
+
module MyFibonacci
|
118
|
+
def self.fibonacci(index, back_one = 1, back_two = 0)
|
119
|
+
index < 1 ? back_two : fibonacci(index - 1, back_one + back_two, back_one)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
If you really want to get crazy, you could include the `TCOMethod::Mixin` module
|
125
|
+
in the Module class and add these behaviors to all Modules and Classes. To quote
|
126
|
+
VIM plugin author extraordinaire Tim Pope, "I don't like to get crazy." Consider
|
127
|
+
yourself warned.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
# Don't say I didn't warn you...
|
131
|
+
|
132
|
+
Module.include(TCOMethod::Mixin)
|
133
|
+
|
134
|
+
module MyFibonacci
|
135
|
+
def self.fibonacci(index, back_one = 1, back_two = 0)
|
136
|
+
index < 1 ? back_two : fibonacci(index - 1, back_one + back_two, back_one)
|
137
|
+
end
|
138
|
+
tco_module_method :fibonacci
|
139
|
+
end
|
140
|
+
|
141
|
+
puts MyFibonacci.fibonacci(10_000).to_s.length
|
142
|
+
# => 2090
|
143
|
+
```
|
144
|
+
|
145
|
+
## Gotchas
|
146
|
+
|
147
|
+
Quirks with Module and Class annotations:
|
148
|
+
- Annotations only work with methods defined using the `def` keyword.
|
149
|
+
- Annotations use the [`method_source` gem](https://github.com/banister/method_source)
|
80
150
|
to retrieve the method source to reevaluate. As a result, class annotations
|
81
151
|
can act strangely when used in more dynamic contexts like `irb` or `pry`.
|
82
|
-
|
152
|
+
- Annotations reopen the Module or Class by name to redefine the given method.
|
153
|
+
This process will fail for dynamic Modules and Classes that aren't assigned to
|
154
|
+
constants and, ergo, don't have names.
|
83
155
|
|
84
156
|
I'm sure there are more and I will document them here as I come across them.
|
85
157
|
|
data/lib/tco_method/mixin.rb
CHANGED
@@ -4,18 +4,18 @@ module TCOMethod
|
|
4
4
|
# call optimization enabled and re-evaluating existing methods with tail call
|
5
5
|
# optimization enabled.
|
6
6
|
module Mixin
|
7
|
-
# Class annotation causing the class or module method identified
|
8
|
-
# given method name to be reevaluated with tail call optimization
|
9
|
-
# Only works for methods defined using the `def` keyword.
|
7
|
+
# Module or Class annotation causing the class or module method identified
|
8
|
+
# by the given method name to be reevaluated with tail call optimization
|
9
|
+
# enabled. Only works for methods defined using the `def` keyword.
|
10
10
|
#
|
11
11
|
# @param [String, Symbol] method_name The name of the class or module method
|
12
12
|
# that should be reeevaluated with tail call optimization enabled.
|
13
13
|
# @return [Symbol] The symbolized method name.
|
14
14
|
# @see TCOMethod.reevaluate_method_with_tco
|
15
|
-
def
|
16
|
-
TCOMethod.reevaluate_method_with_tco(self, method_name, :
|
15
|
+
def tco_module_method(method_name)
|
16
|
+
TCOMethod.reevaluate_method_with_tco(self, method_name, :module)
|
17
17
|
end
|
18
|
-
alias_method :
|
18
|
+
alias_method :tco_class_method, :tco_module_method
|
19
19
|
|
20
20
|
# Evaluate the given code String with tail call optimization enabled.
|
21
21
|
#
|
@@ -39,7 +39,6 @@ module TCOMethod
|
|
39
39
|
# @see TCOMethod.reevaluate_method_with_tco
|
40
40
|
def tco_method(method_name)
|
41
41
|
TCOMethod.reevaluate_method_with_tco(self, method_name, :instance)
|
42
|
-
method_name.to_sym
|
43
42
|
end
|
44
43
|
end
|
45
44
|
end
|
data/lib/tco_method/version.rb
CHANGED
data/lib/tco_method.rb
CHANGED
@@ -47,8 +47,9 @@ module TCOMethod
|
|
47
47
|
if method_info.type != :method
|
48
48
|
raise TypeError, "Invalid method type: #{method_info.type}"
|
49
49
|
end
|
50
|
+
receiver_class = receiver.is_a?(Class) ? :class : :module
|
50
51
|
code = <<-CODE
|
51
|
-
|
52
|
+
#{receiver_class} #{receiver.name}
|
52
53
|
#{existing_method.source}
|
53
54
|
end
|
54
55
|
CODE
|
data/test/unit/mixin_test.rb
CHANGED
@@ -3,37 +3,67 @@ require "test_helper"
|
|
3
3
|
class MixinTest < TCOMethod::TestCase
|
4
4
|
include TCOMethod::TestHelpers::FactorialStackBusterHelper
|
5
5
|
|
6
|
-
TestClass = Class.new
|
7
|
-
|
8
|
-
|
6
|
+
TestClass = Class.new { extend TCOMethod::Mixin }
|
7
|
+
TestModule = Module.new { extend TCOMethod::Mixin }
|
8
|
+
|
9
9
|
|
10
|
-
|
10
|
+
context "Module extensions" do
|
11
|
+
subject { TestModule }
|
11
12
|
|
12
|
-
|
13
|
-
context "##{method_alias}" do
|
13
|
+
context "#tco_module_method" do
|
14
14
|
should "call TCOMethod.reevaluate_method_with_tco with expected arguments" do
|
15
15
|
method_name = :some_method
|
16
|
-
args = [
|
16
|
+
args = [subject, method_name, :module]
|
17
17
|
TCOMethod.expects(:reevaluate_method_with_tco).with(*args)
|
18
|
-
subject.
|
18
|
+
subject.tco_module_method(method_name)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "#tco_eval" do
|
23
|
+
should "call TCOMethod.eval with expected arguments" do
|
24
|
+
code = "some_code"
|
25
|
+
TCOMethod.expects(:tco_eval).with(code)
|
26
|
+
subject.tco_eval(code)
|
19
27
|
end
|
20
28
|
end
|
21
|
-
end
|
22
29
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
30
|
+
context "#tco_method" do
|
31
|
+
should "call TCOMethod.reevaluate_method_with_tco with expected arguments" do
|
32
|
+
method_name = :some_method
|
33
|
+
args = [subject, method_name, :instance]
|
34
|
+
TCOMethod.expects(:reevaluate_method_with_tco).with(*args)
|
35
|
+
subject.tco_method(method_name)
|
36
|
+
end
|
28
37
|
end
|
29
38
|
end
|
30
39
|
|
31
|
-
context "
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
TCOMethod.
|
36
|
-
|
40
|
+
context "Class extensions" do
|
41
|
+
subject { TestClass }
|
42
|
+
|
43
|
+
context "#tco_class_method" do
|
44
|
+
should "call TCOMethod.reevaluate_method_with_tco with expected arguments" do
|
45
|
+
method_name = :some_method
|
46
|
+
args = [subject, method_name, :module]
|
47
|
+
TCOMethod.expects(:reevaluate_method_with_tco).with(*args)
|
48
|
+
subject.tco_class_method(method_name)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "#tco_eval" do
|
53
|
+
should "call TCOMethod.eval with expected arguments" do
|
54
|
+
code = "some_code"
|
55
|
+
TCOMethod.expects(:tco_eval).with(code)
|
56
|
+
subject.tco_eval(code)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "#tco_method" do
|
61
|
+
should "call TCOMethod.reevaluate_method_with_tco with expected arguments" do
|
62
|
+
method_name = :some_method
|
63
|
+
args = [subject, method_name, :instance]
|
64
|
+
TCOMethod.expects(:reevaluate_method_with_tco).with(*args)
|
65
|
+
subject.tco_method(method_name)
|
66
|
+
end
|
37
67
|
end
|
38
68
|
end
|
39
69
|
end
|
@@ -5,13 +5,21 @@ class TCOMethodTest < TCOMethod::TestCase
|
|
5
5
|
|
6
6
|
Subject = TCOMethod
|
7
7
|
|
8
|
-
|
8
|
+
test_subject_builder = proc do
|
9
9
|
extend TCOMethod::Mixin
|
10
10
|
|
11
|
-
class << self
|
12
|
-
define_method(:
|
11
|
+
class << self
|
12
|
+
define_method(:singleton_block_method) { }
|
13
13
|
end
|
14
14
|
|
15
|
+
# Equivalent to the below, but provides a target for verifying that
|
16
|
+
# tco_module_method works on Classes and tco_class_method works on Modules.
|
17
|
+
def self.module_factorial(n, acc = 1)
|
18
|
+
n <= 1 ? acc : module_factorial(n - 1, n * acc)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Equivalent to the above, but provides a target for verifying that
|
22
|
+
# tco_module_method works on Classes and tco_class_method works on Modules.
|
15
23
|
def self.class_factorial(n, acc = 1)
|
16
24
|
n <= 1 ? acc : class_factorial(n - 1, n * acc)
|
17
25
|
end
|
@@ -23,6 +31,9 @@ class TCOMethodTest < TCOMethod::TestCase
|
|
23
31
|
end
|
24
32
|
end
|
25
33
|
|
34
|
+
TestModule = Module.new(&test_subject_builder)
|
35
|
+
TestClass = Class.new(&test_subject_builder)
|
36
|
+
|
26
37
|
subject { Subject }
|
27
38
|
|
28
39
|
context Subject.name do
|
@@ -67,73 +78,105 @@ class TCOMethodTest < TCOMethod::TestCase
|
|
67
78
|
context "::reevaluate_method_with_tco" do
|
68
79
|
subject { Subject.method(:reevaluate_method_with_tco) }
|
69
80
|
|
70
|
-
|
71
|
-
|
72
|
-
assert_raises(ArgumentError) do
|
73
|
-
subject.call(nil, :nil?, :instance)
|
74
|
-
end
|
75
|
-
end
|
81
|
+
[TestClass, TestModule].each do |method_owner|
|
82
|
+
method_owner_class = method_owner.class.name.downcase.to_sym
|
76
83
|
|
77
|
-
|
78
|
-
|
79
|
-
|
84
|
+
context "validation" do
|
85
|
+
should "raise ArgumentError unless receiver given" do
|
86
|
+
assert_raises(ArgumentError) do
|
87
|
+
subject.call(nil, :nil?, :instance)
|
88
|
+
end
|
80
89
|
end
|
81
|
-
end
|
82
90
|
|
83
|
-
|
84
|
-
|
85
|
-
|
91
|
+
should "raise ArgumentError unless method name given" do
|
92
|
+
assert_raises(ArgumentError) do
|
93
|
+
subject.call(method_owner, nil, :instance)
|
94
|
+
end
|
86
95
|
end
|
87
|
-
end
|
88
96
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
assert_raises(TypeError) do
|
94
|
-
subject.call(TestClass, :instance_block_method, :instance)
|
97
|
+
should "raise ArgumentError unless method owner given" do
|
98
|
+
assert_raises(ArgumentError) do
|
99
|
+
subject.call(method_owner, :class_factorial, nil)
|
100
|
+
end
|
95
101
|
end
|
96
|
-
end
|
97
|
-
end
|
98
102
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
+
should "raise TypeError for block methods" do
|
104
|
+
assert_raises(TypeError) do
|
105
|
+
subject.call(method_owner, :singleton_block_method, :class)
|
106
|
+
end
|
107
|
+
assert_raises(TypeError) do
|
108
|
+
subject.call(method_owner, :instance_block_method, :instance)
|
109
|
+
end
|
103
110
|
end
|
104
111
|
end
|
105
112
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
113
|
+
context "#{method_owner_class} receiver" do
|
114
|
+
context "with module method" do
|
115
|
+
should "raise NameError if no #{method_owner_class} method with given name defined" do
|
116
|
+
assert_raises(NameError) do
|
117
|
+
subject.call(method_owner, :marmalade, method_owner_class)
|
118
|
+
end
|
119
|
+
end
|
112
120
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
121
|
+
should "re-compile the given method with tail call optimization" do
|
122
|
+
# Exceed maximum available stack depth by 100 for good measure
|
123
|
+
factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
|
124
|
+
assert_raises(SystemStackError) do
|
125
|
+
method_owner.module_factorial(factorial_seed)
|
126
|
+
end
|
118
127
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
128
|
+
subject.call(method_owner, :module_factorial, :module)
|
129
|
+
expected_result = iterative_factorial(factorial_seed)
|
130
|
+
assert_equal expected_result, method_owner.module_factorial(factorial_seed)
|
131
|
+
end
|
123
132
|
end
|
124
|
-
end
|
125
133
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
134
|
+
context "with class method" do
|
135
|
+
should "raise NameError if no class method with given name defined" do
|
136
|
+
assert_raises(NameError) do
|
137
|
+
subject.call(method_owner, :marmalade, :class)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
should "re-compile the given method with tail call optimization" do
|
142
|
+
# Exceed maximum available stack depth by 100 for good measure
|
143
|
+
factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
|
144
|
+
assert_raises(SystemStackError) do
|
145
|
+
method_owner.class_factorial(factorial_seed)
|
146
|
+
end
|
147
|
+
|
148
|
+
subject.call(method_owner, :class_factorial, method_owner_class)
|
149
|
+
expected_result = iterative_factorial(factorial_seed)
|
150
|
+
assert_equal expected_result, method_owner.class_factorial(factorial_seed)
|
151
|
+
end
|
131
152
|
end
|
132
153
|
|
133
|
-
|
134
|
-
|
135
|
-
|
154
|
+
context "with instance method" do
|
155
|
+
should "raise NameError if no instance method with given name defined" do
|
156
|
+
assert_raises(NameError) do
|
157
|
+
subject.call(method_owner, :marmalade, :instance)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
should "re-compile the given method with tail call optimization" do
|
162
|
+
# Exceed maximum available stack depth by 100 for good measure
|
163
|
+
factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
|
164
|
+
instance_class = instance_class_for_receiver(method_owner)
|
165
|
+
assert_raises(SystemStackError) do
|
166
|
+
instance_class.new.instance_factorial(factorial_seed)
|
167
|
+
end
|
168
|
+
|
169
|
+
subject.call(method_owner, :instance_factorial, :instance)
|
170
|
+
expected_result = iterative_factorial(factorial_seed)
|
171
|
+
assert_equal expected_result, instance_class.new.instance_factorial(factorial_seed)
|
172
|
+
end
|
173
|
+
end
|
136
174
|
end
|
137
175
|
end
|
138
176
|
end
|
177
|
+
|
178
|
+
def instance_class_for_receiver(receiver)
|
179
|
+
return receiver if receiver.is_a?(Class)
|
180
|
+
Class.new { include receiver }
|
181
|
+
end
|
139
182
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tco_method
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Danny Guinther
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-03-
|
11
|
+
date: 2015-03-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: method_source
|