我们可以为此使用 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 中的粗略计时统计数据,以确定重复使用相同的函数或每次略有不同的函数是否明显更快。首先我会检查 sum 的结果是否通常缓存在 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)
所以(包括一点差异,使用map 将十个数字乘以sum [2..10000000] 所花费的时间几乎与乘以一个数字所用的时间完全相同。十对数字相乘几乎不需要任何时间。所以 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)
嗯,这花费了大约十倍的时间,正如我们预期的那样,因为现在我们要求它每次都做不同的事情。我可以为您验证,这对每个数字都有暂停,而当我们没有更改计算成本高的数字时,它只评估了一次,并且暂停在第一个数字之前,其余的很快出现。