簡體   English   中英

如何在Haskell中正確處理IO返回值

[英]How to handle IO return values properly in Haskell

嗨,我有一個非常新手的問題,可以說我想在必須回答問題時創建一個游戲,我寫了這個

data Question = Question { answer::String, text::String }
data Player = Player { name::String, points::String }

answerQuestion ::  Question -> Player -> Player 
answerQuestion question player
    | isCorrect question playerAnswer = Player (name player) (points player + 1)
    | otherwise = player
    where
      playerAnswer = do
          putStrLn text(question)
          getLine

isCorrect ::  Question -> String -> Bool 
isCorrect question try = try == answer(question)

現在playerAnswer具有IO String類型,所以我必須在do塊內調用isCorrect嗎? 還有另一種方法可以將IO String解析為String嗎?

在第一種情況下,我感覺失去了函數式編程的所有好處,因為我最終將在do塊中編寫整個代碼,以便訪問String

注意:這篇文章是用識字的Haskell撰寫的。 您可以將其另存為Game.lhs並在GHCi中嘗試。

現在playerAnswer具有IO String類型,所以我必須在do塊內調用isCorrect嗎?

是的,如果您堅持那條路線。

還有另一種方法可以將IO String解析為String嗎?

沒有什么不是不安全的。 IO String可以為您提供一個String ,但是使用該String任何方式都必須保留在IO

首先,我感覺失去了函數式編程的所有好處,因為我最終會在do塊中編寫整個代碼,以便訪問String值。

如果您不及早進行測量,可能會發生這種情況。 但是,讓我們從上至下的方法進行處理。 首先,讓我們介紹一些類型別名,以便我們清楚地將String作為答案還是名稱:

> type Text   = String
> type Answer = String
> type Name   = String
> type Points = Int    -- points are usually integers

您的原始類型保持不變:

> data Question = Question { answer :: Answer
>                          , text   :: Text } deriving Show
> data Player   = Player { name   :: Name
>                        , points :: Points } deriving Show

現在,讓我們考慮一下游戲的一個回合。 您想問玩家一個問題,得到他的答案,如果他是對的,請添加一些要點:

> gameTurn :: Question -> Player -> IO Player
> gameTurn q p = do
>    askQuestion q
>    a <- getAnswer
>    increasePointsIfCorrect q p a

這足以讓您的游戲充滿一個回合。 讓我們用生命填充這些功能。 askQuestionsgetAnswer改變了世界:它們在終端上打印一些內容並要求用戶輸入。 他們必須在某個時候進入IO

> askQuestion :: Question -> IO ()
> askQuestion q = putStrLn (text q)

> getAnswer :: IO String
> getAnswer = getLine

之前,我們實際上定義increasePointsIfCorrect ,讓我們想想不使用一個版本的IO ,再次,在一個稍微更抽象的方式:

> increasePointsIfCorrect' :: Question -> Player -> Answer -> Player
> increasePointsIfCorrect' q p a =
>    if isCorrect q a
>       then increasePoints p
>       else p

順便說一句,如果您仔細觀察,您會注意到“ increasePointsIfCorrect'實際上是一個回合。 畢竟,它會檢查答案並增加分數。 說起:

> increasePoints :: Player -> Player
> increasePoints (Player n p) = Player n (p + 1)

> isCorrect :: Question -> Answer -> Bool
> isCorrect q a = answer q == a

現在,我們定義了幾個不使用IO函數。 所缺少的只是increasePointsIfCorrect

> increasePointsIfCorrect :: Question -> Player -> Answer -> IO Player
> increasePointsIfCorrect q p a = return (increasePointsIfCorrect' q p a)

您現在可以通過一個簡單的簡短游戲進行檢查:

> theQuestion = Question { text   = "What is your favourite programming language?"
>                        , answer = "Haskell (soon)"}
> thePlayer   = Player { name   = "Alberto Pellizzon"
>                      , points = 306 }
>
> main :: IO ()
> main = gameTurn theQuestion thePlayer >>= print

還有其他方法可以解決此問題,但是我認為這對於初學者來說是較容易的方法之一。

無論哪種方式,最棒的是我們現在可以在不使用IO情況下測試所有邏輯。 例如:

prop_increasesPointsOnCorrectAnswer q p =
   increasePointsIfCorrect' q p (answer q) === increasePoints p

prop_doesnChangePointsOnWrongAnswer q p a = a /= answer q ==>
   increasePointsIfCorrect' q p a === p

ghci> quickCheck prop_increasesPointsOnCorrectAnswer 
OK. Passed 100 tests.
ghci> quickCheck prop_doesnChangePointsOnWrongAnswer 
OK. Passed 100 tests.

但是,完全實施那些測試超出了這個問題的范圍。

練習題

  • 告訴玩家他的答案是否正確。
  • 添加playGame :: [Question] -> Player -> IO () ,它接着又問幾個問題,並告訴玩家最終得分。
  • 向播放器詢問他/她的名字,並將其存儲在初始播放器中。
  • (對初學者來說非常辛苦)嘗試找到一種方法,使您可以自動玩游戲(例如,進行測試),也可以“對抗”人類。 提示:查找“特定於域的語言”。

作為替代,你可以促進 answerQuestion到一個動作,以及:

answerQuestion ::  Question -> Player -> IO Player 
answerQuestion question player =
    answer <- playerAnswer
    if isCorrect question answer
    then return $ Player (name player) (points player + 1)
    else return player
    where
      playerAnswer = do
          putStrLn $ text question
          getLine

所以是的,您可以說在這種情況下,您應該從do塊內部調用isCorrect

還有另一種方法可以將IO String解析為String嗎?

不,沒有將IO String轉換為String安全方法。 這是 IO型!

現在playerAnswer具有IO String類型,所以我必須在do塊內調用isCorrect嗎? [...]首先,我感覺失去了函數式編程的所有好處,因為我最終會在do塊中編寫整個代碼,以便訪問String

乍一看就是這樣,但事實並非如此。 訣竅是我們使用適配器函數來連接純凈的世界和有效的世界。 例如,假設您有:

  • 問題question :: IO Question
  • 答案answer :: IO String

然后您要在QuestionString上調用isCorrect :: Question -> String -> Bool 一種方法是使用liftA2函數:

import Control.Applicative (liftA2)

example :: IO Bool
example = liftA2 isCorrect question answer
  where question :: IO Question
        question = _
        answer :: IO String
        answer = _

liftA2具有以下通用類型:

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c

在此示例中,我們將其用於:

  • f := IO
  • a := Question
  • b := String
  • c := Bool

因此,liftA2所做的是使純函數適應於副作用類型。 這就是Haskell編程通常的工作方式:

  1. 您嘗試將大多數代碼編寫為純函數
  2. 您可以使用liftA2類的輔助功能來使其適應不純效果。

要掌握這一點,需要一些學習和實踐。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM