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 返回的值,或者在缓存 hit 的情况下返回为 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 Guava的缓存。

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

它允许这样的备忘录。你只需要调用
(.get c (link: arg1] (fn [) "calc for (link: arg1)")) ;; 注意利用这个功能可以实现一个自定义备忘录是多么简单!

所需的核心逻辑fn synchronized (once)在核心.memoize中得到了实现。所以,如果core.memoize公开相似的功能,我相信这将是一个非常通用的API,允许构建更多东西(自定义备忘录实现)。

它还有.put方法,用于处理非同步情况。

0 投票

评论由:seancorfield 发布

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

在{{core.memoize}}中的基础机制与以前类似,使用一个* }函数来处理thunk等,但是{{build-memoizer}}函数封装了所有这些,以及{{PluggableMemoizer}}类型。

我需要认真考虑一下。缓存与从{{build-memoizer}}返回的函数对象的元数据相关联,因此,如果您想自己编程,技术上已经全部公开了。

0 投票

评论由:lgs32a发表

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

真的不能称之为公开:)

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缓存的用户都必须处理在访问过程中可能出现的边缘情况(除非他们不介意返回{{null}}),因此,是的,上述操作的{{spoof}}函数可能会需要重试thunk调用——你在这个点上是否也更新了缓存将由你自己决定(即,如果{{spoof}}调用期间发生缓存驱逐,你希望随后的调用有何行为?)。

关键是{{spoof}}需要实际的缓存来完成它的任务。携带函数只能通过闭包间接访问该缓存,因此{{spoof}}必须操作携带记值函数的{{Var}}。

这澄清了我想说的吗?

0 投票

评论由:lgs32a发表

{{Spoof}}也会利用内部通过*在d-lay中提供锁定功能。可能需要微调以支持thunk。

上面推荐的上述设计故意采用了一个携带函数,以便它能提取::cache。

关于TTL修复,我需要再看一下。如果你对带有spoof/spoof-aysnc...的补订感兴趣,请告诉我。

0 投票

评论由:seancorfield 发布

我讨厌这些名字,但我会考虑一下暴露足够多的机器结构,以便用户能够更轻松地在库外构建自己的版本的补订。

0 投票

评论由:lgs32a发表

选择了合适的名称以符合库的设计。相对于Guava提供了强大的原语并声称是通用的底层名称,这个库采用的风格是做出强假设关于用户如何使用它。它让用户立即拥有一个带缓存的备忘化函数。

这就是为什么我选择的名字只描述了你可能会用它们的原因,也可能有比“欺骗”更好的词 - 我不是母语者。

core.cache只暴露了缓存的层结构,但它不暴露合适的引用类型以缓存计算(而不是值),如Guava所做。我怀疑core.cache的作者没有做这个,因为他们认为Clojure的引用类型足以满足。core.memoize以内部方式自己实现了这样的引用类型。我不确定暴露这种功能,它在备忘化以外的许多用例中都有很好的应用,是否应该在这个库中正确的地方。因此,命名并围绕备忘化函数包装,以清楚地表示该功能是在备忘化的范围内。

0 投票
by
参考: https://clojure.atlassian.net/browse/CMEMOIZE-24 (由lgs32a报告)
...