【问题标题】:Working with the same random number multiple times in Haskell在 Haskell 中多次使用相同的随机数
【发布时间】:2012-12-11 18:30:53
【问题描述】:

我正在尝试使用 Haskell 创建一个纸剪刀石头游戏来练习我对它的理解。

很遗憾,下面的源代码给出了不需要的答案。

例如:

>play pStone pRandom 1
1 games were played. Player 1 won 1 and player 2 won 1, making the match a draw.

如果进行 1 场比赛,则应该只有 1 场或 0 场胜利。

>play pStone pCopy 100
100 games were played. Player 1 won 1 and player 2 won 1, making the match a draw.

如果进行了 100 场比赛,其中(在第一轮之后)双方都走相同的棋,那么应该只有 1 或 0 胜。

>play pCopy pAntiCopy 100
100 games were played. Player 1 won 31 and player 2 won 37, making player 2 the overall winner.

根据 pCopy 和 pAntiCopy 的预期定义,pAntiCopy 应该赢得 99 或 100,而 pCopy 应该赢得 0 或 1。

我认为这种行为最可能的原因是随机数在最后进行评估,这意味着应该依赖于相同随机数的 2 个值而不是依赖于 2 个单独的随机数。我上面说的对吗?

如果我是正确的,请你告诉我应该如何纠正这个问题?如果我不正确,请您告诉我问题是什么以及应该如何解决?

我已经阅读了一个单独问题的解决方案,它建议生成一个随机数列表,然后使用它们,将它们作为参数从主函数传递给相关函数。不过,我不相信这在这里能很好地工作,因为所需的随机数的数量可能是从 0 到 2*numRounds 的任何值,这取决于正在使用的计划(我打算在这工作时创建更高级的计划)和代码的可读性会进一步降低。

我是 Haskell 和函数式编程的新手,所以对于下面源代码的风格,我深表歉意。如果您对如何改进它有任何建议,也非常欢迎。

import System.Random

data Move= Paper|Scissors|Stone deriving Show
type Plan=([IO Move]->[IO Move]->IO Move)

play :: Plan -> Plan -> Integer -> IO ()
play plan1 plan2 numRounds=do p1intwins<-p1wins;p2intwins<-p2wins;putStr(show numRounds ++ " games were played. Player 1 won " ++ show p1intwins ++ " and player 2 won " ++ show p2intwins ++ ", making " ++ (if p1intwins > p2intwins then "player 1 the overall winner." else (if p1intwins < p2intwins then "player 2 the overall winner." else "the match a draw."))) where (_, _, _, _, _, _, p1wins, p2wins)=(playRound (plan1, plan2, numRounds,[],[], 0, return 0, return 0))

playRound :: (Plan, Plan, Integer, [IO Move], [IO Move], Integer, IO Integer, IO Integer) -> (Plan, Plan, Integer, [IO Move], [IO Move], Integer, IO Integer, IO Integer)
playRound (plan1, plan2, numRounds, p1moves, p2moves, elapsedRounds, p1wins, p2wins)=if elapsedRounds==numRounds then (plan1, plan2, numRounds, p1moves, p2moves, elapsedRounds, p1wins, p2wins) else (playRound (plan1, plan2, numRounds, p1moves++[p1move], p2moves++[p2move], elapsedRounds+1, do p1win<-beatsCaller p1move p2move;p1intwins<-p1wins;return (p1intwins+if p1win then 1 else 0), do p2win<-beatsCaller p2move p1move;p2intwins<-p2wins;return(p2intwins+(if p2win then 1 else 0)) )) where p1move=plan1 p1moves p2moves; p2move=plan2 p2moves p1moves

beatsCaller :: IO Move -> IO Move -> IO Bool
beatsCaller iom1 iom2=do m1<-iom1;m2<-iom2;return(beats m1 m2)

beats :: Move -> Move -> Bool
beats Scissors Paper=True
beats Stone Scissors=True
beats Paper Stone=True
beats _ _=False

--                                           ###############Plans###################
pStone :: Plan
pStone _ _ = return Stone

pScissors :: Plan
pScissors _ _ = return Scissors

pPaper :: Plan
pPaper _ _ = return Paper

pUScissors :: Plan
pUScissors [] _ = randomMove
pUScissors _ _ = return Scissors

pCopy :: Plan
pCopy _ []= randomMove
pCopy _ theirMoves= last theirMoves

pRandom :: Plan
pRandom _ _=randomMove

pAntiCopy :: Plan
pAntiCopy [] _ = randomMove
pAntiCopy ourMoves _ = do ourMove<-last ourMoves;return(beaterOf ourMove)

--                                           ##############Plan Logic###############

beaterOf ::  Move -> Move
beaterOf Scissors = Stone
beaterOf Paper = Scissors
beaterOf Stone = Paper

randomMove :: IO Move
randomMove=do x<-genRandom;return (doRMove x)

doRMove:: Int -> Move
doRMove rand
    |rand==1 =Paper
    |rand==2 =Scissors
    |rand==3 =Stone

genRandom :: IO Int
genRandom =getStdRandom (randomR (1,3))

【问题讨论】:

  • 如果您重新格式化playplayRound 的代码,您可以让人们更轻松地查看您的问题。目前它们非常难以阅读。
  • 我仍然习惯于将函数拆分为多行所需的空白数量,并且通常会留下它,直到我让它正常工作为止。我有过糟糕的经历,我花了很长时间重新阅读我的代码以找出错误发生的原因,然后才意识到我在空白方面犯了一个小错误。不过,您完全正确,我应该在提交帮助之前完成此操作,所以我再次为我糟糕的风格道歉。
  • 我不是在批评你,这只是一个提示,可以帮助你获得更多答案 :) 你的问题很好,它给出了两个很好的答案。

标签: haskell random monads


【解决方案1】:

我稍微重新格式化并注释了您的源文件。要点是您应该将Plan 的类型更改为[Move] -&gt; [Move] -&gt; IO Move,如下所述。

import System.Random

data Move = Paper | Scissors | Stone
  deriving (Show, Eq, Enum, Bounded)

-- Make Move an instance of Random, so that we can use randomIO
-- in order to pick a random Move rather than a hand-written
-- function. We use the derived Enum instance and integer random
-- numbers to do the hard work for us.
instance Random Move where
  randomR (l, u) g = (toEnum n, g')
    where (n, g') = randomR (fromEnum l, fromEnum u) g
  random = randomR (minBound, maxBound)

-- Unchanged, just realigned.
beaterOf :: Move -> Move
beaterOf Scissors = Stone
beaterOf Paper    = Scissors
beaterOf Stone    = Paper

-- Reimplemented in terms of beaterOf, to avoid unnecessary
-- duplication of error-prone information.
beats :: Move -> Move -> Bool
beats x y = x == beaterOf y

-- Most important change. A plan is a strategy that takes two
-- lists of past moves. These are of type Move, not IO Move, as
-- they have already happened. We then have to determine a new
-- one. Here, to choose the next, we allow IO (such as picking
-- a random number, or asking a human player). I also reverse
-- the order of moves, so that the most recent moves are first,
-- because most strategies access the most recent move, and
-- accessing the head is more efficient in a list.
type Plan =  [Move]  -- my moves, latest move first
          -> [Move]  -- opponent's moves, latest move first
          -> IO Move -- my next move, may involve IO

--
-- Specific plans (slightly renamed, otherwise unchanged):
--

-- Always plays a particular move.
always :: Move -> Plan
always m _ _ = return m

-- Copies the latest move of opponent.
copy :: Plan
copy _ []           = randomIO
copy _ (latest : _) = return latest

randomly :: Plan
randomly _ _ = randomIO

-- Moves to the beater of our previous move.
antiCopy :: Plan
antiCopy []           _ = randomIO
antiCopy (latest : _) _ = return (beaterOf latest)

uScissors :: Plan
uScissors [] _ = randomIO
uScissors _  _ = return Scissors

-- Play wrapper. Interface as before.
play :: Plan    -- my plan
     -> Plan    -- opponent's plan
     -> Integer -- number of rounds to be played
     -> IO ()   -- output is printed as text
play myPlan opPlan rounds = do
  (myWins, opWins) <- playRounds
                        myPlan opPlan rounds
                        [] [] -- initialize with empty move lists
                        0 0   -- and 0 wins each
  -- print statistics
  let overallStatus | myWins > opWins = "Player 1 the overall winner"
                    | opWins > myWins = "Player 2 the overall winner"
                    | otherwise       = "the match a draw"
  putStrLn $ show rounds ++ " games were played. "
          ++ "Player 1 won " ++ show myWins ++ " and "
          ++ "Player 2 won " ++ show opWins ++ ", making "
          ++ overallStatus ++ "."

-- Does all the hard work.
playRounds :: Plan    -- my plan
           -> Plan    -- opponent's plan
           -> Integer -- number of rounds still to be played
           -> [Move]  -- our moves so far, latest first
           -> [Move]  -- opponent's moves so far, latest first
           -> Int     -- my wins
           -> Int     -- opponent's wins
           -> IO (Int, Int)  -- final wins
playRounds _      _      0      _       _       myWins opWins =
  return (myWins, opWins) -- if no rounds are left to play, return the final statistics
playRound myPlan opPlan rounds myMoves opMoves myWins opWins = do
  myMove <- myPlan myMoves opMoves -- here's where a random number might get chosen
  opMove <- opPlan opMoves myMoves -- and here again (but nowhere else)
  let myWin = myMove `beats` opMove -- this works on the already chosen Move, not on IO Move
      opWin = opMove `beats` myMove -- dito
  playRound
    myPlan opPlan      -- as before
    (rounds - 1)       -- one round is played
    (myMove : myMoves) -- adding moves in front of the list is easier
    (opMove : opMoves)
    (fromEnum myWin + myWins) -- update win count, turning True to 1 and False to 0
    (fromEnum opWin + opWins)

【讨论】:

  • 非常感谢您彻底重写了我的源代码。它向我展示了我哪里出错了,以及我应该在哪里改进我的风格。我以前没有使用过实例声明,但是您的回答促使我查找它们 - 这是学习练习的一个很好的结果。我想我需要进行一些实验才能完全理解它们如何在这种情况下和其他情况下工作。我非常感谢修复、改进和注释我的代码所花费的时间。
【解决方案2】:

我认为这种行为最可能的原因是随机数在最后进行评估,这意味着应该依赖于相同随机数的 2 个值而不是依赖于 2 个单独的随机数。我上面说的对吗?

你是。由于您实际上并没有在迭代期间进行游戏,而是通过IO-actions,因此您的策略没有简单的Moves - 前几轮的结果 - 可以构建,而是生成Move的配方,在某些情况下涉及执行genRandom 以获得Move。每次在

ourMove<-last ourMoves

last ourMoves 涉及一个genRandom,会产生一个新的(伪)随机数 - 通常与产生前一个Move 的随机数不同。

如果我是正确的,请你告诉我应该如何纠正这个问题?

不要传递IO 操作和IO 的列表-使用genRandom 的操作。您需要纯值来确定策略(pRandom 以及 pCopypAntiCopy 中的初始选择除外)。

必要时在每一步运行伪随机数生成,并将得到的纯Moves传递给下一次迭代。


另外,使用更短的行、使用空格和使用布局使代码更具可读性。

我打算改写成更惯用的风格,但我打字慢,所以可能需要一段时间。

好吧,还是有点乱,因为我保留了很多原始输出和逻辑,但它更容易阅读和遵循,并且产生了预期的输出:

module Rocks where

import System.Random

data Move
    = Paper
    | Scissors
    | Stone
      deriving Show

type Plan = [Move] -> [Move] -> IO Move

play :: Plan -> Plan -> Int -> IO ()
play plan1 plan2 numRounds = do
    (wins1, wins2) <- playRounds plan1 plan2 numRounds [] [] 0 0
    putStrLn $ show numRounds ++ " games were played. Player 1 won "
                ++ show wins1 ++ " and player 2 won " ++ show wins2
                ++ ", making " ++ result wins1 wins2
  where
    result a b
        | a == b    = "the match a draw."
        | otherwise = "player " ++ (if a > b then "1" else "2") ++ " the overall winner."

playRounds :: Plan -> Plan -> Int -> [Move] -> [Move] -> Int -> Int -> IO (Int, Int)
playRounds _     _     0      _      _      wins1 wins2 = return (wins1,wins2)
playRounds plan1 plan2 rounds moves1 moves2 wins1 wins2 = do
    choice1 <- plan1 moves1 moves2
    choice2 <- plan2 moves2 moves1
    let (w1, w2)
            | beats choice1 choice2 = (wins1+1 ,wins2)
            | beats choice2 choice1 = (wins1, wins2+1)
            | otherwise             = (wins1, wins2)
    playRounds plan1 plan2 (rounds-1) (moves1 ++ [choice1]) (moves2 ++ [choice2]) w1 w2

beats :: Move -> Move -> Bool
beats Scissors  Paper       = True
beats Stone     Scissors    = True
beats Paper     Stone       = True
beats _         _           = False

--       ###############Plans###################

pStone :: Plan
pStone _ _ = return Stone

pScissors :: Plan
pScissors _ _ = return Scissors

pPaper :: Plan
pPaper _ _ = return Paper

pUScissors :: Plan
pUScissors [] _ = randomMove
pUScissors _  _ = return Scissors

pCopy :: Plan
pCopy _ []         = randomMove
pCopy _ theirMoves = return $ last theirMoves

pRandom :: Plan
pRandom _ _ = randomMove

pAntiCopy :: Plan
pAntiCopy []       _ = randomMove
pAntiCopy ourMoves _ = return (beaterOf $ last ourMoves)

--       ##############Plan Logic###############

beaterOf ::  Move -> Move
beaterOf Scissors   = Stone
beaterOf Paper      = Scissors
beaterOf Stone      = Paper

randomMove :: IO Move
randomMove = fmap doRMove genRandom

doRMove:: Int -> Move
doRMove rand
    | rand == 1 = Paper
    | rand == 2 = Scissors
    | rand == 3 = Stone
    | otherwise = error "oops"

genRandom :: IO Int
genRandom = getStdRandom (randomR (1,3))

*Rocks> play pCopy pAntiCopy 100
100 games were played. Player 1 won 0 and player 2 won 99, making player 2 the overall winner.
*Rocks> play pStone pRandom 100
100 games were played. Player 1 won 33 and player 2 won 34, making player 2 the overall winner.
*Rocks> play pStone pCopy 100
100 games were played. Player 1 won 1 and player 2 won 0, making player 1 the overall winner.
*Rocks> play pStone pAntiCopy 100
100 games were played. Player 1 won 33 and player 2 won 33, making the match a draw.

【讨论】:

  • 非常感谢您修复我的代码并直接回答我的问题。我以前没有使用过 fmap 函数,尽管我已经阅读过它。我在试图理解它时有些挣扎,但现在我要再看一下它。我真的很感激纠正我的代码所花费的时间。
猜你喜欢
  • 1970-01-01
  • 2019-02-05
  • 2018-05-20
  • 1970-01-01
  • 2019-03-10
  • 1970-01-01
  • 2016-04-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多