2013-09-18 3 views
9

Попытка узнать Haskell Я реализую игру Quarto в Haskell. Я уже реализовал игру на Python как упражнение в курсе, который я взял в прошлом году, когда идея заключалась в том, чтобы реализовать игру вместе с тремя разными игроками «AI», случайным игроком, новичком и минимаксным игроком. Логика кусков и логика платы довольно прямолинейны для реализации, но я пришел к тому моменту, когда мне нужно реализовать игроков, и мне интересно, как лучше всего создавать игроков, чтобы игровая логика не нуждалась в знании о конкретных игроках, но все же позволяя им использовать разные монады.Дизайн Haskell, охватывающий несколько монад

Проблема в том, что каждому игроку нужны разные монады, случайный игрок должен работать либо в государственной монаде, либо в монаде RandomState. Новичкам, вероятно, также понадобится какая-то форма состояния, и минимаксный игрок может использовать любое состояние или быть чистым (это сделало бы его намного медленнее и немного сложнее реализовать, но это можно сделать), кроме того, мне бы хотелось, чтобы «человек », который должен будет работать в монаде IO, чтобы получить доступ от человека. Одно простое решение - просто поместить все в монаду IO, но я чувствую, что это несколько усложняет индивидуальный дизайн и заставляет дизайн каждого игрока иметь дело больше, чем нужно.

Моя первая мысль была бы что-то вроде:

class QuartoPlayer where 
    place :: (Monad m) => QuartoPiece -> QuartoBoard -> m (Int, Int) 
    nextPiece :: (Monad m) => QuartoBoard -> [QuartoPiece] -> m QuartoPiece 

Я не знаю, если это будет работать, так как я не пробовал, но я хотел бы некоторый входной сигнал, если я движется в правильном направлении и если дизайн имеет смысл в Haskell.

+1

использование монада трансформаторов 'Control.Monad.Trans. *' Объединить несколько Монады – viorior

+1

Да, монада трансформаторов является ответом. С помощью монадных трансформаторов вы можете создать единую объединенную монаду из нескольких монадов, и комбинация обладает всеми возможностями отдельных монадов. – kqr

+0

Будет ли это означать, что всем «игрокам» придется делиться одними и теми же монадами? – Nordmoen

ответ

9

Есть две части того, что здесь происходит. Во-первых, как объединить несколько разных типов монады для запуска в одно и то же время - и, как было указано, это можно сделать с помощью монадных трансформаторов, а второе позволяет каждому из ваших типов игроков получать доступ только к монадам, в которых они нуждаются. Ответ на эту последнюю проблему - это классы типов.

Итак, во-первых, рассмотрим монадные трансформаторы. Монадный трансформатор похож на монаду с дополнительной «внутренней» монадой. Если эта внутренняя монада является монадой Идентичности (которая в основном ничего не делает), то поведение подобно обычной монаде. По этой причине монады обычно реализуются как трансформаторы и завернуты в 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 
+0

Прежде всего, очень хорошее введение в монадные трансформаторы, я пробовал немного прочитать об этом после предыдущих комментариев, и ваше резюме прекрасно суммирует то, что я читаю, но гораздо более кратким.Один из вопросов, который у меня есть, заключается в том, могу ли я с вашей системой управлять всеми игроками взаимозаменяемо? С точки зрения игровой логики я хотел бы, чтобы все реализованные игроки казались похожими. Что-то длинное в строках определения интерфейса в Java, имеющем некоторый специальный код, инициализирует необходимых игроков и имеет логику игры только для работы с интерфейсом. – Nordmoen

+0

Вы должны иметь возможность создать экземпляр данных, например data 'Player = IOPayer | StatePlayer | и т. д., а затем есть метод в основном модуле, который включает этот тип, а затем получает правильный эффект от файлов других игроков. Он может это сделать, потому что главный модуль знает обо всем стеке и поэтому будет набирать проверку. Таким образом, этот основной модуль может обеспечить унифицированный интерфейс для игрока, в то время как каждый отдельный модуль игрока знает только то, что ему нужно, через свой интерфейс класса. (Помните, что я называю это основным, но это не обязательно должно быть * the * main) –

+0

Хм, это все равно означает, что логика игры (ваша основная) должна знать о конкретных игроках. Не удалось бы построить что-то, что сделало бы логику игры совершенно незаметной для конкретного типа. Используя ваше предложение, можно ли построить еще один слой поверх StatePlayer и IOPlayer, который может заставить логику не знать, является ли ее StatePlayer или IOPlayer? – Nordmoen

Смежные вопросы