在Haskell中计算大量骰子掷出的平均值

4

为了更好地学习Haskell,我正在尝试编写一个程序,显示掷X次2个骰子的和的平均值。在C、Java、Python中,这相当简单,但是在Haskell中我卡住了。以下是一个天真的尝试:

import System.Random

main = do
    g <- getStdGen
    let trials = 10000000
    let rolls = take trials (randomRs (2, 12) g :: [Int])
    let average = div (sum rolls) trials
    print average

对于少量试验,程序可以正常运行。但当我使用一千万次试验运行此代码时,会出现错误:

Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.

写这个程序的方法肯定有更好的办法。在C、Java和Python版本中,这是一项简单的任务。我看过这篇文章(大约理解了其中75%的内容),但当我将那段代码适应到这种情况下时,对一个R [Int]序列求和没有起作用(而且我不确定如何“解开”[Int])。我做错了什么?正确的方法是什么?如何在Haskell中达到随机数启示?

编辑:除了已选择的答案外,正如rtperson在下面指出的那样,模拟两个骰子的方法是不正确的;它实际上应该是从1到6独立投掷两次的总和。


结果是6,SCNR :) - duedl0r
2
实际上,两个骰子点数之和的期望值为(1+2+3+4+5+6)/6 * 2 = 7。这个练习的重点是学习如何使用编程语言,而不是统计方面。 - benson
哈哈 :) 在计算一个骰子的期望值后出现了一个四舍五入误差 :) - duedl0r
在链接的帖子中,展开 R [Int] 的方法是使用 runRandom 函数。 - Dan Burton
2个回答

7

sum不能很好地处理长列表,它在线性空间中运行。请尝试使用这个严格版本的sum

sum' = foldl' (+) 0

foldl'Data.List中定义。

编辑更多信息可以在这篇HaskellWiki文章中找到。


谢谢,我已经很久没有做函数式编程了。顺便说一下,如果程序分析表明计算将被评估,是否可能使sum从惰性转换为严格? - benson
@benson:是的,有严格性分析这种东西,但我不确定为什么在这种情况下它不起作用。 - n. m.
2
据我所知,严格性分析仅在启用优化时才进行。使用 ghc -O1 或更高版本进行编译即可。 - hammar
1
@n.m. 默认情况下不进行优化。 - hammar

4
实际上,这里的概率模型是错误的。代码编写方式导致得到2到12的可能性相同。但是骰子并不是这样工作的。在12种可能结果中,只有一种方法可以得到2(通过1和1)和12(通过6和6)。但是有6种方法可以得到7(1-6、2-5、3-4、4-3、5-2、6-1)。使用两个骰子进行建模,而不是单个2到12的机会,将给出正确的期望值为7。
然而,这里真正让你难以置信的是,你不能简单地做以下事情:
let rolls1 = take trials (randomRs (1, 6) g :: [Int])
let rolls2 = take trials (randomRs (1, 6) g :: [Int])

因为rolls1和rolls2将得到相同的结果。

*Main> let rolls = zip rolls1 rolls2
*Main> take 10 rolls
[(3,3),(4,4),(5,5),(3,3),(5,5),(1,1),(3,3),(1,1),(3,3),(3,3)]

你得到的结果将始终是偶数,因此仍然是错误答案6。

要获得两个不同的随机列表,您需要生成一个新的StdGen:

import System.Random
import Data.List

main = do
    g <- getStdGen
    b <- newStdGen   -- calls "split" on the global generator
    let trials = 10000000
    let rolls1 = take trials (randomRs (1, 6) g :: [Int])
    let rolls2 = take trials (randomRs (1, 6) b :: [Int])
    let rolls = zipWith (+) rolls1 rolls2
    let average = div (foldl' (+) 0 rolls) trials
    print average

(请注意,我特意避免使用State monad。不用谢。)
这个版本比原版本运行时间更长,但是zipWith的惰性和foldl'的严格性确保你不会溢出堆栈。
如果你只是想修复溢出问题,n.m.的答案是正确的。 foldl'的严格性将解决性能问题。但在这种情况下,获得正确答案也将教你一些关于Haskell中随机数生成的知识。

当然,你可以只从一个列表中查看一对,这可能会更加简洁 :-) - sclv
@sclv - 是的。我是从 State monad Wikibooks 文章中的 "clumsyRollDice" 函数反向思考的(http://en.wikibooks.org/wiki/Haskell/Understanding_monads/State),该函数在每次掷骰子后都会创建一个新的 StdGen。 - rtperson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接