请在 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 返回的值,或在缓存命中的情况下返回为 args 缓存的值。也就是说,如果在调用 (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] (函数 [) "为(link:arg1)计算")) ;; 注意如何仅使用该功能轻松实现自定义记忆化器!

在我们的情况下,运行fn synchronized (once)的必要逻辑在core.memoize中实现。因此,如果core.memoize公开类似的设施,我相信这将是一个非常通用的API,它允许构建更多东西(自定义记忆化器实现)。

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

0 投票

评论由:seancorfield

很有趣。我接下来要问的问题是“你从哪里得到这个想法的?”。

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

我需要考虑这个问题。缓存本身连接到由{{build-memoizer}}返回的函数对象的元数据,所以从技术上讲,如果您自己编程这个,所有这些都已经是公开的。

0 投票

评论者:lgs32a

我的理解是,我需要通过带有修补程序(忽略参数)的fn调用private。

真的不能算公开的东西。

0 投票

评论者:lgs32a

而且我还需要复制粘贴以下行之后的代码来处理一个罕见的时间问题。

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 [_] "calc for [arg1]"))

任何使用TTL缓存的用户都必须处理在访问期间的过期问题(除非他们不介意返回{{nil}}),因此,是的,一个执行上述swap!的{{spoof}}函数可能需要重试thunk调用--您是否在此时更新缓存取决于您(即,如果您的{{spoof}}调用期间发生缓存过期,后续调用应该有什么行为?)。

关键是{{spoof}}需要实际的缓存来完成其工作。归一化函数只能通过闭包访问这个缓存,所以{{spoof}}需要在承载归一化函数的{{Var}}上操作。

这样说明的是我提的建议吗?

0 投票

评论者:lgs32a

{{Spoof}}还将利用d-lay中的内部through*用于它的锁定设施。它可能需要微小的调整以支持thunk。

上述推荐的设计有意采用归一化函数,以便可以提取::cache。

关于TTL修复,我必须再次看它。如果你对包含spoof/spoof-aysnc...的补丁感兴趣,请告诉我。

0 投票

评论由:seancorfield

我讨厌这些名称,但如果有人愿意提供一个补丁,暴露出足够多的机制,让用户更容易在库外构建自己的版本。

0 投票

评论者:lgs32a

选择了名称以适应lib的当前设计。Guava为您提供强大的原语并声明通用的低级名称,而本库以其对用户如何使用它的强烈假设为特色编写。它使您立即拥有一个带有缓存的记忆化函数。

这就是为什么我只选择用于描述您使用它们的原因/附加/。可能“欺骗”这个词更好 - 我不是母语人士。

core.cache仅公开缓存的数据结构,但它没有像Guava那样公开适当的引用类型来缓存计算(而不是值)。我认为core.cache的作者没有这样做,因为他们认为Clojure的引用类型就足够了。core.memoize将自己实现为一个内部的引用类型。我不确定是否将此功能(具有大量非记忆化用例)公开为库中的适当位置。因此,对记忆化函数的命名和包装,以清楚地表明该功能是在记忆化范围内。

0 投票
...