请在 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

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

我不同意不应该内置这个功能。它非常通用,并允许非常灵活的用户自定义备忘录实现。

上面推荐的设计是从谷歌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] (函数 [) "为零计算 (链接:arg1)"));; 注意,仅使用这个功能就很容易实现定制的备忘录!

运行 fn synchronized (一次) 所需的逻辑在我们的例子中是在 core.memoize 中实现的。所以如果 core.memoize 会暴露类似的功能,我相信这将是一个非常通用的API,它能允许构建更多的东西(定制的备忘录实现)。

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

0

由 seancorfield 发表的评论:

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

core.memoize 的底层机制以类似的方式进行,有一个带有 * } 函数处理 thunks 等,但封装所有这些的是 {{build-memoizer}} 函数,还有可插拔的 {{PluggableMemoizer}} 类型。

我得好好想想。缓存本身附属于从 {{build-memoizer}} 返回的函数对象的元数据,所以,如果你要自己编程,技术上其实已经全部暴露了。

0

评论者:lgs32a

我的理解是,我需要通过带补丁(忽略参数)的 fn 绑定 "private"。

无法真正调用这暴露的 :)

0

评论者:lgs32a

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

0

评论者:lgs32a

或者,是我理解错了,你建议我编写补丁吗?

0
by

由 seancorfield 发表的评论:

我的意思是:如果需要检查参数列表键是否存在以及/或使用上面提到的延迟调用来更新缓存的新值,原始缓存就在元数据中。可以在这个缓存上直接调用{{core.cache/through}}(或者更可能地,{{core.cache/through-cache}}),使用一个延迟调用(虽然只需要一个参数,你可以忽略它)。

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

任何TTL缓存的用户都必须潜在地处理访问期间被驱逐的情况(除非他们不关心返回{{null}}),因此,是的,一个执行上述swap!的{{spoof}}函数可能需要重试延迟调用的调用——你是否也在此时更新了缓存将取决于你(即,如果{{spoof}}调用期间发生了缓存驱逐,你想要后续调用有哪些行为?)。

重点是{{spoof}}需要实际的缓存来完成其工作。memoized函数只能通过闭包访问该缓存,因此{{spoof}}必须在包含memoized函数的{{Var}}上操作。

这澄清了我建议的内容吗?

0
by

评论者:lgs32a

{{Spoof}}也会利用d-lay中的内部through*提供其锁定功能。可能需要对支持thunk进行一些小的调整。

上面建议的设计有意识地采用了一个memoized函数,以便从中提取::cache。

关于TTL修复,我需要再看一遍。如果你有兴趣,请告诉我,是否有关于spoof/spoof-aysnc的补丁...

0
by

由 seancorfield 发表的评论:

我不喜欢这些名字,但我会考虑一个补丁,使足够多的机制暴露出来,以使用户更容易在库外部构建自己的版本。

0
by

评论者:lgs32a

我为满足当前库的设计选择了名字。Guava为您提供了强大的原语并提供了通用的低级别名称,而此库编写的方式则假定您想如何使用它。它使您立即有一个带有缓存的memoized函数。

这就是我为什么选择只描述为什么使用名称的原因/此外/。可能比“欺骗”更好的词 - 我不是母语人士。

core.cache 仅公开缓存的数据结构,但它不公开适当的引用类型来缓存计算(而不是值),就像Guava那样。我认为core.cache的作者疏忽了这一操作,因为他们认为Clojures的引用类型足以满足需求。core.memoize将此类引用类型作为内部实现。我不确定是否在lib中将这种功能暴露出去(这种功能有许多用例,而不仅仅是记忆化)是否合适。因此,命名和围绕记忆函数包装以清楚地表明该功能应在记忆化的范围内。

0
...