请在 2024 Clojure 状态调查! 中分享您的想法。

欢迎!有关这一点如何工作的更多信息,请参阅 关于 页面。

0
core.memoize

问题:函数 A 已进行缓存。函数 B 在其他方面计算函数 A 的有效函数调用,而从未调用 A。我们希望在 B 中利用这一效果来进一步减少 A 的计算量。如果我们预计 B 的执行速度比 A 快,我们还希望调用 A 的人等待对应的 B 执行。

B 中实现此操作的一个期望操作是
(spoof A args calc)

其中 calc 是一个零参数的 fn,它产生一个值等于 (apply A args)。这允许调用者将 calc 需要执行的恰好是预期的比 A 更快的工作量。

Spoof 返回 calc 返回的内容,或者在缓存命中的情况下,返回给定的参数缓存的值。例如,如果之前的调用已经在 (spoof A args calc) 调用之前发生了 (A args),则不会调用 calc。

在已经获得 (A args) 的结果,并且不希望被 (A args) 的并发调用阻塞的情况下,期望的操作是

(spoof-unsynchronized A args val)

它立即返回 val 并很快更新 A 的缓存。由于非同步欺骗是一个非常快的操作,它本质上会与 (A args) 的调用发生竞争条件,因此不需要阻塞 (A args) 的调用。(所以直接对内部缓存状态进行一次 swap! 就足够了)。

11 个回答

0

评论由:seancorfield 制作

这看起来不像应该是 core.memoize 库的一部分,但它应该可以在其之上构建类似的东西(并且目前还不是,对吗?)。

必要的最小更改是否需要暴露底层的缓存(这样可以直接对缓存调用 deref/swap!)?

0

评论人:lgs32a

对于非同步情况是,对于同步(有趣的)情况,可能需要恶意黑客技术,我需要查看代码。

我不同意这种功能不应该内置。它相当通用,允许非常灵活的、自定义的缓存实现。

上面推荐的设计是从Google Guavas缓存中借鉴的。

https://google.github.io/guava/releases/23.0/api/docs/com/google/common/cache/Cache.html#get-K-java.util.concurrent.Callable-

它允许这样缓存。您只需调用
(.get c (链接:arg1] (fn []) "为(链接:arg1)计算")) ;; 注意如何仅使用这一设施就轻松实现自定义缓存器!

运行fn synchronized(一次性)所必需的逻辑在我们的情况下在core.memoize中实现。所以如果core.memoize公开类似的设施,我相信这将是一个非常通用的API,允许构建更多东西(自定义缓存器实现)。

它还有.put来处理非同步情况。

0

评论由:seancorfield 制作

很有趣。我的下一个问题是“你是怎么想到这个的?”。

在{{core.memoize}}中的底层机制以类似的方式工作,有* }函数来处理thunk等,但是{{build-memoizer}}函数封装了所有这些,以及{{PluggableMemoizer}}类型。

我需要好好思考这个问题。缓存本身附加到{{build-memoizer}}返回的函数对象的元数据上,所以,如果您想自己编程,技术上已经全部公开了。

0

评论人:lgs32a

我的理解是,我需要通过*with调用带有patched(args忽略)fn的private。

确实无法称其为公开:

0

评论人:lgs32a

我还需要复制粘贴第209行之后的代码来处理一个罕见的时序问题。

0

评论人:lgs32a

或者,我误解了你的意思,你建议如果我想编写补丁的话?

0

评论由:seancorfield 制作

我的意思是:如果您想检查一个参数列表键是否存在,并且或通过您上面提到的thunk来更新缓存中的新值,原始缓存就在元数据中。您可以直接在缓存上调用{{core.cache/through}}(或者更有可能是{{core.cache/through-cache}}),配合thunk(尽管只有一个参数,您可以忽略它)。

(swap! (:clojure.core.memoize/cache (meta the-fn-var)) clojure.core.cache/through-cache [arg1] (fn [_] "为[arg1]计算"))

任何使用TTL缓存的用户都必须处理在访问期间被逐出(除非他们不在意返回{{nil}})的边缘情况,所以,是的,一个执行上述swap!的{{spoof}}函数可能需要重试thunk调用——此时是否更新缓存取决于您(即,如果{{spoof}}调用期间发生了缓存驱逐,您希望后续调用有什么行为?)。

关键点是{{spoof}}需要实际的缓存来完成工作。备忘录函数只能通过闭包访问该缓存,因此{{spoof}}必须对持有备忘录函数的{{Var}}进行操作。

这澄清我建议的意思了吗?

0

评论人:lgs32a

{{Spoof}}也会利用d-lay中的内部通过*来使用其锁定功能。这可能需要稍作调整以支持thunk。

上述建议的设计故意采用备忘录函数,以便可以提取::cache。

关于TTL修复,我需要再次审视。如果你对带有spoof/spoof-aysnc...的补丁感兴趣,请告诉我。

0

评论由:seancorfield 制作

我不喜欢这些名字,但我会考虑一个补丁,它允许用户更容易地在外部构建自己的版本,而不需要库。

0

评论人:lgs32a

我选择了这些名字以符合库当前的架构。Guava提供了强大的原语并声称具有通用的低级名称,而这个库是以它假定用户想要如何使用它而编写的。它使您拥有一个带缓存的备忘录函数。

这就是我选择只有描述为什么你会使用它们(以及/或额外地)的名称的原因。可能“spoof”这个词不是很好——我不是母语者。

core.cache仅暴露了缓存的datastructure结构,但是并没有暴露适当的引用类型来对缓存计算(而不是值)进行引用,就像Guava那样。据我推测,core.cache的作者没有这样做,因为他们认为Clojure的引用类型已经足够了。core.memoize为此内部实现了一个引用类型。我不确定是否在库中将这种具有很多用途(而不仅仅是memoization)的功能暴露出来是否是合适的。因此,命名和包装memoized函数被用来清楚地表明该功能是在memoization的范围内。

0
...