Haskell - Как не вводить один и тот же контекст снова и снова? - PullRequest
22 голосов
/ 15 июля 2011

Я недавно запустил небольшой хобби-проект , где я пытаюсь реализовать карточную игру Skat с трюками (для 3 игроков).Чтобы позволить игрокам разных типов (например, ИИ, сетевым и локальным) играть вместе, я разработал интерфейс , используя класс типов 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)

Как мне избежать написанияэтот большой контекст снова и снова?

Ответы [ 3 ]

11 голосов
/ 15 июля 2011
  • Единственное, что вы можете наблюдать из класса игрока, это функция типа

    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 из моего рабочего пакета . Этот дизайн позволяет вам записывать каждого игрока (человека, ИИ, повтор, ...) в специализированную монаду, а затем вводить все в общий интерпретатор.

6 голосов
/ 15 июля 2011

Обновление: Когда я попытался реализовать 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аналогичным образом.

На самом деле, в подобных случаях я чувствую, что классы типов используются слишком часто.Может быть, это новичок из Хаскелла, говорящий во мне, но я бы написал это вместо этого:

data Player m = Player { playerMessage :: Message answer -> m (Either Error answer, Player m) }
2 голосов
/ 16 июля 2011

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

class (Storable.Storable (X, y), Y y) => Signal y

Теперь запись '(Signal y) => ...' подразумевает все другие классы типов и предотвращает попадание деталей реализации, таких как Storable, в каждый API. Тем не менее, вы должны объявить экземпляры для сигнала. Это легко, потому что у него нет методов, но, вероятно, в основном подходит для случаев, когда у вас мало экземпляров, но много функций.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...