Ответ 1
В этом вопросе есть две части:
- Как смешивать независимую от монады шахматную структуру с инкрементным входом, специфичным для монады.
- Как указать часть, специфичную для монады, во время выполнения
Вы решаете прежнюю проблему с помощью генератора, который является частным случаем свободного монадного трансформатора:
import Control.Monad.Trans.Free -- from the "free" package
type GeneratorT a m r = FreeT ((,) a) m r
-- or: type Generator a = FreeT ((,) a)
yield :: (Monad m) => a -> GeneratorT a m ()
yield a = liftF (a, ())
GeneratorT a
является монадным трансформатором (потому что FreeT f
является монадным трансформатором, если f
является Functor
). Это означает, что мы можем смешивать yield
(который является полиморфным в базовой монаде), с моноданными вызовами, используя lift
для вызова базовой монады.
Я определяю некоторые поддельные шахматные движения только для этого примера:
data ChessMove = EnPassant | Check | CheckMate deriving (Read, Show)
Теперь я определяю генератор шахматных движений IO
:
import Control.Monad
import Control.Monad.Trans.Class
ioPlayer :: GeneratorT ChessMove IO r
ioPlayer = forever $ do
lift $ putStrLn "Enter a move:"
move <- lift readLn
yield move
Это было легко! Мы можем развернуть результат одним движением за раз, используя runFreeT
, который потребует, чтобы игрок ввел ход, когда вы привязываете результат:
runIOPlayer :: GeneratorT ChessMove IO r -> IO r
runIOPlayer p = do
x <- runFreeT p -- This is when it requests input from the player
case x of
Pure r -> return r
Free (move, p') -> do
putStrLn "Player entered:"
print move
runIOPlayer p'
Протестируйте его:
>>> runIOPlayer ioPlayer
Enter a move:
EnPassant
Player entered:
EnPassant
Enter a move:
Check
Player entered:
Check
...
Мы можем сделать то же самое, используя монаду Identity
в качестве базовой монады:
import Data.Functor.Identity
type Free f r = FreeT f Identity r
runFree :: (Functor f) => Free f r -> FreeF f r (Free f r)
runFree = runIdentity . runFreeT
Примечание. Пакеты transformers-free
определяют их уже (Отказ от ответственности: я написал его, и Эдвард объединил его функциональность, был объединен в пакет free
. Я держу его только для учебных целей, и вы должны использовать free
, если это возможно).
С теми, кто находится в руке, мы можем определить чистые генераторы движения шахмат:
type Generator a r = Free ((,) a) r
-- or type Generator a = Free ((,) a)
purePlayer :: Generator ChessMove ()
purePlayer = do
yield Check
yield CheckMate
purePlayerToList :: Generator ChessMove r -> [ChessMove]
purePlayerToList p = case (runFree p) of
Pure _ -> []
Free (move, p') -> move:purePlayerToList p'
purePlayerToIO :: Generator ChessMove r -> IO r
purePlayerToIO p = case (runFree p) of
Pure r -> return r
Free (move, p') -> do
putStrLn "Player entered: "
print move
purePlayerToIO p'
Протестируйте его:
>>> purePlayerToList purePlayer
[Check, CheckMate]
Теперь, чтобы ответить на ваш следующий вопрос, каким образом выбрать базовую монаду во время выполнения. Это легко:
main = do
putStrLn "Pick a monad!"
whichMonad <- getLine
case whichMonad of
"IO" -> runIOPlayer ioPlayer
"Pure" -> purePlayerToIO purePlayer
"Purer!" -> print $ purePlayerToList purePlayer
Теперь, здесь все становится сложным. Вам действительно нужны два игрока, и вы хотите указать базовую монаду для них обоих самостоятельно. Чтобы сделать это, вам нужен способ получить один ход от каждого игрока в качестве действия в монаде IO
и сохранить оставшуюся часть списка перемещения плеера позже:
step
:: GeneratorT ChessMove m r
-> IO (Either r (ChessMove, GeneratorT ChessMove m r))
Часть Either r
находится в случае, если у игрока заканчиваются ходы (т.е. доходит до конца их монады), и в этом случае r
является возвращаемым значением блока.
Эта функция специфична для каждой монады m
, поэтому мы можем ввести класс it:
class Step m where
step :: GeneratorT ChessMove m r
-> IO (Either r (ChessMove, GeneratorT ChessMove m r))
Определите некоторые экземпляры:
instance Step IO where
step p = do
x <- runFreeT p
case x of
Pure r -> return $ Left r
Free (move, p') -> return $ Right (move, p')
instance Step Identity where
step p = case (runFree p) of
Pure r -> return $ Left r
Free (move, p') -> return $ Right (move, p')
Теперь мы можем написать наш игровой цикл, чтобы он выглядел так:
gameLoop
:: (Step m1, Step m2)
=> GeneratorT ChessMove m1 a
-> GeneratorT ChessMove m2 b
-> IO ()
gameLoop p1 p2 = do
e1 <- step p1
e2 <- step p2
case (e1, e2) of
(Left r1, _) -> <handle running out of moves>
(_, Left r2) -> <handle running out of moves>
(Right (move1, p2'), Right (move2, p2')) -> do
<do something with move1 and move2>
gameLoop p1' p2'
И наша функция main
просто выбирает, какие игроки использовать:
main = do
p1 <- getStrLn
p2 <- getStrLn
case (p1, p2) of
("IO", "Pure") -> gameLoop ioPlayer purePlayer
("IO", "IO" ) -> gameLoop ioPlayer ioPlayer
...
Надеюсь, это поможет. Вероятно, это было чуть-чуть убить (и вы, вероятно, можете использовать что-то более простое, чем генераторы), но я хотел дать общий обзор крутых иконов Haskell, которые вы можете попробовать при разработке своей игры. Я печатал все, кроме последних нескольких блоков кода, так как я не мог придумать разумную логику игры для проверки на лету.
Вы можете узнать больше о свободных монадах и свободных монадных трансформаторах если этих примеров недостаточно.