【问题标题】:Is memoizing possible without side effects记忆是否可能没有副作用
【发布时间】:2015-08-08 17:45:25
【问题描述】:

我有一些 F# 代码可以缓存结果以供将来查找。我的理解是,您添加的字典和其他数据结构需要副作用。 (即改变字典的状态)这是正确的吗?这被认为是不纯的还是仍然在无副作用计算的模型中。

let rec fib =
    let dict = new System.Collections.Generic.Dictionary<_,_>()
    fun n ->
        match dict.TryGetValue(n) with
        | true, v -> v
        | false, _ -> 
            let fibN =
                match n with
                | 0 | 1 -> n
                | _ -> fib (n - 1) + fib(n - 2)
            dict.Add(n, fibN)
            fibN

【问题讨论】:

  • 我的 F# 可能有点生锈,但我看不出对 fib 的连续调用如何共享 dict 的实例以提供缓存。函数的每个调用不是都有自己的dict 实例吗?
  • @CoderDennis 它是对字典的闭包。 fib 一词与执行逻辑的 lamda 函数绑定。每个调用共享 dict 绑定
  • 副作用的问题在于范围和推理系统如何运作的能力。这就是为什么全局变量是终极邪恶的原因。 fib 函数本身没有副作用。它在实现中具有副作用的事实是无关紧要的。
  • @mydogisbox 不会 dict.TryGetValue(n) 容易受到副作用的影响。不是在这个例子中,而是在一般情况下。因为内存中某个地方的状态会影响运行时值。例如,如果我在第一次调用时在 dict 中存储了错误的值,但返回了正确的值。我第二次调用它时会得到错误的值。
  • @TomDDD:如果(实际)函数是纯函数,则不能在 dict 中存储错误的值,因为对于给定的 arg,函数只能产生一个值。

标签: f# functional-programming memoization side-effects


【解决方案1】:

要重述 cmets 中提到的内容,您可以将记忆提取到一个更高阶的函数中,该函数将返回作为参数传入的函数的记忆版本:

let memoize f =
    let dict = new System.Collections.Generic.Dictionary<_,_>()
    fun x ->
        match dict.TryGetValue(n) with
        | true, v -> v
        | false, _ ->
             let v = f x
             dict.Add(x, v)
             v

通过这样做,您实际上可以使 memoized 函数成为纯函数,并且 memoization 成为实现细节。您发布的代码将这两个问题纠缠在一起,推理起来并不那么简单。

请注意,记忆递归函数有点棘手 - 您需要以适合记忆的方式构造函数以记忆。

这里的另一个问题是您可能遇到的并发问题。为了解决这些问题,您可以锁定dict.Add

...
let v = f x
lock dict <| fun () ->
    if not <| dict.ContainsKey(x) then
       dict.Add(x, v)
v

或者用一个ref 单元来代替Dictionary,而不是Map(在这种情况下,您可能遇到的任何并发问题仍然存在,但本质上不再是灾难性的)。

【讨论】:

    【解决方案2】:

    memoized 函数存储结果,因此它不必在使用相同参数的后续调用中计算结果。存储结果的事实是一个副作用,它也是记忆函数的定义属性。因此,我得出结论,您的问题的答案是“否”。

    解决您关于在 dict 中存储错误值的评论;是的,你是对的,但还有另一个问题不涉及不正确的结果。 Dictionary 类不是线程安全的。如果两个线程同时尝试读取和/或写入字典,您可能会遇到异常。例如:

    let data = [| 1 .. 20 |]
    let fibs = data |> Array.Parallel.map fib
    

    当我在 F# 交互中多次运行此程序时,我没有收到任何异常,但经过某些更改,我得到了 System.ArgumentException:

    已添加具有相同密钥的项目。

    变化是这些;在每种情况下,我在第一次或第二次尝试时都遇到了异常:

    • 使用printfn检测打印诊断信息的功能
    • 将数字类型更改为 uint64(删除 printfn 诊断)
    • 将数值类型更改为float(即System.Double)
    • 将数字类型更改为bigint

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-01
      • 2023-04-06
      • 1970-01-01
      • 2021-05-23
      • 1970-01-01
      • 2018-02-05
      相关资源
      最近更新 更多