-
-
Notifications
You must be signed in to change notification settings - Fork 9.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Cached
module to simplify caching code.
#16671
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
module Cached | ||
module Clear | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is a new module needed now? Is this only used once? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
To make the code more readable and easier to maintain, i.e. not having to manually keep track of which cached variables need to be cleared.
So far only in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it'd be good to use this in 2+ places if it's added here to verify the API is sufficiently generic and better test the edges. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think having both I like the approach and I think the code is cool but I'm not sure it's worth the cost here from a maintainability point of view. As a quick comparison of # Now (on master)
def git_head
raise TapUnavailableError, name unless installed?
@git_head ||= git_repo.head_ref
end
# Cached
cached def git_head
raise TapUnavailableError, name unless installed?
git_repo.head_ref
end
# Cachable
def git_head
raise TapUnavailableError, name unless installed?
cache[:git_head] ||= git_repo.head_ref
end These functions are all similar complexity. The advantage of caching is being able to clear the cache with one easy method which would be provided by both Note: Here's another example. # Now (on master)
def audit_exceptions
@audit_exceptions ||= begin
ensure_installed!
super
end
end
# Cached
cached def audit_exceptions
ensure_installed!
super
end
# Cachable
def audit_exceptions
cache[:audit_exceptions] ||= begin
ensure_installed!
super
end
end The complexity is not much different in this case either. I would argue that it's also easier to understand at a glance since it uses basic Ruby patterns as well but that's very subjective. One fine example, that's a bit different. # Cachable (on master)
def self.fetch(user, repo)
...
cache_key = "#{user}/#{repo}".downcase
cache.fetch(cache_key) { |key| cache[key] = Tap.new(user, repo) }
end
# Cached
def self.fetch(user, repo)
...
_fetch(user, repo)
end
private_class_method cached_class_method def self._fetch(user, repo)
new(user, repo)
end The I feel like the simplest approach would just be to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree with most of the above except: I don't think this should be "long-term", I would really like to avoid having both existing separately for any time/at all. |
||
sig { void } | ||
def clear_cache | ||
super if defined?(super) | ||
|
||
return unless defined?(@cached_method_calls) | ||
|
||
remove_instance_variable(:@cached_method_calls) | ||
end | ||
end | ||
|
||
sig { params(method: Symbol).returns(Symbol) } | ||
def cached(method) | ||
uncached_instance_method = instance_method(method) | ||
|
||
define_method(method) do |*args, **options, &block| | ||
@cached_method_calls ||= T.let({}, T.nilable(T::Hash[Symbol, T::Hash[T.untyped, T.untyped]])) | ||
cache = @cached_method_calls[method] ||= {} | ||
|
||
key = [args, options, block] | ||
if cache.key?(key) | ||
cache.fetch(key) | ||
else | ||
cache[key] = uncached_instance_method.bind(self).call(*args, **options, &block) | ||
end | ||
end | ||
end | ||
|
||
sig { params(method: Symbol).returns(Symbol) } | ||
def cached_class_method(method) | ||
uncached_singleton_method = singleton_method(method) | ||
|
||
define_singleton_method(method) do |*args, **options, &block| | ||
@cached_method_calls ||= T.let({}, T.nilable(T::Hash[Symbol, T::Hash[T.untyped, T.untyped]])) | ||
cache = @cached_method_calls[method] ||= {} | ||
|
||
key = [args, options, block] | ||
if cache.key?(key) | ||
cache[key] | ||
else | ||
cache[key] = uncached_singleton_method.call(*args, **options, &block) | ||
end | ||
end | ||
end | ||
Comment on lines
+16
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure it really makes sense for cached methods to take arguments so this can probably be simplified a bit more. None of the cache methods in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I'm a bit surprised that you can use a block as a hash key. I guess I'd just never considered even attempting that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Agreed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually it does make sense, for example There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The block may not be needed though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove this functionality unless it's needed in 3+ places. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is also a pattern in the code to create complex hash keys by concatenating strings with dashes. I'm not sure if this is preferable though to just using arrays of objects. Does anyone have strong feelings either way? |
||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# typed: true | ||
|
||
module Cached | ||
requires_ancestor { Module } | ||
|
||
module Clear | ||
include Kernel | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What changed to make this necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually returning
super
inCoreTap#remote
whenHOMEBREW_NO_INSTALL_FROM_API
is set.