2012-08-08 192 views
7

我一直試圖在過去幾週中獨立學習Haskell。目前,我正試圖實施一個愚蠢的小猜謎遊戲,其中計算機選擇一個隨機數並且用戶試圖猜測它。如果用戶錯了,程序會告訴用戶答案是更高或更低,並允許用戶猜測,直到他們猜測正確。我已經掌握了它的工作原理,但是我想增加跟蹤用戶每場比賽的猜測次數的能力,並且一旦他們猜測正確,就向用戶報告這個數字。如何跟蹤一個簡單的猜謎遊戲中的猜測次數(Haskell)

從勢在必行的背景來看,自然要做的事情就是每次用戶猜測時都會增加一個計數器,但在Haskell中你不能這麼做(至少看起來像無狀態和一切的不變性會阻止)。 (我們把它稱爲numGuesses),並且在每次調用這些方法時,都會傳遞(numGuesses + 1),這會導致getGuess和giveHints函數需要額外的參數, 。但我無法讓它工作(更不用說我甚至不知道這是否會奏效)。

我的代碼如下。任何建議將非常感激。我主要在尋找想法,但也可以隨時發佈實際的代碼。此外,隨時讓我知道,如果我的代碼很爛,我怎麼能改善它,如果你注意到有什麼十惡不赦

import System.Random 
    import System.IO 
    import Control.Monad 

    main = do 
     gen <- getStdGen 
     let (ans,_) = randomR (1,100) gen :: (Int,StdGen) 
     putStrLn $ "I'm thinking of a number between 1 and 100..." 
     getGuess ans 
     putStrLn "You guessed it in __ guesses!" 
     putStr "Play again? " 
     hFlush stdout 
     desire <- getLine 
     when ((desire !! 0) `elem` ['y','Y']) $ do 
      putStrLn "" 
      newStdGen 
      main 

    getGuess ans = do 
     putStr "Your guess? " 
     hFlush stdout 
     guessStr <- getLine 
     giveHints ans (read guessStr) 

    giveHints ans guess = do 
     when (ans /= guess) $ do 
      if ans > guess 
       then putStrLn "It's higher." 
       else putStrLn "It's lower." 
      getGuess ans 

注(我只是一直在功能上編程的幾個星期!):我使用hFlush標準輸出,因爲我使用線緩衝,沒有它,一些交互的順序不是人們所期望的。

+3

你的想法(添加一個參數,已經計算了多少次猜測)是完美的。當你嘗試時出了什麼問題? – 2012-08-08 01:44:03

+4

我想解決這個代碼中與實際問題無關的一個奇怪的位(這就是爲什麼這是一個單獨的註釋)。如果使用的是明確的種子,則應該使用'setStdGen'將'randomR'返回的新種子放回,而不是使用'newStdGen'完全重新放入。 (事實上​​,在一個理想的世界裏,你根本就不會再種下種子,而是在節目的不同運行中保存種子,如果你有興趣,可以問我爲什麼。)或者,你可以跳過'getStdGen/randomR/setStdGen通過使用'randomRIO'來封裝這個模式。 – 2012-08-08 01:48:15

+1

我很感興趣。爲什麼? ;-) – 2012-08-08 02:21:43

回答

8

你實際上可以實現你正在考慮的計數方法,但是你仍然必須明確地傳遞狀態。但在這種情況下,它根本不是什麼麻煩。實際上,這是一種很常見的幫助函數模式,實際上使用State monad會過度。

我指的模式往往是這樣的:

doStuff xs' = go xs' 0 
    where 
    go (x:xs) n = .. etc .. 

下面的代碼。

import System.Random  (randomRIO) 
import Control.Applicative ((<$>)) 
import Control.Monad  (when) 
import Text.Printf   (printf) 

playGame :: Int -> Int -> IO() 
playGame answer curGuesses = do 
    putStrLn "What is your guess?" 
    putStr ">" 
    guess <- getGuessFromUser 
    when (guess /= answer) $ do 
     giveHints answer guess 
     playGame answer (curGuesses + 1) 
    when (guess == answer) $ do 
     putStrLn "You guessed it!" 
     printf "You guessed %d times!\n" (curGuesses + 1) 

giveHints :: Int -> Int -> IO() 
giveHints answer guess 
    | answer > guess = putStrLn "It's higher!" 
    | otherwise  = putStrLn "It's lower!" 

getGuessFromUser :: IO Int 
getGuessFromUser = do 
    read <$> getLine 

main :: IO() 
main = do 
    answer <- randomRIO (1, 100) 
    putStrLn "I'm thinking of a number between 1 and 100." 
    playGame answer 0 

注意

  • <$>fmap
  • 我以前randomRIO丹尼爾提到的,由於我們在IO單子是已。
  • 我沒有必要使用hSetBufferinghFlush在Windows上使用命令提示符來獲取正確的輸出。 YMMV,但是。
+0

你也可以使用'if guess == answer then ... else do ...'而不是有兩個類似的'when'語句。 – huon 2012-08-08 05:54:18

+0

是的,我知道,但如果可能的話,我喜歡遠離if-then-else。只是一個偏好問題。 – identity 2012-08-08 10:10:02

+1

@identity感謝您的詳細回覆。我現在看到,我的主要缺陷是我的相互遞歸的giveHints和getGuesses函數,這非常糟糕。你的組織很有意義。 – mkfarrow 2012-08-08 17:06:44

3

爲猜測數目增加一個額外的參數正是你在功能上做這種事情的方式。

思考的基本功能模式是,如果您有一個函數需要根據「某些」的不同值而表現出不同的行爲,那麼這個函數就是一個參數。這純粹是一個簡單的結果;一個函數必須始終爲相同的輸入返回相同的內容。

當你掌握更先進的技術時,有多種方法可以「隱藏」額外的參數,從而不必明確寫入/傳遞它們;這基本上就是State monad所做的事情,而單向思考IO monad的方法是它正在做類似的事情。但是,當你對函數式編程還不熟悉時,習慣這種思維模式可能會更有幫助;你將信息傳遞給你通過它的參數調用的函數,並通過它的參數接收信息。你不能訴諸於把信息留在某個外部地方(例如計數器的價值),你知道你所調用的功能會尋找它(甚至修改它)的必要技巧。