请在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] (函数 [) "计算链接:arg1")) ;; 注意这样使用这个功能就能轻松实现自定义内存化器!

运行 fn synchronized (once) 的必要逻辑在我们这个案例中是由 core.memoize 实现的。所以如果 core.memoize 能够暴露类似的功能,我相信这将是一个非常通用的 API,它允许在顶部构建更多的东西(自定义内存化器实现)。

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

0

评论由:seancorfield做出

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

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

我需要认真思考一下。缓存本身就附在从 {{build-memoizer}} 返回的函数对象的相关元数据上,因此,技术上,如果你想自己编程,所有这些都已经暴露出来。

0

评论者:lgs32a

我的理解是,我需要用修改过的(忽略参数)fn 通过 * 调用。

真的不能称为公开的 :)

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}}也会利用inner through*作为其在d-lay中的锁定机制。这可能需要稍作调整以支持thunk。

上述建议的设计故意采用被缓存的函数,以便它可以提取::cache。

关于TTL修复,我必须再看一下。如果你对spoof/spoof-aysnc…的补丁感兴趣,请告诉我。

0

评论由:seancorfield做出

我不喜欢这些名称,但我会考虑一个补丁,暴露足够的机制以满足用户在外部构建自己的版本的需要 easier。

0

评论者:lgs32a

我选择了这些名称以适应库的当前设计。Guava提供强大的原语,并声称具有通用低级名称,而此库是按照它对用户想用它做什么有强假设的风格编写的。这使得你立即具有一个带有缓存附加的缓存函数。

这就是为什么我选择只有描述为什么你会使用它们/额外/的名字。可能比“spoof”更好的词——我不是母语人士。

core.cache只公开了缓存的数据结构,但没有像Guava那样公开一个适合缓存计算的引用类型(而不是值)。我怀疑core.cache的作者没有做这件事是因为他们认为Clojure的引用类型就足够了。core.memoize在其内部实现了一个这样的引用类型。我不确定是否在lib中公开这项功能(这是一个相当多的使用场景外的memoization)是否是正确的位置。因此,命名和包装memoized函数以清楚地表明功能是在memoization范围内。

0
...