Есть две части того, что здесь происходит. Во-первых, как объединить несколько разных типов монады для запуска в одно и то же время - и, как было указано, это можно сделать с помощью монадных трансформаторов, а второе позволяет каждому из ваших типов игроков получать доступ только к монадам, в которых они нуждаются. Ответ на эту последнюю проблему - это классы типов.
Итак, во-первых, рассмотрим монадные трансформаторы. Монадный трансформатор похож на монаду с дополнительной «внутренней» монадой. Если эта внутренняя монада является монадой Идентичности (которая в основном ничего не делает), то поведение подобно обычной монаде. По этой причине монады обычно реализуются как трансформаторы и завернуты в Identity для экспорта обычной монады. Трансформаторные версии монад обычно добавляют T в конец типа, поэтому государственный монадный трансформатор называется StateT. Единственная разница в типах заключается в добавлении внутренней монады, State s a
против Monad m => StateT s m a
. Итак, для примера, монада IO с присоединенным списком целых чисел как состояние может иметь тип StateT [Int] IO
.
Для правильного использования трансформаторов необходимы еще две точки. Во-первых, чтобы создать внутреннюю монаду, вы используете функцию lift
(которая будет определена любым существующим монадным трансформатором). Каждый вызов лифта перемещает вас по стопке трансформаторов. liftIO
- это специальный ярлык, когда монада IO находится в нижней части стека. (И это не может быть нигде, поскольку нет трансформатора IO, как вы ожидали бы.) Таким образом, мы могли бы создать функцию, которая выталкивает головку нашего списка int из государственной части и печатает ее с использованием части ввода-вывода:
popAndPrint :: StateT [Int] IO Int
popAndPrint = do
(x:xs) <- get
liftIO $ print x
put xs
return x
Второй момент заключается в том, что вам нужны трансформаторные версии работающих функций, по одному для каждого монадного трансформатора в стеке. Таким образом, в этом случае, чтобы продемонстрировать эффект GHCi нам нужно
> runStateT popAndPrint [1,2,3]
1
(1,[2,3])
Если мы обернули это в монаду Error, мы должны были бы назвать runErrorT $ runStateT popAndPrint [1,2,3]
и так далее.
Это быстрое введение огня в монадные трансформаторы, есть еще много доступных в Интернете.
Тем не менее, для вас это только половина истории, так как в идеале вы хотите разделить между собой мозаики, которые могут использовать ваши разные типы игроков. Подход трансформатора, кажется, дает вам все, и вы не хотите, чтобы все игроки имели доступ к 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
Это дает вам контроль над тем, что часть приложения могут получить доступ, что из общего состояния, и этот контроль все происходит в одном файле. Каждая отдельная часть получает определение своего интерфейса и логики, отличную от конкретной реализации основного состояния.
PS, вам может понадобиться их на вершине:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
import Control.Monad.Trans.State
import Control.Monad.IO.Class
import Control.Monad
-
UPDATE
Там была некоторая путаница, о ли вы можете сделать это таким образом, и до сих пор имеют общий интерфейс для всех игроков. Я утверждаю, что вы можете. Haskell не ориентирован на объекты, поэтому нам нужно немного поработать с рассылкой, но результаты будут такими же мощными, и вы получите лучший контроль над деталями и сможете достичь полной инкапсуляции. Чтобы лучше показать это, я включил полностью рабочий игрушечный пример.
Здесь мы видим, что класс Play
обеспечивает единый интерфейс для нескольких разных типов игроков, каждый из которых имеет свою логику в своем собственном файле и видит только определенный интерфейс в стеке трансформатора. Этот интерфейс контролируется в модуле Play, и игровая логика должна использовать только этот интерфейс.
Добавление нового игрока включает в себя создание нового файла для них, разработку требуемого интерфейса, добавление его в AppMonad и подключение его к новому тегу в типе проигрывателя.
Обратите внимание, что все игроки получают доступ к доске через класс 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
использование монада трансформаторов 'Control.Monad.Trans. *' Объединить несколько Монады – viorior
Да, монада трансформаторов является ответом. С помощью монадных трансформаторов вы можете создать единую объединенную монаду из нескольких монадов, и комбинация обладает всеми возможностями отдельных монадов. – kqr
Будет ли это означать, что всем «игрокам» придется делиться одними и теми же монадами? – Nordmoen