memoist 0.1.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 +54 -0
- data/lib/memoist.rb +109 -0
- data/lib/memoist/core_ext/singleton_class.rb +13 -0
- data/test/memoist_test.rb +279 -0
- data/test/test_helper.rb +4 -0
- metadata +73 -0
data/README.md
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
Memoist
|
2
|
+
=============
|
3
|
+
|
4
|
+
Memoist is an extraction of ActiveSupport::Memoizable.
|
5
|
+
|
6
|
+
Since June 2011 ActiveSupport::Memoizable has been deprecated.
|
7
|
+
But I love it,
|
8
|
+
and so I plan to keep it alive.
|
9
|
+
|
10
|
+
Usage
|
11
|
+
-----
|
12
|
+
|
13
|
+
Just extend with the Memoist module
|
14
|
+
|
15
|
+
require 'memoist'
|
16
|
+
class Person
|
17
|
+
extend Memoist
|
18
|
+
|
19
|
+
def social_security
|
20
|
+
decrypt_social_security
|
21
|
+
end
|
22
|
+
memoize :social_security
|
23
|
+
end
|
24
|
+
|
25
|
+
And person.social_security will only be calculated once.
|
26
|
+
|
27
|
+
You can even do it with a method that takes arguments.
|
28
|
+
|
29
|
+
|
30
|
+
class Person
|
31
|
+
def taxes_due(income)
|
32
|
+
income * 0.40
|
33
|
+
end
|
34
|
+
memoize :taxes_due
|
35
|
+
end
|
36
|
+
|
37
|
+
This will only be calculated once per value of income.
|
38
|
+
|
39
|
+
Authors
|
40
|
+
===========
|
41
|
+
|
42
|
+
Everyone who contributed to it in the rails repository.
|
43
|
+
|
44
|
+
* Joshua Peek
|
45
|
+
* Tarmo Tänav
|
46
|
+
* Jeremy Kemper
|
47
|
+
* Eugene Pimenov
|
48
|
+
* Xavier Noria
|
49
|
+
* Niels Ganser
|
50
|
+
* Carl Lerche & Yehuda Katz
|
51
|
+
* jeem
|
52
|
+
* Jay Pignata
|
53
|
+
* Damien Mathieu
|
54
|
+
* José Valim
|
data/lib/memoist.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# require 'active_support/core_ext/kernel/singleton_class'
|
2
|
+
require 'memoist/core_ext/singleton_class'
|
3
|
+
|
4
|
+
module Memoist
|
5
|
+
|
6
|
+
def self.memoized_ivar_for(symbol)
|
7
|
+
"@_memoized_#{symbol.to_s.sub(/\?\Z/, '_query').sub(/!\Z/, '_bang')}".to_sym
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
def self.included(base)
|
12
|
+
base.class_eval do
|
13
|
+
unless base.method_defined?(:freeze_without_memoizable)
|
14
|
+
alias_method :freeze_without_memoizable, :freeze
|
15
|
+
alias_method :freeze, :freeze_with_memoizable
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def freeze_with_memoizable
|
21
|
+
memoize_all unless frozen?
|
22
|
+
freeze_without_memoizable
|
23
|
+
end
|
24
|
+
|
25
|
+
def memoize_all
|
26
|
+
prime_cache ".*"
|
27
|
+
end
|
28
|
+
|
29
|
+
def unmemoize_all
|
30
|
+
flush_cache ".*"
|
31
|
+
end
|
32
|
+
|
33
|
+
def prime_cache(*syms)
|
34
|
+
syms.each do |sym|
|
35
|
+
methods.each do |m|
|
36
|
+
if m.to_s =~ /^_unmemoized_(#{sym})/
|
37
|
+
if method(m).arity == 0
|
38
|
+
__send__($1)
|
39
|
+
else
|
40
|
+
ivar = Memoist.memoized_ivar_for($1)
|
41
|
+
instance_variable_set(ivar, {})
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def flush_cache(*syms)
|
49
|
+
syms.each do |sym|
|
50
|
+
(methods + private_methods + protected_methods).each do |m|
|
51
|
+
if m.to_s =~ /^_unmemoized_(#{sym.to_s.gsub(/\?\Z/, '\?')})/
|
52
|
+
ivar = Memoist.memoized_ivar_for($1)
|
53
|
+
instance_variable_get(ivar).clear if instance_variable_defined?(ivar)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def memoize(*symbols)
|
61
|
+
symbols.each do |symbol|
|
62
|
+
original_method = :"_unmemoized_#{symbol}"
|
63
|
+
memoized_ivar = Memoist.memoized_ivar_for(symbol)
|
64
|
+
|
65
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
66
|
+
include InstanceMethods # include InstanceMethods
|
67
|
+
#
|
68
|
+
if method_defined?(:#{original_method}) # if method_defined?(:_unmemoized_mime_type)
|
69
|
+
raise "Already memoized #{symbol}" # raise "Already memoized mime_type"
|
70
|
+
end # end
|
71
|
+
alias #{original_method} #{symbol} # alias _unmemoized_mime_type mime_type
|
72
|
+
#
|
73
|
+
if instance_method(:#{symbol}).arity == 0 # if instance_method(:mime_type).arity == 0
|
74
|
+
def #{symbol}(reload = false) # def mime_type(reload = false)
|
75
|
+
if reload || !defined?(#{memoized_ivar}) || #{memoized_ivar}.empty? # if reload || !defined?(@_memoized_mime_type) || @_memoized_mime_type.empty?
|
76
|
+
#{memoized_ivar} = [#{original_method}] # @_memoized_mime_type = [_unmemoized_mime_type]
|
77
|
+
end # end
|
78
|
+
#{memoized_ivar}[0] # @_memoized_mime_type[0]
|
79
|
+
end # end
|
80
|
+
else # else
|
81
|
+
def #{symbol}(*args) # def mime_type(*args)
|
82
|
+
#{memoized_ivar} ||= {} unless frozen? # @_memoized_mime_type ||= {} unless frozen?
|
83
|
+
args_length = method(:#{original_method}).arity # args_length = method(:_unmemoized_mime_type).arity
|
84
|
+
if args.length == args_length + 1 && # if args.length == args_length + 1 &&
|
85
|
+
(args.last == true || args.last == :reload) # (args.last == true || args.last == :reload)
|
86
|
+
reload = args.pop # reload = args.pop
|
87
|
+
end # end
|
88
|
+
#
|
89
|
+
if defined?(#{memoized_ivar}) && #{memoized_ivar} # if defined?(@_memoized_mime_type) && @_memoized_mime_type
|
90
|
+
if !reload && #{memoized_ivar}.has_key?(args) # if !reload && @_memoized_mime_type.has_key?(args)
|
91
|
+
#{memoized_ivar}[args] # @_memoized_mime_type[args]
|
92
|
+
elsif #{memoized_ivar} # elsif @_memoized_mime_type
|
93
|
+
#{memoized_ivar}[args] = #{original_method}(*args) # @_memoized_mime_type[args] = _unmemoized_mime_type(*args)
|
94
|
+
end # end
|
95
|
+
else # else
|
96
|
+
#{original_method}(*args) # _unmemoized_mime_type(*args)
|
97
|
+
end # end
|
98
|
+
end # end
|
99
|
+
end # end
|
100
|
+
#
|
101
|
+
if private_method_defined?(#{original_method.inspect}) # if private_method_defined?(:_unmemoized_mime_type)
|
102
|
+
private #{symbol.inspect} # private :mime_type
|
103
|
+
elsif protected_method_defined?(#{original_method.inspect}) # elsif protected_method_defined?(:_unmemoized_mime_type)
|
104
|
+
protected #{symbol.inspect} # protected :mime_type
|
105
|
+
end # end
|
106
|
+
EOS
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Kernel
|
2
|
+
# Returns the object's singleton class.
|
3
|
+
def singleton_class
|
4
|
+
class << self
|
5
|
+
self
|
6
|
+
end
|
7
|
+
end unless respond_to?(:singleton_class) # exists in 1.9.2
|
8
|
+
|
9
|
+
# class_eval on an object acts like singleton_class.class_eval.
|
10
|
+
def class_eval(*args, &block)
|
11
|
+
singleton_class.class_eval(*args, &block)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,279 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'memoist'
|
3
|
+
|
4
|
+
class MemoistTest < Test::Unit::TestCase
|
5
|
+
class Person
|
6
|
+
extend Memoist
|
7
|
+
|
8
|
+
attr_reader :name_calls, :age_calls, :is_developer_calls, :name_query_calls
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@name_calls = 0
|
12
|
+
@age_calls = 0
|
13
|
+
@is_developer_calls = 0
|
14
|
+
@name_query_calls = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
@name_calls += 1
|
19
|
+
"Josh"
|
20
|
+
end
|
21
|
+
|
22
|
+
def name?
|
23
|
+
@name_query_calls += 1
|
24
|
+
true
|
25
|
+
end
|
26
|
+
memoize :name?
|
27
|
+
|
28
|
+
def update(name)
|
29
|
+
"Joshua"
|
30
|
+
end
|
31
|
+
memoize :update
|
32
|
+
|
33
|
+
def age
|
34
|
+
@age_calls += 1
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
memoize :name, :age
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def memoize_protected_test
|
43
|
+
'protected'
|
44
|
+
end
|
45
|
+
memoize :memoize_protected_test
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def is_developer?
|
50
|
+
@is_developer_calls += 1
|
51
|
+
"Yes"
|
52
|
+
end
|
53
|
+
memoize :is_developer?
|
54
|
+
end
|
55
|
+
|
56
|
+
class Company
|
57
|
+
attr_reader :name_calls
|
58
|
+
def initialize
|
59
|
+
@name_calls = 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def name
|
63
|
+
@name_calls += 1
|
64
|
+
"37signals"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
module Rates
|
69
|
+
extend Memoist
|
70
|
+
|
71
|
+
attr_reader :sales_tax_calls
|
72
|
+
def sales_tax(price)
|
73
|
+
@sales_tax_calls ||= 0
|
74
|
+
@sales_tax_calls += 1
|
75
|
+
price * 0.1025
|
76
|
+
end
|
77
|
+
memoize :sales_tax
|
78
|
+
end
|
79
|
+
|
80
|
+
class Calculator
|
81
|
+
extend Memoist
|
82
|
+
include Rates
|
83
|
+
|
84
|
+
attr_reader :fib_calls
|
85
|
+
def initialize
|
86
|
+
@fib_calls = 0
|
87
|
+
end
|
88
|
+
|
89
|
+
def fib(n)
|
90
|
+
@fib_calls += 1
|
91
|
+
|
92
|
+
if n == 0 || n == 1
|
93
|
+
n
|
94
|
+
else
|
95
|
+
fib(n - 1) + fib(n - 2)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
memoize :fib
|
99
|
+
|
100
|
+
def add_or_subtract(i, j, add)
|
101
|
+
if add
|
102
|
+
i + j
|
103
|
+
else
|
104
|
+
i - j
|
105
|
+
end
|
106
|
+
end
|
107
|
+
memoize :add_or_subtract
|
108
|
+
|
109
|
+
def counter
|
110
|
+
@count ||= 0
|
111
|
+
@count += 1
|
112
|
+
end
|
113
|
+
memoize :counter
|
114
|
+
end
|
115
|
+
|
116
|
+
def setup
|
117
|
+
@person = Person.new
|
118
|
+
@calculator = Calculator.new
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_memoization
|
122
|
+
assert_equal "Josh", @person.name
|
123
|
+
assert_equal 1, @person.name_calls
|
124
|
+
|
125
|
+
3.times { assert_equal "Josh", @person.name }
|
126
|
+
assert_equal 1, @person.name_calls
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_memoization_with_punctuation
|
130
|
+
assert_equal true, @person.name?
|
131
|
+
|
132
|
+
assert_nothing_raised(NameError) do
|
133
|
+
@person.memoize_all
|
134
|
+
@person.unmemoize_all
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_memoization_flush_with_punctuation
|
139
|
+
assert_equal true, @person.name?
|
140
|
+
@person.flush_cache(:name?)
|
141
|
+
3.times { assert_equal true, @person.name? }
|
142
|
+
assert_equal 2, @person.name_query_calls
|
143
|
+
end
|
144
|
+
|
145
|
+
def test_memoization_with_nil_value
|
146
|
+
assert_equal nil, @person.age
|
147
|
+
assert_equal 1, @person.age_calls
|
148
|
+
|
149
|
+
3.times { assert_equal nil, @person.age }
|
150
|
+
assert_equal 1, @person.age_calls
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_reloadable
|
154
|
+
assert_equal 1, @calculator.counter
|
155
|
+
assert_equal 2, @calculator.counter(:reload)
|
156
|
+
assert_equal 2, @calculator.counter
|
157
|
+
assert_equal 3, @calculator.counter(true)
|
158
|
+
assert_equal 3, @calculator.counter
|
159
|
+
end
|
160
|
+
|
161
|
+
def test_flush_cache
|
162
|
+
assert_equal 1, @calculator.counter
|
163
|
+
|
164
|
+
assert @calculator.instance_variable_get(:@_memoized_counter).any?
|
165
|
+
@calculator.flush_cache(:counter)
|
166
|
+
assert @calculator.instance_variable_get(:@_memoized_counter).empty?
|
167
|
+
|
168
|
+
assert_equal 2, @calculator.counter
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_unmemoize_all
|
172
|
+
assert_equal 1, @calculator.counter
|
173
|
+
|
174
|
+
assert @calculator.instance_variable_get(:@_memoized_counter).any?
|
175
|
+
@calculator.unmemoize_all
|
176
|
+
assert @calculator.instance_variable_get(:@_memoized_counter).empty?
|
177
|
+
|
178
|
+
assert_equal 2, @calculator.counter
|
179
|
+
end
|
180
|
+
|
181
|
+
def test_memoize_all
|
182
|
+
@calculator.memoize_all
|
183
|
+
assert @calculator.instance_variable_defined?(:@_memoized_counter)
|
184
|
+
end
|
185
|
+
|
186
|
+
def test_memoization_cache_is_different_for_each_instance
|
187
|
+
assert_equal 1, @calculator.counter
|
188
|
+
assert_equal 2, @calculator.counter(:reload)
|
189
|
+
assert_equal 1, Calculator.new.counter
|
190
|
+
end
|
191
|
+
|
192
|
+
def test_memoized_is_not_affected_by_freeze
|
193
|
+
@person.freeze
|
194
|
+
assert_equal "Josh", @person.name
|
195
|
+
assert_equal "Joshua", @person.update("Joshua")
|
196
|
+
end
|
197
|
+
|
198
|
+
def test_memoization_with_args
|
199
|
+
assert_equal 55, @calculator.fib(10)
|
200
|
+
assert_equal 11, @calculator.fib_calls
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_reloadable_with_args
|
204
|
+
assert_equal 55, @calculator.fib(10)
|
205
|
+
assert_equal 11, @calculator.fib_calls
|
206
|
+
assert_equal 55, @calculator.fib(10, :reload)
|
207
|
+
assert_equal 12, @calculator.fib_calls
|
208
|
+
assert_equal 55, @calculator.fib(10, true)
|
209
|
+
assert_equal 13, @calculator.fib_calls
|
210
|
+
end
|
211
|
+
|
212
|
+
def test_memoization_with_boolean_arg
|
213
|
+
assert_equal 4, @calculator.add_or_subtract(2, 2, true)
|
214
|
+
assert_equal 2, @calculator.add_or_subtract(4, 2, false)
|
215
|
+
end
|
216
|
+
|
217
|
+
def test_object_memoization
|
218
|
+
[Company.new, Company.new, Company.new].each do |company|
|
219
|
+
company.extend Memoist
|
220
|
+
company.memoize :name
|
221
|
+
|
222
|
+
assert_equal "37signals", company.name
|
223
|
+
assert_equal 1, company.name_calls
|
224
|
+
assert_equal "37signals", company.name
|
225
|
+
assert_equal 1, company.name_calls
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def test_memoized_module_methods
|
230
|
+
assert_equal 1.025, @calculator.sales_tax(10)
|
231
|
+
assert_equal 1, @calculator.sales_tax_calls
|
232
|
+
assert_equal 1.025, @calculator.sales_tax(10)
|
233
|
+
assert_equal 1, @calculator.sales_tax_calls
|
234
|
+
assert_equal 2.5625, @calculator.sales_tax(25)
|
235
|
+
assert_equal 2, @calculator.sales_tax_calls
|
236
|
+
end
|
237
|
+
|
238
|
+
def test_object_memoized_module_methods
|
239
|
+
company = Company.new
|
240
|
+
company.extend(Rates)
|
241
|
+
|
242
|
+
assert_equal 1.025, company.sales_tax(10)
|
243
|
+
assert_equal 1, company.sales_tax_calls
|
244
|
+
assert_equal 1.025, company.sales_tax(10)
|
245
|
+
assert_equal 1, company.sales_tax_calls
|
246
|
+
assert_equal 2.5625, company.sales_tax(25)
|
247
|
+
assert_equal 2, company.sales_tax_calls
|
248
|
+
end
|
249
|
+
|
250
|
+
def test_double_memoization
|
251
|
+
assert_raise(RuntimeError) { Person.memoize :name }
|
252
|
+
person = Person.new
|
253
|
+
person.extend Memoist
|
254
|
+
assert_raise(RuntimeError) { person.memoize :name }
|
255
|
+
|
256
|
+
company = Company.new
|
257
|
+
company.extend Memoist
|
258
|
+
company.memoize :name
|
259
|
+
assert_raise(RuntimeError) { company.memoize :name }
|
260
|
+
end
|
261
|
+
|
262
|
+
def test_protected_method_memoization
|
263
|
+
person = Person.new
|
264
|
+
|
265
|
+
assert_raise(NoMethodError) { person.memoize_protected_test }
|
266
|
+
assert_equal "protected", person.send(:memoize_protected_test)
|
267
|
+
end
|
268
|
+
|
269
|
+
def test_private_method_memoization
|
270
|
+
person = Person.new
|
271
|
+
|
272
|
+
assert_raise(NoMethodError) { person.is_developer? }
|
273
|
+
assert_equal "Yes", person.send(:is_developer?)
|
274
|
+
assert_equal 1, person.is_developer_calls
|
275
|
+
assert_equal "Yes", person.send(:is_developer?)
|
276
|
+
assert_equal 1, person.is_developer_calls
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: memoist
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Joshua Peek
|
9
|
+
- Tarmo Tänav
|
10
|
+
- Jeremy Kemper
|
11
|
+
- Eugene Pimenov
|
12
|
+
- Xavier Noria
|
13
|
+
- Niels Ganser
|
14
|
+
- Carl Lerche & Yehuda Katz
|
15
|
+
- jeem
|
16
|
+
- Jay Pignata
|
17
|
+
- Damien Mathieu
|
18
|
+
- José Valim
|
19
|
+
autorequire:
|
20
|
+
bindir: bin
|
21
|
+
cert_chain: []
|
22
|
+
date: 2012-01-24 00:00:00.000000000 Z
|
23
|
+
dependencies: []
|
24
|
+
description:
|
25
|
+
email:
|
26
|
+
- josh@joshpeek.com
|
27
|
+
- tarmo@itech.ee
|
28
|
+
- jeremy@bitsweat.net
|
29
|
+
- libc@mac.com
|
30
|
+
- fxn@hashref.com
|
31
|
+
- niels@herimedia.co
|
32
|
+
- wycats@gmail.com
|
33
|
+
- jeem@hughesorama.com
|
34
|
+
- john.pignata@gmail.com
|
35
|
+
- 42@dmathieu.com
|
36
|
+
- jose.valim@gmail.com
|
37
|
+
executables: []
|
38
|
+
extensions: []
|
39
|
+
extra_rdoc_files:
|
40
|
+
- README.md
|
41
|
+
files:
|
42
|
+
- README.md
|
43
|
+
- test/memoist_test.rb
|
44
|
+
- test/test_helper.rb
|
45
|
+
- lib/memoist/core_ext/singleton_class.rb
|
46
|
+
- lib/memoist.rb
|
47
|
+
homepage: https://github.com/matthewrudy/memoist
|
48
|
+
licenses: []
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --main
|
52
|
+
- README.md
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.8.10
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: memoize methods invocation
|
73
|
+
test_files: []
|