tco_method 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|