11

我正在尝试编写一个蜘蛛纸牌播放器作为 Haskell 学习练习。

我的函数将为每个游戏main调用一次函数(使用),传入游戏编号和随机生成器()。该函数应该返回一个monad 和一个 IO monad,其中包含一个显示游戏画面和一个指示游戏是赢还是输的信息。playGamemapMStdGenplayGameControl.Monad.StateStringBool

如何将Statemonad 与IOmonad 结合起来作为返回值?`playGame 的类型声明应该是什么?

playGame :: Int -> StdGen a -> State IO (String, Bool)

State IO (String, Bool)正确的吗?如果不是,应该是什么?

main,我打算使用

do
  -- get the number of games from the command line (already written)
  results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames]

这是正确的调用方式playGame吗?

4

3 回答 3

17

你想要的是StateT s IO (String, Bool),哪里StateTControl.Monad.State(来自mtl包)和Control.Monad.Trans.State(来自transformers包)提供。

这种普遍现象称为 monad 转换器,您可以在Monad Transformers, Step by Step中阅读对它们的精彩介绍。

有两种定义它们的方法。其中之一位于transformers使用MonadTrans类来实现它们的包中。第二种方法可以在mtl类中找到,并为每个 monad 使用单独的类型类。

transformers方法的优点是使用单个类型类来实现所有内容(在此处找到):

class MonadTrans t where
    lift :: Monad m => m a -> t m a

lift有两个很好的属性,任何实例都MonadTrans必须满足:

(lift .) return = return
(lift .) f >=> (lift .) g = (lift .) (f >=> g)

这些是变相的函子定律,(lift .) = fmapwherereturn = id(>=>) = (.)

类型类方法也有它的mtl好处,有些事情只能使用mtl类型类来彻底解决,但缺点是每个mtl类型类都有自己的一套法则,在为它实现实例时你必须记住. 例如,MonadError类型类(在此处找到)定义为:

class Monad m => MonadError e m | m -> e where
    throwError :: e -> m a
    catchError :: m a -> (e -> m a) -> m a

这个类也有法律:

m `catchError` throwError = m
(throwError e) `catchError` f = f e
(m `catchError` f) `catchError` g = m `catchError` (\e -> f e `catchError` g)

这些只是伪装的 monad 法则,wherethrowError = returncatchError = (>>=)(而 monad 法则是伪装的范畴法则,wherereturn = id(>=>) = (.))。

对于您的具体问题,您编写程序的方式将是相同的:

do
  -- get the number of games from the command line (already written)
  results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames]

...但是当您编写playGame函数时,它看起来像:

-- transformers approach :: (Num s) => StateT s IO ()
do x <- get
   y <- lift $ someIOAction
   put $ x + y

-- mtl approach :: (Num s, MonadState s m, MonadIO m) => m ()
do x <- get
   y <- liftIO $ someIOAction
   put $ x + y

当您开始堆叠多个单子变压器时,这些方法之间的差异会变得更加明显,但我认为现在这是一个好的开始。

于 2012-06-06T16:56:16.727 回答
8

State是一个单子,并且IO是一个单子。您尝试从头开始编写的内容称为“monad 转换器”,Haskell 标准库已经定义了您需要的内容。

看看 state monad 转换StateT器:它有一个参数,它是你想要包装到State.

每个 monad 转换器实现了一堆类型类,这样对于每个实例,转换器每次都可以处理它(例如状态转换器只能直接处理与状态相关的函数),或者它将调用传播到内部 monad以这样一种方式,当您可以堆叠所有您想要的转换器时,并有一个统一的界面来访问所有转换器的功能。这是一种责任链,如果你想这样看的话。

如果您查看hackage,或者快速搜索堆栈溢出或 google,您会发现很多StateT.

编辑:另一个有趣的读物是Monad Transformers Explained

于 2012-06-06T12:13:02.403 回答
2

好的,这里需要澄清一些事情:

  • 你不能“返回一个单子”。monad 是一种类型,而不是一种值(准确地说,monad 是具有类实例的类型构造函数Monad)。我知道这听起来很迂腐,但它可能会帮助你在头脑中理清事物和事物类型之间的区别,这很重要。
  • 请注意,如果没有它,您将无法做任何事情State,所以如果您对如何使用它感到困惑,那就不要觉得您需要!通常,我只写我想要的普通函数类型,然后如果我注意到我有很多形状像Thing -> (Thing, a)我的函数,我会说“啊哈,这看起来有点像State,也许这可以简化为State Thing a”。State理解和使用普通函数是使用或使用它的朋友的重要的第一步。
  • IO另一方面,它是唯一可以完成其工作的东西。但是这个名字playGame并没有立即出现在我身上,因为它是需要做 I/O 的东西的名字。特别是,如果您需要(伪)随机数,则无需IO. 正如评论者所指出的,MonadRandom非常适合简化此操作,但您也可以再次使用纯函数来获取并返回StdGenfrom System.Random。你只需要确保你StdGen正确地穿线你的种子(the )(自动这样做基本上State是发明的原因;你可能会发现在没有它的情况下尝试编程后你会更好地理解它!)
  • Finally, you're not quite using getStdGen correctly. It's an IO action, so you need to bind its result with <- in a do-block before using it (technically, you don't need to, you have lots of options, but that's almost certainly what you want to do). Something like this:

    do
      seed <- getStdGen
      results <- mapM (\game -> playGame game seed) [1..numberOfGames]
    

    Here playGame :: Integer -> StdGen -> IO (String, Bool). Notice, however, that you're passing the same random seed to each playGame, which may or may not be what you want. If it isn't, well, you could return the seed from each playGame when you were done with it, to pass to the next one, or repeatedly get new seeds with newStdGen (which you could do from inside playGame, if you decide to keep it in IO).

Anyway, this hasn't been a very structured answer, for which I apologise, but I hope it gives you something to think about.

于 2012-06-06T21:01:14.510 回答