【问题标题】:How to handle IO return values properly in Haskell如何在 Haskell 中正确处理 IO 返回值
【发布时间】:2016-01-13 17:10:26
【问题描述】:

您好,我有一个非常菜鸟的问题,假设我想在您必须回答问题时创建一个游戏,我写了这个

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

【问题讨论】:

  • 当您意识到answerQuestion 将无法编译时,您的麻烦就开始了(因为playerAnswerIO String 操作) - 这就是为什么您应该拆分功能并将答案添加为附加@ 987654334@ 参数到您的answerQuestion IMO
  • 不错的方法,那么我如何针对IO Stringanswer(question) String 的答案执行字符串匹配
  • 顺便说一句。 points :: String - 这真的是你想要的吗?
  • 我建议您阅读"Inside My World (Ode to Functor and Monad)"。我认为它很好地解决了这个话题。

标签: haskell io functional-programming


【解决方案1】:

注意:这篇文章是用 literate Haskell 写的。您可以将其保存为 Game.lhs 并在您的 GHCi 中尝试。

现在 playerAnswer 的类型为 IO String,所以我必须在 do 块内调用 isCorrect 吗?

是的,如果你坚持那个课程。

还有其他方法可以将IO String 解析为String

没有不安全的。 IO String 是给你一个String 的东西,但无论使用String 都必须留在IO

在第一次的情况下,我感觉失去了函数式编程的所有好处,因为我最终会在 do 块中编写我的整个代码以访问字符串值。

如果您不及早进行测量,就会发生这种情况。但是,让我们从自上而下的方法来解决这个问题。首先,让我们引入一些类型别名,以便清楚我们将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] -&gt; Player -&gt; IO (),它会一个接一个地问几个问题,告诉玩家最终得分。
  • 询问玩家他/她的名字并将其存储在初始玩家中。
  • (对于初学者来说很难)尝试找到一种方法,以便您可以自动玩游戏(例如用于测试)或“对抗”人类。提示:寻找“领域特定语言”。

【讨论】:

  • 这几天我要写一个正常大小的答案。
  • 或者只是编写一些脚本来提取所有答案,将它们串在一起并发布“Haskell 编程宇宙的终极参考和所有其他东西”;)
  • @Carsten 当然。 “一开始,我的账号是被创建的,这让很多人非常愤怒,被广泛认为是一个坏举动。”。
  • 大家好,谢谢大家的回答,我需要一些时间来阅读和理解所有内容,我注意到的一件事是所有 3 个答案都有很大不同,也许这意味着它不是简单的话题:D,当我完全理解它时,我会接受这个作为正确答案(当然因为它是最长的)
【解决方案2】:

作为替代方案,您也可以将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

【讨论】:

  • 这是一个可行的解决方案,我不喜欢的是 1 - 以命令式的方式编写整个函数(if / then / else) 2 - 我希望 askQuestion 返回一个 Player 3 - 如果我必须从另一个函数调用answerQuestion,我还必须在 do 块内调用它,依此类推
  • 好吧,如果您愿意,您可以将其重写为case,但它与您的相同......一旦您输入do,它看起来总是非常必要 - 顺便说一句:您的问题是什么那么呢?
  • 所以这里真正的问题是我无法使用 IO 字符串对字符串执行匹配,为了做到这一点,我知道我必须将 IO 字符串“转换”为一个字符串,这只能在 do 块内完成,但这样做会使我的代码只在里面,对我来说这似乎是一个 hack,我是从一个非常初学者的角度说话,你实际上已经回答了我的问题,所以我是打算接受它,只是想更深入地挖掘这个问题
  • 不完全是——你不转换一些值——你应该把IO String看作一个你可以执行action(更多如果你愿意,不止一次)并取回String(如果你执行它不止一次,那可能会有所不同) - 你可以将这些 actions 加入/绑定/组合在一起(这是单子部分) 并且这样做的一种方法是 do-block 语法糖 - 但最终您只会在运行时执行 main :: IO () action 时执行这些操作
  • @chi 是的,谢谢 - 在这种情况下,我只复制并粘贴了代码
【解决方案3】:

还有其他方法可以将IO String 解析为String

不,没有将IO String 转换为String 的安全方法。这是IO 类型的point

现在 playerAnswer 的类型为 IO String,所以我必须在 do 块内调用 isCorrect 吗? [...] 首先,我感觉失去了函数式编程的所有好处,因为我最终会在 do 块中编写整个代码,以便访问 String

一开始是这样的,但事实并非如此。诀窍是我们使用 适配器 函数来连接纯粹和有效的世界。例如,假设您有:

  • 一个问题question :: IO Question
  • 回复answer :: IO String

您想在QuestionString 上致电isCorrect :: Question -&gt; String -&gt; 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 所做的是 adapt 一个纯函数来处理副作用类型。这就是 Haskell 编程的一般工作方式:

  1. 您尝试将大部分代码编写为纯函数
  2. 您可以使用像 liftA2 这样的辅助函数来调整它们以处理不纯的效果。

需要一些学习和练习才能掌握这一点。

【讨论】:

  • 抱歉,这是一个不好的例子。 Question 那时已经知道了。 liftM (isCorrect question) getAnswerfmap (isCorrect question) getAnswer 会更好,除非你的 question :: IO Question 是有点假的 putQuestion q &gt;&gt; return q。此外,您在本地绑定中使用了已经存在的函数answer,从而影响了他的问题中提供的answer :: Question -&gt; String OP。您的答案的整体心态/范式成立,但该示例可能会让语言初学者感到困惑。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-01
  • 1970-01-01
  • 2011-12-05
  • 1970-01-01
  • 2017-01-26
相关资源
最近更新 更多