Объединение монад в Haskell

Я пытаюсь написать Пасьянс Паука в качестве учебного упражнения Haskell.

Моя функция main будет вызывать функцию playGame один раз для каждой игры (используя mapM), передавая номер игры и случайный генератор (StdGen). Функция playGame должна возвращать монаду Control.Monad.State и монаду IO, содержащую String, показывающую таблицу игр и Bool, указывающую, была ли игра выиграна или потеряна.

Как объединить монаду State с монадой IO для возвращаемого значения? Каким должно быть объявление типа для `playGame?

playGame :: Int -> StdGen a -> State IO (String, Bool)

Правильно ли State IO (String, Bool)? Если нет, что это должно быть?

В main, я планирую использовать

do
  -- get the number of games from the command line (already written)
  results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames]

Правильно ли это вызов playGame?

Ответ 1

Вы хотите StateT s IO (String, Bool), где StateT предоставляется как Control.Monad.State (из пакета mtl), так и Control.Monad.Trans.State (из пакета transformers).

Это общее явление называется монадным трансформатором, и вы можете прочитать отличное введение в них в Monad Transformers, шаг за шагом.

Существует два подхода к их определению. Один из них находится в пакете transformers, который использует класс MonadTrans для их реализации. Второй подход найден в классе mtl и использует отдельный тип-класс для каждой монады.

Преимущество подхода transformers заключается в использовании одного типа-класса для реализации всего (найдено здесь):

class MonadTrans t where
    lift :: Monad m => m a -> t m a

lift имеет два приятных свойства, которые должен удовлетворять любой экземпляр MonadTrans:

(lift .) return = return
(lift .) f >=> (lift .) g = (lift .) (f >=> g)

Это законы функтора в маскировке, где (lift .) = fmap, return = id и (>=>) = (.).

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

class Monad m => MonadError e m | m -> e where
    throwError :: e -> m a
    catchError :: m a -> (e -> m a) -> m a

Этот класс также содержит законы:

m `catchError` throwError = m
(throwError e) `catchError` f = f e
(m `catchError` f) `catchError` g = m `catchError` (\e -> f e `catchError` g)

Это просто законы монады в маскировке, где throwError = return и catchError = (>>=) (а законы монады - это маскировка категорий, где return = id и (>=>) = (.)).

Для вашей конкретной проблемы способ записи вашей программы будет таким же:

do
  -- get the number of games from the command line (already written)
  results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames]

... но когда вы напишете свою функцию playGame, она будет выглядеть так:

-- transformers approach :: (Num s) => StateT s IO ()
do x <- get
   y <- lift $ someIOAction
   put $ x + y

-- mtl approach :: (Num s, MonadState s m, MonadIO m) => m ()
do x <- get
   y <- liftIO $ someIOAction
   put $ x + y

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

Ответ 2

State является монадой, а IO является монадой. То, что вы пытаетесь написать с нуля, называется "монадным трансформатором", а стандартная библиотека Haskell уже определяет, что вам нужно.

Посмотрите на трансформатор состояния монады StateT: у него есть параметр, который является внутренней монадой, которую вы хотите обернуть в State.

Каждый трансформатор монады реализует кучу классных таблиц, так что для каждого экземпляра трансформатор имеет дело с ним каждый раз, когда он может (например, трансформатор состояния способен обрабатывать только связанные с ним функции) или он распространяет вызов на внутренняя монада таким образом, что когда вы можете складывать все необходимые вам трансформаторы и иметь единый интерфейс для доступа к функциям всех из них. Это своего рода цепочка ответственности, если вы хотите посмотреть на нее таким образом.

Если вы посмотрите hackage или выполните быстрый поиск при переполнении стека или Google, вы найдете множество примеров использования StateT.

edit. Еще одно интересное сообщение: Разъяснение трансформаторов Монады.

Ответ 3

Хорошо, кое-что прояснилось здесь:

  • Вы не можете "вернуть монаду". Монада - это своего рода тип, а не своего рода ценность (точнее, монада - это конструктор типа, который имеет экземпляр класса Monad). Я знаю, что это звучит педантично, но это может помочь вам разобраться в различии между вещами и типами вещей в вашей голове, что важно.
  • Обратите внимание: вы ничего не можете сделать с помощью State, что невозможно без него, поэтому, если вы смущены тем, как его использовать, тогда не чувствуйте, что вам нужно! Часто я просто пишу обычный тип функции, я хочу, а затем, если я заметил, что у меня много функций в форме Thing -> (Thing, a), я бы пошел "ага", это немного похоже на State, возможно, это можно упростить до State Thing a". Понимание и работа с равными функциями - важный первый шаг на пути к использованию State или его друзей.
  • IO, с другой стороны, это единственное, что может сделать свою работу. Но имя playGame не сразу spring у меня как имя того, что нужно делать I/O. В частности, если вам нужны только (псевдо) случайные числа, вы можете сделать это без IO. Как отметил комментатор, MonadRandom отлично подходит для создания этого простого, но снова вы можете просто использовать чистые функции, которые берут и возвращают StdGen от System.Random. Вам просто нужно убедиться, что вы правильно написали свое семя (StdGen) (это автоматически было основано на том, почему State был изобретен, вы можете обнаружить, что понимаете это лучше после того, как пытаетесь выполнить программу без него!)
  • Наконец, вы не совсем используете getStdGen правильно. Это действие IO, поэтому вам нужно связать его результат с <- в do -блоке перед его использованием (технически вам не нужно, у вас много вариантов, но это почти наверняка, что вы хотеть сделать). Что-то вроде этого:

    do
      seed <- getStdGen
      results <- mapM (\game -> playGame game seed) [1..numberOfGames]
    

    Здесь playGame :: Integer -> StdGen -> IO (String, Bool). Обратите внимание, однако, что вы передаете одно и то же случайное семя каждому playGame, которое может быть или не быть тем, что вы хотите. Если это не так, вы можете вернуть семя из каждого playGame, когда вы закончите с ним, перейти к следующему или повторно получить новые семена с помощью newStdGen (что вы могли бы сделать изнутри playGame, если вы решите сохранить его в IO).

Во всяком случае, это не был очень структурированный ответ, за который я извиняюсь, но надеюсь, что это даст вам о чем подумать.