记忆可能没有副作用

时间:2015-05-26 21:07:34

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

我有一些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

2 个答案:

答案 0 :(得分:5)

要重述注释中提到的内容,您可以将memoization提取到更高阶的函数中,该函数将返回作为参数传入的函数的memoized版本:

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是一个实现细节。你发布的代码,这两个问题纠缠在一起,并不像理所当然那么容易理解。

请注意,记忆递归函数有点棘手 - 您需要以一种有助于记忆的方式构造函数以进行memoize。

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

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

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

答案 1 :(得分:1)

memoized函数存储结果,因此它不必在后续调用中使用相同的参数计算结果。存储结果的事实是副作用,它也是memoized函数的定义属性。因此,我的结论是,你的问题的答案是&#34;没有。&#34;

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

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

当我在F#interactive中多次运行时,我没有得到任何异常,但是在某些更改的情况下,我得到了一个System.ArgumentException:

  

已添加具有相同键的项目。

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

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