Haskell设计包含几个monad

Nor*_*oen 9 haskell design-patterns

试图学习Haskell我正在Haskell中实现一个Quarto游戏.我已经在Python中实现了这个游戏,这是我去年开设的一个课程,其中的想法是与三个不同的"AI"玩家,一个随机玩家,一个新手玩家和一个minimax玩家一起实现游戏.片段逻辑和电路板逻辑很容易实现,但我已经到了需要实现播放器的地步,我想知道如何最好地设计播放器,以便游戏逻辑不需要知道任何东西关于特定的玩​​家,但仍然允许他们使用不同的monad.

问题是每个玩家需要不同的monad,随机玩家需要在State monad或RandomState monad中工作.新手玩家可能也需要某种形式的状态,而minimax玩家可能会使用状态或纯粹(这会使它变得更慢,实现起来有点棘手,但可以做到)另外我想要一个"人类" "玩家需要在IO monad中工作以获取人类的输入.一个简单的解决方案是将所有内容都放在IO monad中,但我觉得这有点使得个人设计变得更加困难,并迫使每个玩家的设计必须处理超出他们应有的程度.

我最初的想法是这样的:

class QuartoPlayer where
    place :: (Monad m) => QuartoPiece -> QuartoBoard -> m (Int, Int)
    nextPiece :: (Monad m) => QuartoBoard -> [QuartoPiece] -> m QuartoPiece
Run Code Online (Sandbox Code Playgroud)

我不知道这是否会起作用,因为我没有尝试过,但如果我朝着正确的方向前进并且如果设计在Haskell中有意义,我想要一些输入.

Vic*_*ith 9

这里有两个部分.首先是如何将几种不同类型的monad组合在一起运行 - 正如已经指出的那样,可以使用monad变换器完成 - 第二种是允许每种玩家类型只访问他们需要的monad.后一个问题的答案是类型类.

首先,让我们来看看monad变形金刚.单子变换器就像一个带有额外"内部"单子的单子.如果这个内部monad是Identity monad(它基本上什么都不做),那么行为就像常规monad一样.出于这个原因,monad通常被实现为变换器并包含在Identity中以导出普通的monad.monad的变换器版本通常将T附加到类型的末尾,因此状态monad变换器称为StateT.类型的唯一区别在于内部monad,State s avs Monad m => StateT s m a.因此,举个例子,一个带有附加的整数列表的IO monad可以有类型StateT [Int] IO.

要正确使用变压器还需要两点.首先是为了影响内部monad,你使用lift函数(任何现有的monad变换器将定义).电梯的每次调用都会让你从一堆变压器中移出.liftIO是IO monad位于堆栈底部的特殊快捷方式.(并且它不能像其他任何地方一样,因为没有你期望的IO变换器.)所以我们可以创建一个从状态部分弹出我们的int列表的头部并使用IO部分打印它的函数:

popAndPrint :: StateT [Int] IO Int
popAndPrint = do
    (x:xs) <- get
    liftIO $ print x
    put xs
    return x
Run Code Online (Sandbox Code Playgroud)

第二点是你需要运行函数的变换器版本,一个用于堆栈中的每个monad变换器.因此,在这种情况下,我们需要证明GHCi的效果

> runStateT popAndPrint  [1,2,3]
1
(1,[2,3])
Run Code Online (Sandbox Code Playgroud)

如果我们将它包装在Error monad中,我们需要调用runErrorT $ runStateT popAndPrint [1,2,3]等等.

这是monad变形金刚的快速介绍,网上有更多可用的.

但是,对于你来说,这只是故事的一半,因为理想情况下你想要分离不同玩家类型可以使用的monad.变压器的方法似乎给你一切,你真的不想让所有的玩家只需要它就可以访问IO.那怎么办?

每种不同类型的播放器都需要访问变压器堆栈的不同部分.因此,为每个玩家制作一个类型类,只暴露玩家需要的东西.每个人都可以使用不同的文件.例如:

-- IOPlayer.hs
class IOPlayerMonad a where
    getMove :: IO Move

doSomethingWithIOPLayer :: IOPlayerMonad m => m ()
doSomethingWithIOPLayer = ...

-- StatePlayer.hs
class StatePlayerMonad s a where
    get :: Monad m => StateT s m s
    put :: Monad m => s -> StateT s m ()

doSomethingWithStatePlayer :: StatePlayerMonad s m => m ()
doSomethingWithStatePlayer = ...

-- main.hs
instance IOPlayerMonad (StateT [Int] IO) where
   getMove = liftIO getMoveIO

instance StatePlayerMonad s (StateT [Int] IO) where
   get' = get
   put' = put
Run Code Online (Sandbox Code Playgroud)

这使您可以控制应用程序的哪个部分可以访问整个状态中的内容,并且此控件都发生在一个文件中.每个单独的部分都可以定义其接口和逻辑,与主状态的特定实现完全不同.

PS,你可能需要在顶部:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}

import Control.Monad.Trans.State
import Control.Monad.IO.Class
import Control.Monad
Run Code Online (Sandbox Code Playgroud)

-

UPDATE

关于你是否可以这样做并且仍然具有所有玩家的共同界面存在一些混淆.我坚持认为你可以.Haskell不是面向对象的,所以我们需要自己做一些调度管道,但结果同样强大,你可以更好地控制细节,仍然可以实现完全封装.为了更好地展示这一点,我已经包含了一个完整的玩具示例

在这里,我们看到Play该类为许多不同的播放器类型提供了单个接口,每个类型的逻辑都在自己的文件中,并且只看到变换器堆栈上的特定接口.此界面在Play模块中控制,游戏逻辑只需使用此界面.

添加新播放器包括为它们创建一个新文件,设计它们需要的接口,将其添加到AppMonad,并使用Player类型中的新标签进行连接.

请注意,所有玩家都可以通过AppMonadClass类访问该板,该类可以扩展为包含任何所需的通用界面元素.

-- Common.hs --
data Board = Board
data Move = Move

data Player = IOPlayer | StackPlayer Int

class Monad m => AppMonadClass m where
    board :: m Board

class Monad m => Play m where
    play :: Player -> m Move

-- IOPlayer.hs --
import Common

class AppMonadClass m => IOPLayerMonad m where
    doIO :: IO a -> m a

play1 :: IOPLayerMonad m => m Move
play1 = do
    b <- board
    move <- doIO (return Move)
    return move


-- StackPlayer.hs --
import Common

class AppMonadClass m => StackPlayerMonad s m | m -> s where
    pop  :: Monad m => m s
    peak :: Monad m => m s
    push :: Monad m => s -> m ()

play2 :: (StackPlayerMonad Int m) => Int -> m Move
play2 x = do
    b <- board
    x <- peak
    push x
    return Move


-- Play.hs --
import Common
import IOPLayer
import StackPlayer

type AppMonad = StateT [Int] (StateT Board IO)

instance AppMonadClass AppMonad where
    board = return Board

instance StackPlayerMonad Int AppMonad where
    pop  = do (x:xs) <- get; put xs; return x;
    peak = do (x:xs) <- get; return x;
    push x = do (xs) <- get; put (x:xs);

instance IOPLayerMonad AppMonad where
    doIO = liftIO

instance Play AppMonad where
    play IOPlayer = play1
    play (StackPlayer x) = play2 x


-- GameLogic.hs
import Play

updateBoard :: Move -> Board -> Board
updateBoard _ = id

players :: [Player]
players = [IOPlayer, StackPlayer 4]

oneTurn :: Player -> AppMonad ()
oneTurn p = do
    move <- play p
    oldBoard <- lift get
    newBoard <- return $ updateBoard move oldBoard
    lift $ put newBoard
    liftIO $ print newBoard

oneRound :: AppMonad [()]
oneRound = forM players $ (\player -> oneTurn player)

loop :: AppMonad ()
loop = forever oneRound

main = evalStateT (evalStateT loop [1,2,3]) Board
Run Code Online (Sandbox Code Playgroud)