Haskell - Как избежать повторения одного и того же контекста снова и снова?
Недавно я начал небольшой , где я пытаюсь реализовать трюковую карточную игру Skat (для 3 игроков). Чтобы иметь возможность играть с разными игроками (например, AI, network и local), я разработал interface с помощью typeclass Player
:
class Monad m => Player p m | p -> m where
playerMessage :: Message answer -> p -> m (Either Error answer,p)
Я использую StateT
для обертывания этих трех игроков:
type PST a b c m x = StateT (Players a b c) m x
Но теперь я должен написать большую кучу контекста в каждой сигнатуре типа:
dealCards :: (Player a m, Player b m, Player c m, RandomGen g)
=> g -> PST a b c m (SomeOtherState,g)
Как я могу избежать записи этого большого контекста снова и снова?
Ответы
Ответ 1
-
Единственное, что вы можете наблюдать из класса игрока, - это функция типа
playerMessage' :: Message answer -> m (Either Error answer, p)
Следовательно, вы можете полностью исключить класс и использовать обычный тип данных
data Player m = Player { playerMessage'
:: Message answer -> m (Either Error answer, Player m) }
Это в основном мой предыдущий ответ.
-
Альтернативным решением является перемещение контекста в тип данных с помощью GADT.
data PST a b c m x where
PST :: (Player a m, Player b m, Player c m)
=> StateT (Players a b c) m x -> PST a b c m x
Другими словами, ограничения становятся частью типа данных.
-
Лучшим решением является, на мой взгляд, отказаться от всего этого и перепроектировать его по строкам Пример TicTacToe из моего операционного пакета . Эта конструкция позволяет вам писать каждого игрока (человека, AI, повтор,...) в специализированной монаде, а затем вводить все в общий интерпретатор.
Ответ 2
Update:
Когда я попытался реализовать dealCards
, я понял, что мое решение снижает безопасность типа, заставляя игроков взаимозаменяемыми. Таким образом, вы можете легко использовать одного игрока вместо другого, что может быть нежелательным.
Если вы не против использования ExistentialQuantification
, я думаю, что он может (и должен?) использоваться здесь. В конце концов, функция dealCards
не должна заботиться или знать о a
, b
и c
, правильно?
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
import Control.Monad.State
import System.Random
type Message answer = answer
type Error = String
class Monad m => Player p m | p -> m where
playerMessage :: Message answer -> p -> m (Either Error answer,p)
data SomePlayer m = forall p. Player p m => SomePlayer p
data Players m = Players (SomePlayer m) (SomePlayer m) (SomePlayer m)
type PST m x = StateT (Players m) m x
dealCards :: (RandomGen g, Monad m) => g -> PST m x
dealCards = undefined
Я думаю, что должно быть возможно исключить ограничение Monad
аналогичным образом.
Собственно, в таких случаях я чувствую, что классы типов чрезмерно используются. Может быть, что новичок Haskell говорит во мне, но я бы написал вместо этого:
data Player m = Player { playerMessage :: Message answer -> m (Either Error answer, Player m) }
Ответ 3
Ясно, что лучший ответ - иметь дизайн, который не нуждается во всех этих параметрах типа в первую очередь. Однако, если вы действительно не можете избавиться от них и ответить на поставленный вопрос, вот трюк, который я сыграл:
class (Storable.Storable (X, y), Y y) => Signal y
Теперь запись '(Signal y) = > ...' будет означать все остальные типы и не позволяет деталям реализации, таким как Storable, попасть в каждый API. Однако вы должны объявлять экземпляры для Signal. Это легко, потому что у него нет методов, но он, вероятно, в основном подходит для случаев, когда у вас мало примеров, но много функций.