memoizable 0.2.0 → 0.3.0
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.
- data/README.md +56 -1
- data/Rakefile +2 -0
- data/lib/memoizable.rb +6 -4
- data/lib/memoizable/instance_methods.rb +3 -1
- data/lib/memoizable/memory.rb +18 -22
- data/lib/memoizable/method_builder.rb +33 -12
- data/lib/memoizable/module_methods.rb +21 -6
- data/lib/memoizable/version.rb +3 -1
- data/memoizable.gemspec +5 -4
- data/spec/shared/call_super_shared_spec.rb +23 -0
- data/spec/shared/command_method_behavior.rb +7 -0
- data/spec/spec_helper.rb +8 -1
- data/spec/unit/memoizable/class_methods/included_spec.rb +18 -0
- data/spec/{fixtures → unit/memoizable/fixtures}/classes.rb +11 -2
- data/spec/unit/memoizable/instance_methods/freeze_spec.rb +27 -0
- data/spec/unit/memoizable/instance_methods/memoize_spec.rb +40 -0
- data/spec/unit/memoizable/method_builder/call_spec.rb +93 -0
- data/spec/unit/memoizable/method_builder/class_methods/new_spec.rb +34 -0
- data/spec/unit/memoizable/method_builder/original_method_spec.rb +31 -0
- data/spec/unit/memoizable/module_methods/included_spec.rb +23 -0
- data/spec/{memoize_spec.rb → unit/memoizable/module_methods/memoize_spec.rb} +7 -17
- data/spec/{memoized_predicate_spec.rb → unit/memoizable/module_methods/memoized_predicate_spec.rb} +2 -0
- data/spec/unit/memoizable/module_methods/unmemoized_instance_method_spec.rb +43 -0
- metadata +27 -10
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# Memoizable
|
2
2
|
|
3
3
|
Memoize method return values
|
4
4
|
|
@@ -18,6 +18,61 @@ Memoize method return values
|
|
18
18
|
|
19
19
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
20
20
|
|
21
|
+
## Rationale
|
22
|
+
|
23
|
+
Memoization is an optimization that saves the return value of a method so it
|
24
|
+
doesn't need to be re-computed every time that method is called. For example,
|
25
|
+
perhaps you've written a method like this:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class Planet
|
29
|
+
# This is the equation for the area of a sphere. If it's true for a
|
30
|
+
# particular instance of a planet, then that planet is spherical.
|
31
|
+
def spherical?
|
32
|
+
4 * Math::PI * radius ** 2 == area
|
33
|
+
end
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
This code will re-compute whether a particular planet is spherical every time
|
38
|
+
the method is called. If the method is called more than once, it may be more
|
39
|
+
efficient to save the computed value in an instance variable, like so:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class Planet
|
43
|
+
def spherical?
|
44
|
+
@spherical ||= 4 * Math::PI * radius ** 2 == area
|
45
|
+
end
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
One problem with this approach is that, if the return value is `false`, the
|
50
|
+
value will still be computed each time the method is called. It also becomes
|
51
|
+
unweildy for methods that grow to be longer than one line.
|
52
|
+
|
53
|
+
These problems can be solved by mixing-in the `Memoizable` module and memoizing
|
54
|
+
the method.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
require 'memoizable'
|
58
|
+
|
59
|
+
class Planet
|
60
|
+
include Memoizable
|
61
|
+
def spherical?
|
62
|
+
4 * Math::PI * radius ** 2 == area
|
63
|
+
end
|
64
|
+
memoize :spherical?
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
## Warning
|
69
|
+
|
70
|
+
The example above assumes that the radius and area of a planet will not change
|
71
|
+
over time. This seems like a reasonable assumption but such an assumption is
|
72
|
+
not safe in every domain. If it was possible for one of the attributes to
|
73
|
+
change between method calls, memoizing that value could produce the wrong
|
74
|
+
result. Please keep this in mind when considering which methods to memoize.
|
75
|
+
|
21
76
|
## Copyright
|
22
77
|
|
23
78
|
Copyright © 2013 Dan Kubb, Erik Michaels-Ober. See LICENSE for details.
|
data/Rakefile
CHANGED
data/lib/memoizable.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
require 'thread_safe'
|
2
4
|
|
3
5
|
require 'memoizable/instance_methods'
|
@@ -8,6 +10,7 @@ require 'memoizable/version'
|
|
8
10
|
|
9
11
|
# Allow methods to be memoized
|
10
12
|
module Memoizable
|
13
|
+
include InstanceMethods
|
11
14
|
|
12
15
|
# Default freezer
|
13
16
|
Freezer = lambda { |object| object.freeze }.freeze
|
@@ -21,10 +24,9 @@ module Memoizable
|
|
21
24
|
#
|
22
25
|
# @api private
|
23
26
|
def self.included(descendant)
|
24
|
-
|
25
|
-
|
26
|
-
include InstanceMethods
|
27
|
-
end
|
27
|
+
super
|
28
|
+
descendant.extend(ModuleMethods)
|
28
29
|
end
|
30
|
+
private_class_method :included
|
29
31
|
|
30
32
|
end # Memoizable
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Memoizable
|
2
4
|
|
3
5
|
# Methods mixed in to memoizable instances
|
@@ -40,7 +42,7 @@ module Memoizable
|
|
40
42
|
#
|
41
43
|
# @api private
|
42
44
|
def memoized_method_cache
|
43
|
-
@_memoized_method_cache ||= Memory.new
|
45
|
+
@_memoized_method_cache ||= Memory.new
|
44
46
|
end
|
45
47
|
|
46
48
|
end # InstanceMethods
|
data/lib/memoizable/memory.rb
CHANGED
@@ -1,11 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Memoizable
|
2
4
|
|
3
5
|
# Storage for memoized methods
|
4
6
|
class Memory
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
8
|
+
# Initialize the memory storage for memoized methods
|
9
|
+
#
|
10
|
+
# @return [undefined]
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
def initialize
|
14
|
+
@memory = ThreadSafe::Cache.new
|
15
|
+
freeze
|
9
16
|
end
|
10
17
|
|
11
18
|
# Get the value from memory
|
@@ -17,7 +24,7 @@ module Memoizable
|
|
17
24
|
# @api public
|
18
25
|
def [](name)
|
19
26
|
@memory.fetch(name) do
|
20
|
-
|
27
|
+
fail NameError, "No method #{name} is memoized"
|
21
28
|
end
|
22
29
|
end
|
23
30
|
|
@@ -30,10 +37,12 @@ module Memoizable
|
|
30
37
|
#
|
31
38
|
# @api public
|
32
39
|
def []=(name, value)
|
33
|
-
|
34
|
-
|
40
|
+
memoized = true
|
41
|
+
@memory.compute_if_absent(name) do
|
42
|
+
memoized = false
|
43
|
+
value
|
35
44
|
end
|
36
|
-
|
45
|
+
fail ArgumentError, "The method #{name} is already memoized" if memoized
|
37
46
|
end
|
38
47
|
|
39
48
|
# Fetch the value from memory, or store it if it does not exist
|
@@ -44,8 +53,8 @@ module Memoizable
|
|
44
53
|
# the value to memoize
|
45
54
|
#
|
46
55
|
# @api public
|
47
|
-
def fetch(name)
|
48
|
-
@memory.
|
56
|
+
def fetch(name, &block)
|
57
|
+
@memory.compute_if_absent(name, &block)
|
49
58
|
end
|
50
59
|
|
51
60
|
# Set the memory
|
@@ -71,18 +80,5 @@ module Memoizable
|
|
71
80
|
@memory.key?(name)
|
72
81
|
end
|
73
82
|
|
74
|
-
private
|
75
|
-
|
76
|
-
# Freeze the value
|
77
|
-
#
|
78
|
-
# @param [Object] value
|
79
|
-
#
|
80
|
-
# @return [Object]
|
81
|
-
#
|
82
|
-
# @api private
|
83
|
-
def freeze_value(value)
|
84
|
-
@freezer.call(value)
|
85
|
-
end
|
86
|
-
|
87
83
|
end # Memory
|
88
84
|
end # Memoizable
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Memoizable
|
2
4
|
|
3
5
|
# Build the memoized method
|
@@ -14,11 +16,26 @@ module Memoizable
|
|
14
16
|
#
|
15
17
|
# @api private
|
16
18
|
def initialize(descendant, method, arity)
|
17
|
-
super("Cannot memoize #{descendant}##{method},
|
19
|
+
super("Cannot memoize #{descendant}##{method}, its arity is #{arity}")
|
18
20
|
end
|
19
21
|
|
20
22
|
end # InvalidArityError
|
21
23
|
|
24
|
+
# Raised when a block is passed to a memoized method
|
25
|
+
class BlockNotAllowedError < ArgumentError
|
26
|
+
|
27
|
+
# Initialize a block not allowed exception
|
28
|
+
#
|
29
|
+
# @param [Module] descendant
|
30
|
+
# @param [Symbol] method
|
31
|
+
#
|
32
|
+
# @api private
|
33
|
+
def initialize(descendant, method)
|
34
|
+
super("Cannot pass a block to #{descendant}##{method}, it is memoized")
|
35
|
+
end
|
36
|
+
|
37
|
+
end # BlockNotAllowedError
|
38
|
+
|
22
39
|
# The original method before memoization
|
23
40
|
#
|
24
41
|
# @return [UnboundMethod]
|
@@ -30,16 +47,18 @@ module Memoizable
|
|
30
47
|
#
|
31
48
|
# @param [Module] descendant
|
32
49
|
# @param [Symbol] method_name
|
50
|
+
# @param [#call] freezer
|
33
51
|
#
|
34
52
|
# @return [undefined]
|
35
53
|
#
|
36
54
|
# @api private
|
37
|
-
def initialize(descendant, method_name)
|
55
|
+
def initialize(descendant, method_name, freezer)
|
38
56
|
@descendant = descendant
|
39
57
|
@method_name = method_name
|
58
|
+
@freezer = freezer
|
40
59
|
@original_visibility = visibility
|
41
60
|
@original_method = @descendant.instance_method(@method_name)
|
42
|
-
|
61
|
+
assert_arity(@original_method.arity)
|
43
62
|
end
|
44
63
|
|
45
64
|
# Build a new memoized method
|
@@ -61,15 +80,16 @@ module Memoizable
|
|
61
80
|
|
62
81
|
# Assert the method arity is zero
|
63
82
|
#
|
83
|
+
# @param [Integer] arity
|
84
|
+
#
|
64
85
|
# @return [undefined]
|
65
86
|
#
|
66
87
|
# @raise [InvalidArityError]
|
67
88
|
#
|
68
89
|
# @api private
|
69
|
-
def
|
70
|
-
arity = @original_method.arity
|
90
|
+
def assert_arity(arity)
|
71
91
|
if arity.nonzero?
|
72
|
-
|
92
|
+
fail InvalidArityError.new(@descendant, @method_name, arity)
|
73
93
|
end
|
74
94
|
end
|
75
95
|
|
@@ -88,9 +108,12 @@ module Memoizable
|
|
88
108
|
#
|
89
109
|
# @api private
|
90
110
|
def create_memoized_method
|
91
|
-
descendant_exec(@method_name, @original_method) do |name, method|
|
92
|
-
define_method(name) do
|
93
|
-
|
111
|
+
descendant_exec(@method_name, @original_method, @freezer) do |name, method, freezer|
|
112
|
+
define_method(name) do |&block|
|
113
|
+
fail BlockNotAllowedError.new(self.class, name) if block
|
114
|
+
memoized_method_cache.fetch(name) do
|
115
|
+
freezer.call(method.bind(self).call)
|
116
|
+
end
|
94
117
|
end
|
95
118
|
end
|
96
119
|
end
|
@@ -101,9 +124,7 @@ module Memoizable
|
|
101
124
|
#
|
102
125
|
# @api private
|
103
126
|
def set_method_visibility
|
104
|
-
|
105
|
-
send(visibility, name)
|
106
|
-
end
|
127
|
+
@descendant.send(@original_visibility, @method_name)
|
107
128
|
end
|
108
129
|
|
109
130
|
# Get the visibility of the original method
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Memoizable
|
2
4
|
|
3
5
|
# Methods mixed in to memoizable singleton classes
|
@@ -8,7 +10,6 @@ module Memoizable
|
|
8
10
|
# @return [#call]
|
9
11
|
#
|
10
12
|
# @api private
|
11
|
-
#
|
12
13
|
def freezer
|
13
14
|
Freezer
|
14
15
|
end
|
@@ -82,18 +83,33 @@ module Memoizable
|
|
82
83
|
|
83
84
|
private
|
84
85
|
|
86
|
+
# Hook called when module is included
|
87
|
+
#
|
88
|
+
# @param [Module] descendant
|
89
|
+
# the module including ModuleMethods
|
90
|
+
#
|
91
|
+
# @return [self]
|
92
|
+
#
|
93
|
+
# @api private
|
94
|
+
def included(descendant)
|
95
|
+
super
|
96
|
+
descendant.module_eval { include Memoizable }
|
97
|
+
end
|
98
|
+
|
85
99
|
# Memoize the named method
|
86
100
|
#
|
87
101
|
# @param [Symbol] method_name
|
88
102
|
# a method name to memoize
|
89
|
-
# @param [#call] freezer
|
90
|
-
# a freezer for memoized values
|
91
103
|
#
|
92
104
|
# @return [undefined]
|
93
105
|
#
|
94
106
|
# @api private
|
95
107
|
def memoize_method(method_name)
|
96
|
-
memoized_methods[method_name] = MethodBuilder.new(
|
108
|
+
memoized_methods[method_name] = MethodBuilder.new(
|
109
|
+
self,
|
110
|
+
method_name,
|
111
|
+
freezer
|
112
|
+
).call
|
97
113
|
end
|
98
114
|
|
99
115
|
# Return method builder registry
|
@@ -101,9 +117,8 @@ module Memoizable
|
|
101
117
|
# @return [Hash<Symbol, MethodBuilder>]
|
102
118
|
#
|
103
119
|
# @api private
|
104
|
-
#
|
105
120
|
def memoized_methods
|
106
|
-
@_memoized_methods ||= Memory.new
|
121
|
+
@_memoized_methods ||= Memory.new
|
107
122
|
end
|
108
123
|
|
109
124
|
end # ModuleMethods
|
data/lib/memoizable/version.rb
CHANGED
data/memoizable.gemspec
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
require File.expand_path('../lib/memoizable/version', __FILE__)
|
2
4
|
|
3
5
|
Gem::Specification.new do |gem|
|
@@ -12,10 +14,9 @@ Gem::Specification.new do |gem|
|
|
12
14
|
|
13
15
|
gem.require_paths = %w[lib]
|
14
16
|
gem.files = %w[CONTRIBUTING.md LICENSE.md README.md Rakefile memoizable.gemspec]
|
15
|
-
gem.files += Dir.glob('lib/**/*.rb')
|
16
|
-
gem.
|
17
|
-
gem.
|
18
|
-
gem.extra_rdoc_files = %w[LICENSE.md README.md CONTRIBUTING.md]
|
17
|
+
gem.files += Dir.glob('{lib,spec}/**/*.rb')
|
18
|
+
gem.test_files = Dir.glob('spec/{unit,integration}/**/*.rb')
|
19
|
+
gem.extra_rdoc_files = Dir.glob('**/*.md')
|
19
20
|
|
20
21
|
gem.add_runtime_dependency('thread_safe', '~> 0.1.3')
|
21
22
|
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
shared_examples 'it calls super' do |method|
|
4
|
+
around do |example|
|
5
|
+
# Restore original method after each example
|
6
|
+
original = "original_#{method}"
|
7
|
+
superclass.class_eval do
|
8
|
+
alias_method original, method
|
9
|
+
example.call
|
10
|
+
undef_method method
|
11
|
+
alias_method method, original
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it "delegates to the superclass ##{method} method" do
|
16
|
+
# This is the most succinct approach I could think of to test whether the
|
17
|
+
# superclass method is called. All of the built-in rspec helpers did not
|
18
|
+
# seem to work for this.
|
19
|
+
called = false
|
20
|
+
superclass.class_eval { define_method(method) { |_| called = true } }
|
21
|
+
expect { subject }.to change { called }.from(false).to(true)
|
22
|
+
end
|
23
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
require 'simplecov'
|
2
4
|
require 'coveralls'
|
3
5
|
|
@@ -12,12 +14,17 @@ SimpleCov.start do
|
|
12
14
|
add_filter 'spec'
|
13
15
|
add_filter 'vendor'
|
14
16
|
|
15
|
-
minimum_coverage
|
17
|
+
minimum_coverage 100
|
16
18
|
end
|
17
19
|
|
18
20
|
require 'memoizable'
|
19
21
|
require 'rspec'
|
20
22
|
|
23
|
+
# Require spec support files and shared behavior
|
24
|
+
Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].each do |file|
|
25
|
+
require file.chomp('.rb')
|
26
|
+
end
|
27
|
+
|
21
28
|
RSpec.configure do |config|
|
22
29
|
config.expect_with :rspec do |expect_with|
|
23
30
|
expect_with.syntax = :expect
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Memoizable, '.included' do
|
6
|
+
subject { object.class_eval { include Memoizable } }
|
7
|
+
|
8
|
+
let(:object) { Class.new }
|
9
|
+
let(:superclass) { Module }
|
10
|
+
|
11
|
+
it_behaves_like 'it calls super', :included
|
12
|
+
|
13
|
+
it 'extends the descendant with module methods' do
|
14
|
+
subject
|
15
|
+
extended_modules = class << object; included_modules end
|
16
|
+
expect(extended_modules).to include(Memoizable::ModuleMethods)
|
17
|
+
end
|
18
|
+
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Fixture
|
2
4
|
class Object
|
3
5
|
include Memoizable
|
4
6
|
|
@@ -12,6 +14,13 @@ module MemoizableSpecs
|
|
12
14
|
'test'
|
13
15
|
end
|
14
16
|
|
17
|
+
def zero_arity
|
18
|
+
caller
|
19
|
+
end
|
20
|
+
|
21
|
+
def one_arity(arg)
|
22
|
+
end
|
23
|
+
|
15
24
|
def public_method
|
16
25
|
caller
|
17
26
|
end
|
@@ -29,4 +38,4 @@ module MemoizableSpecs
|
|
29
38
|
end
|
30
39
|
|
31
40
|
end # class Object
|
32
|
-
end # module
|
41
|
+
end # module Fixture
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require File.expand_path('../../fixtures/classes', __FILE__)
|
5
|
+
|
6
|
+
describe Memoizable::InstanceMethods, '#freeze' do
|
7
|
+
subject { object.freeze }
|
8
|
+
|
9
|
+
let(:described_class) { Class.new(Fixture::Object) }
|
10
|
+
|
11
|
+
before do
|
12
|
+
described_class.memoize(:test)
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:object) { described_class.allocate }
|
16
|
+
|
17
|
+
it_should_behave_like 'a command method'
|
18
|
+
|
19
|
+
it 'freezes the object' do
|
20
|
+
expect { subject }.to change(object, :frozen?).from(false).to(true)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'allows methods not yet called to be memoized' do
|
24
|
+
subject
|
25
|
+
expect(object.test).to be(object.test)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require File.expand_path('../../fixtures/classes', __FILE__)
|
5
|
+
|
6
|
+
describe Memoizable::InstanceMethods, '#memoize' do
|
7
|
+
subject { object.memoize(method => value) }
|
8
|
+
|
9
|
+
let(:described_class) { Class.new(Fixture::Object) }
|
10
|
+
let(:object) { described_class.new }
|
11
|
+
let(:method) { :test }
|
12
|
+
|
13
|
+
before do
|
14
|
+
described_class.memoize(method)
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'when the method is not memoized' do
|
18
|
+
let(:value) { String.new }
|
19
|
+
|
20
|
+
it 'sets the memoized value for the method to the value' do
|
21
|
+
subject
|
22
|
+
expect(object.send(method)).to be(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
it_should_behave_like 'a command method'
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when the method is already memoized' do
|
29
|
+
let(:value) { double }
|
30
|
+
let(:original) { nil }
|
31
|
+
|
32
|
+
before do
|
33
|
+
object.memoize(method => original)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'raises an exception' do
|
37
|
+
expect { subject }.to raise_error(ArgumentError)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require File.expand_path('../../fixtures/classes', __FILE__)
|
5
|
+
|
6
|
+
describe Memoizable::MethodBuilder, '#call' do
|
7
|
+
subject { object.call }
|
8
|
+
|
9
|
+
let(:object) { described_class.new(descendant, method_name, freezer) }
|
10
|
+
let(:freezer) { lambda { |object| object.freeze } }
|
11
|
+
let(:instance) { descendant.new }
|
12
|
+
|
13
|
+
let(:descendant) do
|
14
|
+
Class.new do
|
15
|
+
include Memoizable
|
16
|
+
|
17
|
+
def public_method
|
18
|
+
__method__.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def protected_method
|
22
|
+
__method__.to_s
|
23
|
+
end
|
24
|
+
protected :protected_method
|
25
|
+
|
26
|
+
def private_method
|
27
|
+
__method__.to_s
|
28
|
+
end
|
29
|
+
private :private_method
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
shared_examples_for 'Memoizable::MethodBuilder#call' do
|
34
|
+
it_should_behave_like 'a command method'
|
35
|
+
|
36
|
+
it 'creates a method that is memoized' do
|
37
|
+
subject
|
38
|
+
expect(instance.send(method_name)).to be(instance.send(method_name))
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'creates a method that returns the expected value' do
|
42
|
+
subject
|
43
|
+
expect(instance.send(method_name)).to eql(method_name.to_s)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'creates a method that returns a frozen value' do
|
47
|
+
subject
|
48
|
+
expect(descendant.new.send(method_name)).to be_frozen
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'creates a method that does not accept a block' do
|
52
|
+
subject
|
53
|
+
expect { descendant.new.send(method_name) {} }.to raise_error(
|
54
|
+
described_class::BlockNotAllowedError,
|
55
|
+
"Cannot pass a block to #{descendant}##{method_name}, it is memoized"
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'public method' do
|
61
|
+
let(:method_name) { :public_method }
|
62
|
+
|
63
|
+
it_should_behave_like 'Memoizable::MethodBuilder#call'
|
64
|
+
|
65
|
+
it 'creates a public memoized method' do
|
66
|
+
subject
|
67
|
+
expect(descendant).to be_public_method_defined(method_name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'protected method' do
|
72
|
+
let(:method_name) { :protected_method }
|
73
|
+
|
74
|
+
it_should_behave_like 'Memoizable::MethodBuilder#call'
|
75
|
+
|
76
|
+
it 'creates a protected memoized method' do
|
77
|
+
subject
|
78
|
+
expect(descendant).to be_protected_method_defined(method_name)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'private method' do
|
84
|
+
let(:method_name) { :private_method }
|
85
|
+
|
86
|
+
it_should_behave_like 'Memoizable::MethodBuilder#call'
|
87
|
+
|
88
|
+
it 'creates a private memoized method' do
|
89
|
+
subject
|
90
|
+
expect(descendant).to be_private_method_defined(method_name)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require File.expand_path('../../../fixtures/classes', __FILE__)
|
5
|
+
|
6
|
+
describe Memoizable::MethodBuilder, '.new' do
|
7
|
+
subject { described_class.new(descendant, method_name, freezer) }
|
8
|
+
|
9
|
+
let(:descendant) { Fixture::Object }
|
10
|
+
let(:freezer) { lambda { |object| object.freeze } }
|
11
|
+
|
12
|
+
context 'with a zero arity method' do
|
13
|
+
let(:method_name) { :zero_arity }
|
14
|
+
|
15
|
+
it { should be_instance_of(described_class) }
|
16
|
+
|
17
|
+
it 'sets the original method' do
|
18
|
+
# original method is not memoized
|
19
|
+
method = subject.original_method.bind(descendant.new)
|
20
|
+
expect(method.call).to_not be(method.call)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'with a one arity method' do
|
25
|
+
let(:method_name) { :one_arity }
|
26
|
+
|
27
|
+
it 'raises an exception' do
|
28
|
+
expect { subject }.to raise_error(
|
29
|
+
described_class::InvalidArityError,
|
30
|
+
'Cannot memoize Fixture::Object#one_arity, its arity is 1'
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Memoizable::MethodBuilder, '#original_method' do
|
6
|
+
subject { object.original_method }
|
7
|
+
|
8
|
+
let(:object) { described_class.new(descendant, method_name, freezer) }
|
9
|
+
let(:method_name) { :foo }
|
10
|
+
let(:freezer) { lambda { |object| object.freeze } }
|
11
|
+
|
12
|
+
let(:descendant) do
|
13
|
+
Class.new do
|
14
|
+
def initialize
|
15
|
+
@foo = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def foo
|
19
|
+
@foo += 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it { should be_instance_of(UnboundMethod) }
|
25
|
+
|
26
|
+
it 'returns the original method' do
|
27
|
+
# original method is not memoized
|
28
|
+
method = subject.bind(descendant.new)
|
29
|
+
expect(method.call).to_not be(method.call)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Memoizable::ModuleMethods, '#included' do
|
6
|
+
subject { descendant.instance_exec(object) { |mod| include mod } }
|
7
|
+
|
8
|
+
let(:object) { Module.new.extend(described_class) }
|
9
|
+
let(:descendant) { Class.new }
|
10
|
+
let(:superclass) { Module }
|
11
|
+
|
12
|
+
before do
|
13
|
+
# Prevent Module.included from being called through inheritance
|
14
|
+
Memoizable.stub(:included)
|
15
|
+
end
|
16
|
+
|
17
|
+
it_behaves_like 'it calls super', :included
|
18
|
+
|
19
|
+
it 'includes Memoizable into the descendant' do
|
20
|
+
subject
|
21
|
+
expect(descendant.included_modules).to include(Memoizable)
|
22
|
+
end
|
23
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
|
-
require File.expand_path('
|
4
|
+
require File.expand_path('../../fixtures/classes', __FILE__)
|
3
5
|
|
4
6
|
shared_examples_for 'memoizes method' do
|
5
7
|
it 'memoizes the instance method' do
|
@@ -8,13 +10,7 @@ shared_examples_for 'memoizes method' do
|
|
8
10
|
expect(instance.send(method)).to be(instance.send(method))
|
9
11
|
end
|
10
12
|
|
11
|
-
it 'creates a method
|
12
|
-
subject
|
13
|
-
instance = object.new
|
14
|
-
expect(instance.send(method)).to be(instance.send(method))
|
15
|
-
end
|
16
|
-
|
17
|
-
it 'creates a method with an arity of 0' do
|
13
|
+
it 'creates a zero arity method', :unless => RUBY_VERSION == '1.8.7' do
|
18
14
|
subject
|
19
15
|
expect(object.new.method(method).arity).to be_zero
|
20
16
|
end
|
@@ -32,17 +28,11 @@ shared_examples_for 'memoizes method' do
|
|
32
28
|
end
|
33
29
|
end
|
34
30
|
|
35
|
-
shared_examples_for 'a command method' do
|
36
|
-
it 'returns self' do
|
37
|
-
should equal(object)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
31
|
describe Memoizable::ModuleMethods, '#memoize' do
|
42
32
|
subject { object.memoize(method) }
|
43
33
|
|
44
34
|
let(:object) do
|
45
|
-
stub_const 'TestClass', Class.new(
|
35
|
+
stub_const 'TestClass', Class.new(Fixture::Object) {
|
46
36
|
def some_state
|
47
37
|
Object.new
|
48
38
|
end
|
@@ -55,7 +45,7 @@ describe Memoizable::ModuleMethods, '#memoize' do
|
|
55
45
|
it 'should raise error' do
|
56
46
|
expect { subject }.to raise_error(
|
57
47
|
Memoizable::MethodBuilder::InvalidArityError,
|
58
|
-
|
48
|
+
'Cannot memoize TestClass#required_arguments, its arity is 1'
|
59
49
|
)
|
60
50
|
end
|
61
51
|
end
|
@@ -66,7 +56,7 @@ describe Memoizable::ModuleMethods, '#memoize' do
|
|
66
56
|
it 'should raise error' do
|
67
57
|
expect { subject }.to raise_error(
|
68
58
|
Memoizable::MethodBuilder::InvalidArityError,
|
69
|
-
|
59
|
+
'Cannot memoize TestClass#optional_arguments, its arity is -1'
|
70
60
|
)
|
71
61
|
end
|
72
62
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Memoizable::ModuleMethods, '#unmemoized_instance_method' do
|
6
|
+
subject { object.unmemoized_instance_method(name) }
|
7
|
+
|
8
|
+
let(:object) do
|
9
|
+
Class.new do
|
10
|
+
include Memoizable
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@foo = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def foo
|
17
|
+
@foo += 1
|
18
|
+
end
|
19
|
+
|
20
|
+
memoize :foo
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'when the method was memoized' do
|
25
|
+
let(:name) { :foo }
|
26
|
+
|
27
|
+
it { should be_instance_of(UnboundMethod) }
|
28
|
+
|
29
|
+
it 'returns the original method' do
|
30
|
+
# original method is not memoized
|
31
|
+
method = subject.bind(object.new)
|
32
|
+
expect(method.call).to_not be(method.call)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'when the method was not memoized' do
|
37
|
+
let(:name) { :bar }
|
38
|
+
|
39
|
+
it 'raises an exception' do
|
40
|
+
expect { subject }.to raise_error(NameError, 'No method bar is memoized')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: memoizable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-12-14 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thread_safe
|
@@ -54,9 +54,9 @@ email: dan.kubb@gmail.com
|
|
54
54
|
executables: []
|
55
55
|
extensions: []
|
56
56
|
extra_rdoc_files:
|
57
|
+
- CONTRIBUTING.md
|
57
58
|
- LICENSE.md
|
58
59
|
- README.md
|
59
|
-
- CONTRIBUTING.md
|
60
60
|
files:
|
61
61
|
- CONTRIBUTING.md
|
62
62
|
- LICENSE.md
|
@@ -69,10 +69,20 @@ files:
|
|
69
69
|
- lib/memoizable/module_methods.rb
|
70
70
|
- lib/memoizable/version.rb
|
71
71
|
- lib/memoizable.rb
|
72
|
-
- spec/
|
73
|
-
- spec/
|
74
|
-
- spec/memoized_predicate_spec.rb
|
72
|
+
- spec/shared/call_super_shared_spec.rb
|
73
|
+
- spec/shared/command_method_behavior.rb
|
75
74
|
- spec/spec_helper.rb
|
75
|
+
- spec/unit/memoizable/class_methods/included_spec.rb
|
76
|
+
- spec/unit/memoizable/fixtures/classes.rb
|
77
|
+
- spec/unit/memoizable/instance_methods/freeze_spec.rb
|
78
|
+
- spec/unit/memoizable/instance_methods/memoize_spec.rb
|
79
|
+
- spec/unit/memoizable/method_builder/call_spec.rb
|
80
|
+
- spec/unit/memoizable/method_builder/class_methods/new_spec.rb
|
81
|
+
- spec/unit/memoizable/method_builder/original_method_spec.rb
|
82
|
+
- spec/unit/memoizable/module_methods/included_spec.rb
|
83
|
+
- spec/unit/memoizable/module_methods/memoize_spec.rb
|
84
|
+
- spec/unit/memoizable/module_methods/memoized_predicate_spec.rb
|
85
|
+
- spec/unit/memoizable/module_methods/unmemoized_instance_method_spec.rb
|
76
86
|
homepage: https://github.com/dkubb/memoizable
|
77
87
|
licenses:
|
78
88
|
- MIT
|
@@ -99,8 +109,15 @@ signing_key:
|
|
99
109
|
specification_version: 3
|
100
110
|
summary: Memoize method return values
|
101
111
|
test_files:
|
102
|
-
- spec/
|
103
|
-
- spec/
|
104
|
-
- spec/
|
105
|
-
- spec/
|
112
|
+
- spec/unit/memoizable/class_methods/included_spec.rb
|
113
|
+
- spec/unit/memoizable/fixtures/classes.rb
|
114
|
+
- spec/unit/memoizable/instance_methods/freeze_spec.rb
|
115
|
+
- spec/unit/memoizable/instance_methods/memoize_spec.rb
|
116
|
+
- spec/unit/memoizable/method_builder/call_spec.rb
|
117
|
+
- spec/unit/memoizable/method_builder/class_methods/new_spec.rb
|
118
|
+
- spec/unit/memoizable/method_builder/original_method_spec.rb
|
119
|
+
- spec/unit/memoizable/module_methods/included_spec.rb
|
120
|
+
- spec/unit/memoizable/module_methods/memoize_spec.rb
|
121
|
+
- spec/unit/memoizable/module_methods/memoized_predicate_spec.rb
|
122
|
+
- spec/unit/memoizable/module_methods/unmemoized_instance_method_spec.rb
|
106
123
|
has_rdoc:
|