【问题标题】:Memoize a function of type () -> 'a记住一个类型为 () -> 'a 的函数
【发布时间】:2013-12-31 04:20:14
【问题描述】:

This memoize function 在运行时在 () -> 'a 类型的任何函数上失败,并出现 Null-Argument-Exception。

let memoize f =
    let cache = System.Collections.Generic.Dictionary()
    fun x ->
        if cache.ContainsKey(x) then 
            cache.[x]
        else 
            let res = f x
            cache.[x] <- res
            res

有没有办法编写一个也适用于() -&gt; 'a 的 memoize 函数?

(我目前唯一的选择是使用Lazy 类型。调用x.Force() 来获取值。)

【问题讨论】:

  • Memoize 通常用于在参数用作查找键的情况下缓存一些计算。在这种情况下,Lazy 是正确的工具。
  • 可以包装lazy,像这样:let memoize f = let result = lazy (f()) in fun () -&gt; result.Value

标签: f# unit-type


【解决方案1】:

具有纯函数(不仅是 unit -&gt; 'a 类型,也包括其他类型)作为查找键的记忆是不可能的,因为函数通常没有 reason 的相等比较器。

对于这种特定类型的函数unit -&gt; 'a,似乎可以提供自定义相等比较器。但是实现这种比较器超越极端(反射、IL 等)的唯一方法是调用查找函数为f1 = f2 iff f1() = f2(),这显然会使记忆化预期的任何性能改进无效。

因此,也许正如已经指出的那样,对于这种情况,优化应该围绕lazy 模式构建,而不是memoization 模式。

更新:确实,在第二次查看问题之后,上面所有关于缺少相等比较器的函数都是正确的,但不适用,因为 memoization 发生在闭包中每个函数的单独 cache 中。另一方面,对于这种带有签名unit-&gt;'a 的特定类型的函数,即最多只有一个参数值,使用Dictionary 和大多数条目是矫枉过正的。以下类似的有状态但更简单的实现只需一个记忆值即可:

let memoize2 f =
    let notFilled = ref true
    let cache = ref Unchecked.defaultof<'a>
    fun () ->
        if !notFilled then
            cache := f ()
            notFilled := false
        !cache

用作let foo = memoize2(fun () -&gt; ...heavy on time and/or space calculation...) 第一次使用foo() 执行并存储计算结果,所有后续的foo() 只是重复使用存储的值。

【讨论】:

    【解决方案2】:

    函数失败的原因是F#使用null类型的unit表示单元()。字典不允许将 null 值作为键,因此它失败了。

    在您的具体情况下,记忆unit -&gt; 'a 类型的函数没有多大意义(因为最好使用lazy),但在其他情况下这会成为问题 - 例如@ 987654327@ 也由null 表示,所以这也失败了:

    let f : int option -> int = memoize (fun a -> defaultArg a 42)
    f None
    

    解决此问题的简单方法是将密钥包装在另一种数据类型中,以确保它永远不会是null

    type Key<'K> = K of 'K
    

    然后你可以用 K 构造函数包装密钥,一切都会很好地工作:

    let memoize f =
        let cache = System.Collections.Generic.Dictionary()
        fun x ->
            if cache.ContainsKey(K x) then 
                cache.[K x]
            else 
                let res = f x
                cache.[K x] <- res
                res
    

    【讨论】:

    • 不应该是Dictionary(HashIdentity.Structural)吗?
    • 它不是为通过调用memoize 定义的每个函数创建自己的cache 字典实例吗?仅因为此多个键K &lt;null&gt; 可能存在unit-&gt;'a 而不会发生冲突。由于函数不共享公共缓存,因此任何文字都可以扮演键的角色,整个安排有效地模仿lazy 方法。
    • @Gene memoize 返回一个关闭它自己的 Dictionary 实例的函数。 memoize 的签名是 f:('a -&gt; 'b) -&gt; ('a -&gt; 'b) when 'a : equality 所以它返回的函数是 'a -&gt; 'b 这是你用来记忆值的返回函数。例如,let m = memoize (fun x -&gt; x + 1);; 给出 val m : (int -&gt; int)
    • @CurtNichols:正如我所说的那样——从memoize 返回的每个函数都关闭了自己的cache 实例,最多只有一个 对。使用Dictionary 仅用于持久化一个value 是IMO 的过度杀伤力,而且根本不需要key,而是使用bool filled 状态指示器就足够了。
    • @Gene Belitski,字典不是由返回的函数创建的,而是由对 memoize 的原始调用创建的。典型用法:调用memoize一次,创建记忆函数m(字典由memoize创建,由函数m关闭);多次调用m,它将缓存字典中的多个条目。如果您修改 fun x -&gt; 表达式以在返回之前打印 (printfn "Cache has %d elements" cache.Count),您将看到字典确实包含多个元素——因为字典是由 memoize 创建的,而不是由 memoize 返回的函数创建的。
    【解决方案3】:

    我刚刚发现最后一个使用Map 而不是Dictionary 的记忆函数on the same website 也适用于'a Option -&gt; 'b() -&gt; 'a

    let memoize1 f =
        let cache = ref Map.empty
        fun x ->
            match (!cache).TryFind(x) with
            | Some res -> res
            | None ->
                let res = f x
                cache := (!cache).Add(x,res)
                res
    

    【讨论】:

      【解决方案4】:

      可变字典和单字典查找调用的解决方案: let memoize1 f = // printfn "Dictionary" let cache = System.Collections.Generic.Dictionary() fun x -> let result, value = cache.TryGetValue(x) match result with | true -> value | false -> // printfn "f x" let res = f x cache.Add(x, res) res

      【讨论】:

      • 这并不能解决问题中提出的问题。出于同样的原因,memoize1 (fun () -&gt; 1) () 也以 ArgumentNullException 失败。
      猜你喜欢
      • 2016-08-26
      • 2015-03-05
      • 2012-02-13
      • 1970-01-01
      • 2018-08-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-09
      相关资源
      最近更新 更多