memo_wise 1.1.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,138 +10,106 @@ module MemoWise
10
10
  #
11
11
  # @return [Object] the passed-in obj
12
12
  def self.create_memo_wise_state!(obj)
13
- # `@_memo_wise` stores memoized results of method calls. For performance
14
- # reasons, the structure differs for different types of methods. It looks
15
- # like:
13
+ # `@_memo_wise` stores memoized results of method calls in a hash keyed on
14
+ # method name. The structure is slightly different for different types of
15
+ # methods. It looks like:
16
16
  # {
17
- # no_args_method_name: :memoized_result,
17
+ # zero_arg_method_name: :memoized_result,
18
18
  # single_arg_method_name: { arg1 => :memoized_result, ... },
19
- # [:multi_arg_method_name, arg1, arg2].hash => :memoized_result
19
+ #
20
+ # # Surprisingly, this is faster than a single top-level hash key of: [:multi_arg_method_name, arg1, arg2]
21
+ # multi_arg_method_name: { [arg1, arg2] => :memoized_result, ... }
20
22
  # }
21
- unless obj.instance_variables.include?(:@_memo_wise)
22
- obj.instance_variable_set(:@_memo_wise, {})
23
- end
24
-
25
- # `@_memo_wise_hashes` stores the `Array#hash` values for each key in
26
- # `@_memo_wise` that represents a multi-argument method call. We only use
27
- # this data structure when resetting memoization for an entire method. It
28
- # looks like:
29
- # {
30
- # multi_arg_method_name: Set[
31
- # [:multi_arg_method_name, arg1, arg2].hash,
32
- # [:multi_arg_method_name, arg1, arg3].hash,
33
- # ...
34
- # ],
35
- # ...
36
- # }
37
- unless obj.instance_variables.include?(:@_memo_wise_hashes)
38
- obj.instance_variable_set(:@_memo_wise_hashes, {})
39
- end
23
+ obj.instance_variable_set(:@_memo_wise, {}) unless obj.instance_variable_defined?(:@_memo_wise)
40
24
 
41
25
  obj
42
26
  end
43
27
 
44
- # Determine whether `method` takes any *positional* args.
45
- #
46
- # These are the types of positional args:
47
- #
48
- # * *Required* -- ex: `def foo(a)`
49
- # * *Optional* -- ex: `def foo(b=1)`
50
- # * *Splatted* -- ex: `def foo(*c)`
51
- #
52
- # @param method [Method, UnboundMethod]
53
- # Arguments of this method will be checked
54
- #
55
- # @return [Boolean]
56
- # Return `true` if `method` accepts one or more positional arguments
57
- #
58
- # @example
59
- # class Example
60
- # def no_args
61
- # end
62
- #
63
- # def position_arg(a)
64
- # end
65
- # end
66
- #
67
- # MemoWise::InternalAPI.
68
- # has_arg?(Example.instance_method(:no_args)) #=> false
69
- #
70
- # MemoWise::InternalAPI.
71
- # has_arg?(Example.instance_method(:position_arg)) #=> true
72
- #
73
- def self.has_arg?(method) # rubocop:disable Naming/PredicateName
74
- method.parameters.any? do |param, _|
75
- param == :req || param == :opt || param == :rest # rubocop:disable Style/MultipleComparison
28
+ NONE = :none
29
+ ONE_REQUIRED_POSITIONAL = :one_required_positional
30
+ ONE_REQUIRED_KEYWORD = :one_required_keyword
31
+ MULTIPLE_REQUIRED = :multiple_required
32
+ SPLAT = :splat
33
+ DOUBLE_SPLAT = :double_splat
34
+ SPLAT_AND_DOUBLE_SPLAT = :splat_and_double_splat
35
+
36
+ # @param method [UnboundMethod] a method to categorize based on the types of
37
+ # arguments it has
38
+ # @return [Symbol] one of:
39
+ # - :none (example: `def foo`)
40
+ # - :one_required_positional (example: `def foo(a)`)
41
+ # - :one_required_keyword (example: `def foo(a:)`)
42
+ # - :multiple_required (examples: `def foo(a, b)`, `def foo(a:, b:)`, `def foo(a, b:)`)
43
+ # - :splat (examples: `def foo(a=1)`, `def foo(a, *b)`)
44
+ # - :double_splat (examples: `def foo(a: 1)`, `def foo(a:, **b)`)
45
+ # - :splat_and_double_splat (examples: `def foo(a=1, b: 2)`, `def foo(a=1, **b)`, `def foo(*a, **b)`)
46
+ def self.method_arguments(method)
47
+ return NONE if method.arity.zero?
48
+
49
+ parameters = method.parameters.map(&:first)
50
+
51
+ if parameters == [:req]
52
+ ONE_REQUIRED_POSITIONAL
53
+ elsif parameters == [:keyreq]
54
+ ONE_REQUIRED_KEYWORD
55
+ elsif parameters.all? { |type| type == :req || type == :keyreq }
56
+ MULTIPLE_REQUIRED
57
+ elsif parameters & %i[req opt rest] == parameters.uniq
58
+ SPLAT
59
+ elsif parameters & %i[keyreq key keyrest] == parameters.uniq
60
+ DOUBLE_SPLAT
61
+ else
62
+ SPLAT_AND_DOUBLE_SPLAT
76
63
  end
77
64
  end
78
65
 
79
- # Determine whether `method` takes any *keyword* args.
80
- #
81
- # These are the types of keyword args:
82
- #
83
- # * *Keyword Required* -- ex: `def foo(a:)`
84
- # * *Keyword Optional* -- ex: `def foo(b: 1)`
85
- # * *Keyword Splatted* -- ex: `def foo(**c)`
86
- #
87
- # @param method [Method, UnboundMethod]
88
- # Arguments of this method will be checked
89
- #
90
- # @return [Boolean]
91
- # Return `true` if `method` accepts one or more keyword arguments
92
- #
93
- # @example
94
- # class Example
95
- # def position_args(a, b=1)
96
- # end
97
- #
98
- # def keyword_args(a:, b: 1)
99
- # end
100
- # end
101
- #
102
- # MemoWise::InternalAPI.
103
- # has_kwarg?(Example.instance_method(:position_args)) #=> false
104
- #
105
- # MemoWise::InternalAPI.
106
- # has_kwarg?(Example.instance_method(:keyword_args)) #=> true
107
- #
108
- def self.has_kwarg?(method) # rubocop:disable Naming/PredicateName
109
- method.parameters.any? do |param, _|
110
- param == :keyreq || param == :key || param == :keyrest # rubocop:disable Style/MultipleComparison
66
+ # @param method [UnboundMethod] a method being memoized
67
+ # @return [String] the arguments string to use when defining our new
68
+ # memoized version of the method
69
+ def self.args_str(method)
70
+ case method_arguments(method)
71
+ when SPLAT then "*args"
72
+ when DOUBLE_SPLAT then "**kwargs"
73
+ when SPLAT_AND_DOUBLE_SPLAT then "*args, **kwargs"
74
+ when ONE_REQUIRED_POSITIONAL, ONE_REQUIRED_KEYWORD, MULTIPLE_REQUIRED
75
+ method.parameters.map do |type, name|
76
+ "#{name}#{':' if type == :keyreq}"
77
+ end.join(", ")
78
+ else
79
+ raise ArgumentError, "Unexpected arguments for #{method.name}"
111
80
  end
112
81
  end
113
82
 
114
- # Determine whether `method` takes only *required* args.
115
- #
116
- # These are the types of required args:
117
- #
118
- # * *Required* -- ex: `def foo(a)`
119
- # * *Keyword Required* -- ex: `def foo(a:)`
120
- #
121
- # @param method [Method, UnboundMethod]
122
- # Arguments of this method will be checked
123
- #
124
- # @return [Boolean]
125
- # Return `true` if `method` accepts only required arguments
126
- #
127
- # @example
128
- # class Ex
129
- # def optional_args(a=1, b: 1)
130
- # end
131
- #
132
- # def required_args(a, b:)
133
- # end
134
- # end
135
- #
136
- # MemoWise::InternalAPI.
137
- # has_only_required_args?(Ex.instance_method(:optional_args))
138
- # #=> false
139
- #
140
- # MemoWise::InternalAPI.
141
- # has_only_required_args?(Ex.instance_method(:required_args))
142
- # #=> true
143
- def self.has_only_required_args?(method) # rubocop:disable Naming/PredicateName
144
- method.parameters.all? { |type, _| type == :req || type == :keyreq } # rubocop:disable Style/MultipleComparison
83
+ # @param method [UnboundMethod] a method being memoized
84
+ # @return [String] the arguments string to use when calling the original
85
+ # method in our new memoized version of the method, i.e. when setting a
86
+ # memoized value
87
+ def self.call_str(method)
88
+ case method_arguments(method)
89
+ when SPLAT then "*args"
90
+ when DOUBLE_SPLAT then "**kwargs"
91
+ when SPLAT_AND_DOUBLE_SPLAT then "*args, **kwargs"
92
+ when ONE_REQUIRED_POSITIONAL, ONE_REQUIRED_KEYWORD, MULTIPLE_REQUIRED
93
+ method.parameters.map do |type, name|
94
+ type == :req ? name : "#{name}: #{name}"
95
+ end.join(", ")
96
+ else
97
+ raise ArgumentError, "Unexpected arguments for #{method.name}"
98
+ end
99
+ end
100
+
101
+ # @param method [UnboundMethod] a method being memoized
102
+ # @return [String] the string to use as a hash key when looking up a
103
+ # memoized value, based on the method's arguments
104
+ def self.key_str(method)
105
+ case method_arguments(method)
106
+ when SPLAT then "args"
107
+ when DOUBLE_SPLAT then "kwargs"
108
+ when SPLAT_AND_DOUBLE_SPLAT then "[args, kwargs]"
109
+ when MULTIPLE_REQUIRED then "[#{method.parameters.map(&:last).join(', ')}]"
110
+ else
111
+ raise ArgumentError, "Unexpected arguments for #{method.name}"
112
+ end
145
113
  end
146
114
 
147
115
  # Find the original class for which the given class is the corresponding
@@ -159,15 +127,18 @@ module MemoWise
159
127
  # Raises if `klass` is not a singleton class.
160
128
  #
161
129
  def self.original_class_from_singleton(klass)
162
- unless klass.singleton_class?
163
- raise ArgumentError, "Must be a singleton class: #{klass.inspect}"
164
- end
130
+ raise ArgumentError, "Must be a singleton class: #{klass.inspect}" unless klass.singleton_class?
131
+
132
+ # Since we call this method a lot, we memoize the results. This can have a
133
+ # huge impact; for example, in our test suite this drops our test times
134
+ # from over five minutes to just a few seconds.
135
+ @original_class_from_singleton ||= {}
165
136
 
166
137
  # Search ObjectSpace
167
138
  # * 1:1 relationship of singleton class to original class is documented
168
139
  # * Performance concern: searches all Class objects
169
- # But, only runs at load time
170
- ObjectSpace.each_object(Module).find do |cls|
140
+ # But, only runs at load time and results are memoized
141
+ @original_class_from_singleton[klass] ||= ObjectSpace.each_object(Module).find do |cls|
171
142
  cls.singleton_class == klass
172
143
  end
173
144
  end
@@ -185,77 +156,11 @@ module MemoWise
185
156
  :"_memo_wise_original_#{method_name}"
186
157
  end
187
158
 
188
- # @param target [Class, Module]
189
- # The class to which we are prepending MemoWise to provide memoization;
190
- # the `InternalAPI` *instance* methods will refer to this `target` class.
191
- def initialize(target)
192
- @target = target
193
- end
194
-
195
- # @return [Class, Module]
196
- attr_reader :target
197
-
198
- # Returns the "fetch key" for the given `method_name` and parameters, to be
199
- # used to lookup the memoized results specifically for this method and these
200
- # parameters.
201
- #
202
- # @param method_name [Symbol]
203
- # Name of method to derive the "fetch key" for, with given parameters.
204
- # @param args [Array]
205
- # Zero or more positional parameters
206
- # @param kwargs [Hash]
207
- # Zero or more keyword parameters
208
- #
209
- # @return [Array, Hash, Object]
210
- # Returns one of:
211
- # - An `Array` if only positional parameters.
212
- # - A nested `Array<Array, Hash>` if *both* positional and keyword.
213
- # - A `Hash` if only keyword parameters.
214
- # - A single object if there is only a single parameter.
215
- def fetch_key(method_name, *args, **kwargs)
216
- method = target_class.instance_method(method_name)
217
-
218
- if MemoWise::InternalAPI.has_only_required_args?(method)
219
- key = method.parameters.map.with_index do |(type, name), index|
220
- type == :req ? args[index] : kwargs[name]
221
- end
222
- key.size == 1 ? key.first : [method_name, *key].hash
223
- else
224
- has_arg = MemoWise::InternalAPI.has_arg?(method)
225
-
226
- if has_arg && MemoWise::InternalAPI.has_kwarg?(method)
227
- [method_name, args, kwargs].hash
228
- elsif has_arg
229
- args.hash
230
- else
231
- kwargs.hash
232
- end
233
- end
234
- end
235
-
236
- # Returns whether the given method should use an array's hash value as the
237
- # cache lookup key. See the comments in `.create_memo_wise_state!` for an
238
- # example.
239
- #
240
- # @param method_name [Symbol]
241
- # Name of memoized method we're checking the implementation of
242
- #
243
- # @return [Boolean] true iff the method uses a hashed cache key; false
244
- # otherwise
245
- def use_hashed_key?(method_name)
246
- method = target_class.instance_method(method_name)
247
-
248
- if MemoWise::InternalAPI.has_arg?(method) &&
249
- MemoWise::InternalAPI.has_kwarg?(method)
250
- return true
251
- end
252
-
253
- MemoWise::InternalAPI.has_only_required_args?(method) &&
254
- method.parameters.size > 1
255
- end
256
-
257
159
  # Returns visibility of an instance method defined on class `target`.
258
160
  #
161
+ # @param target [Class, Module]
162
+ # The class to which we are prepending MemoWise to provide memoization.
163
+ #
259
164
  # @param method_name [Symbol]
260
165
  # Name of existing *instance* method find the visibility of.
261
166
  #
@@ -266,7 +171,7 @@ module MemoWise
266
171
  # Raises `ArgumentError` unless `method_name` is a `Symbol` corresponding
267
172
  # to an existing **instance** method defined on `klass`.
268
173
  #
269
- def method_visibility(method_name)
174
+ def self.method_visibility(target, method_name)
270
175
  if target.private_method_defined?(method_name)
271
176
  :private
272
177
  elsif target.protected_method_defined?(method_name)
@@ -274,27 +179,29 @@ module MemoWise
274
179
  elsif target.public_method_defined?(method_name)
275
180
  :public
276
181
  else
277
- raise ArgumentError,
278
- "#{method_name.inspect} must be a method on #{target}"
182
+ raise ArgumentError, "#{method_name.inspect} must be a method on #{target}"
279
183
  end
280
184
  end
281
185
 
282
186
  # Validates that {.memo_wise} has already been called on `method_name`.
283
187
  #
188
+ # @param target [Class, Module]
189
+ # The class to which we are prepending MemoWise to provide memoization.
190
+ #
284
191
  # @param method_name [Symbol]
285
192
  # Name of method to validate has already been setup with {.memo_wise}
286
- def validate_memo_wised!(method_name)
287
- original_name = self.class.original_memo_wised_name(method_name)
193
+ def self.validate_memo_wised!(target, method_name)
194
+ original_name = original_memo_wised_name(method_name)
288
195
 
289
- unless target_class.private_method_defined?(original_name)
196
+ unless target_class(target).private_method_defined?(original_name)
290
197
  raise ArgumentError, "#{method_name} is not a memo_wised method"
291
198
  end
292
199
  end
293
200
 
294
- private
295
-
201
+ # @param target [Class, Module]
202
+ # The class to which we are prepending MemoWise to provide memoization.
296
203
  # @return [Class] where we look for method definitions
297
- def target_class
204
+ def self.target_class(target)
298
205
  if target.instance_of?(Class)
299
206
  # A class's methods are defined in its singleton class
300
207
  target.singleton_class
@@ -303,5 +210,6 @@ module MemoWise
303
210
  target.class
304
211
  end
305
212
  end
213
+ private_class_method :target_class
306
214
  end
307
215
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MemoWise
4
- VERSION = "1.1.0"
4
+ VERSION = "1.5.0"
5
5
  end