如何避免使用相同的参数重新计算纯函数?

时间:2014-11-15 19:26:47

标签: haskell sum complexity-theory probability memoization

我有以下函数将计数列表转换为离散概率密度函数:

freq2prob l = [ (curr / (sum l))) | curr <- l ]

不幸的是,为每个(sum l)元素计算了l,这使得计算复杂度不必要地高。

什么是最简洁,优雅,&#34; haskellic&#34;处理这个的方法?

2 个答案:

答案 0 :(得分:4)

很简单:

freq2prob l = [ curr / s | let s = sum l, curr <- l ] 

你也可以把它放在列表理解之外:freq2prob l = let s = sum l in [ curr / s | curr <- l ](注意in)。这实际上是相同的计算。

这是因为第一个基本上被翻译成

freq2prob :: (Fractional a) => [a] -> [a]
freq2prob l = [ curr / s | let s = sum l, curr <- l ] 
 = do
     let s = sum l
     curr <- l
     return (curr / s)
 = let s=sum l in
   l >>= (\curr -> [curr / s])
   -- concatMap (\curr -> [curr / s]) l
   -- map (\curr -> curr / s) l

和第二个,显然是相同的代码,

freq2prob l = let s = sum l in [ curr / s | curr <- l ]
 = let s = sum l in
   do
     curr <- l
     return (curr / s)
 = let s=sum l in
   l >>= (\curr -> [curr / s])

答案 1 :(得分:4)

我们可以使用let语句或where子句:

freq2prob l = let s = sum l in 
              [ curr / s | curr <- l ]

freq2prob l = [ curr / s | curr <- l ] 
    where s = sum l

但使用高阶函数比列表理解更惯用,因为你对每个元素都做了同样的事情:

freq2prob l = map (/sum l) l

分割函数sum l中的(/sum l)只会被评估一次。

这是因为在评估map f xs时,编译器不会产生基本错误,即创建要单独评估的函数f的多个副本;它是一个thunk,将在每次需要时指向它。

作为一个简单而直率的测试,我们可以研究ghci中的原始时序统计数据,以确定每次重复使用相同的函数或稍微不同的函数是否明显更快。首先,我将检查总和的结果是否通常缓存在ghci中:

ghci> sum [2..10000000]
50000004999999
(8.31 secs, 1533723640 bytes)
ghci> sum [2..10000000]
50000004999999
(8.58 secs, 1816661888 bytes)

所以你可以看到它没有被缓存,并且这些原始统计数据存在一些差异。 现在让我们每次都乘以同样复杂的东西:

ghci> map (* sum [2..10000000]) [1..10]
[50000004999999,100000009999998,150000014999997,200000019999996,250000024999995,300000029999994,350000034999993,400000039999992,450000044999991,500000049999990]
(8.30 secs, 1534499200 bytes)

所以(包括一点变化,使用sum [2..10000000]乘以map乘以十个数字几乎完全相同,而不是乘以一个。乘以十对数字几乎不需要任何时间所以ghci(一个解释器,甚至不是优化编译器)都没有引入相同计算的多个副本。

这不是因为ghci很聪明,而是因为懒惰的评估,纯函数式编程的一个很好的特性,从来没有做过多于必要的工作。在大多数编程语言中,很难优化掉在整个地方传递冗长的计算而不是将结果保存在变量中。

现在让我们将其与每次略微不同的计算进行比较,我们在这里计算的数字会略微减少。

ghci> map (\x -> sum [x..10000000]) [1..10]
[50000005000000,50000004999999,50000004999997,50000004999994,50000004999990,50000004999985,50000004999979,50000004999972,50000004999964,50000004999955]
(77.98 secs, 16796207024 bytes)

嗯,正如我们预期的那样,大致大约是的十倍,因为现在我们要求它每次都做不同的事情。我可以验证你为每个数字暂停,而当我们没有更改昂贵的计算数字时,它只被评估一次,暂停在第一个数字之前,其余的快速出现。