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

欢迎!请查看关于页面以了解更多关于这个功能的信息。

0 投票
core.memoize

问题:函数A进行了memoize。函数B(及其他功能中)在不调用A的情况下计算A的合法函数调用。我们希望在B中利用这种效果以进一步减少A的计算。如果我们期望B比A运行更快,我们还想让调用A的人等待B的相应运行。

在B中执行这一操作的期望方法是
(spoof A args calc)

其中calc是一个没有参数的fn,返回值等于(apply A args)。这允许调用者将calc中恰好的工作量放入其中,预期会比自己调用A更快。

Spoof返回calc返回的内容,或者在没有缓存命中的情况下,返回为args缓存的值。例如,如果已经发生(A args)的调用,并且我们在调用(spoof A args calc)之前调用了它,那么不会调用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] (函数) "为(链接: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

我还需要复制粘贴从第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 [_] "calc for [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只暴露了缓存的数据结构,但并没有暴露一个合适的引用类型来缓存计算(而不是原始值),就像Guava所做的那样。我认为是因为核心.cache的作者认为Clojure的引用类型已经足够了,所以没有做这件事。core.memoize将这种引用类型以内部的形式实现了。我对是否在库中将这种功能暴露出来感到困惑,因为这种功能在除memoization以外的场景中也有很多用途。所以命名和包装memoized函数以清楚地表明这个功能是在memoization的范围内。

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