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

Я пытаюсь написать игрока Spider Solitaire в качестве учебного упражнения на 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?


person Ralph    schedule 06.06.2012    source источник
comment
Вам также может понравиться RandT из пакета MonadRandom.   -  person Daniel Wagner    schedule 06.06.2012


Ответы (3)


Вам нужен StateT s IO (String, Bool), где StateT предоставляется как Control.Monad.State (из пакета mtl), так и Control.Monad.Trans.State (из пакета 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

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

person Gabriel Gonzalez    schedule 06.06.2012
comment
Очень хороший и полный ответ. Спасибо. - person Ralph; 06.06.2012
comment
StateT IO (String, Bool) неверно - вид несоответствия. Это StateT s m a с s типом состояния, m монадой и a типом результата. - person Ben Millwood; 07.06.2012
comment
Кроме того, подход mtl и подход transformers на самом деле не являются разными способами выполнения одной и той же задачи — MonadError достигает цели, отличной от MonadTrans. И законы не являются замаскированными Monad законами — они очень похожи, но типы вовлеченных вещей и их значение различны. Я имею в виду, что в основном это единичный закон и ассоциативный закон, но в отношении совершенно разных операций. - person Ben Millwood; 07.06.2012
comment
@benmachine Спасибо, исправлено. Кроме того, законы MonadError на самом деле являются просто версиями законов монад с ограничениями по типу, потому что e скрыты в m через функциональную зависимость, поэтому вы не можете получить доступ ко всей их силе, но вы все равно можете использовать эти законы, чтобы проверить, что вы правильно реализуете экземпляр. - person Gabriel Gonzalez; 07.06.2012
comment
Я не уверен, что ваш новый тип имеет то, что ему нужно — для чего нужен s? Что касается материала MonadError, я вроде как понимаю, что вы имеете в виду, но я не уверен, что аналогия настолько фундаментальна, как вы выразились — я думаю, что тот факт, что тип ошибки определяется монадой, а не пользователем, является важным отличием. И я поддерживаю свой комментарий о том, что подходы mtl и transformers в значительной степени решают разные проблемы — обратите внимание, что в настоящее время mtl импортирует и использует интенсивно transformers, поэтому странно предположить, что они конкурируют. - person Ben Millwood; 07.06.2012
comment
Для MonadError начните с конкретного случая Either e a. Теперь измените аргументы типа на type EitherR a e = Either e a. Это формирует монаду успеха, где (>>=) :: EitherR a e1 -> (e1 -> EitherR a e2) -> EitherR a e2. Теперь введите ограничить это до: (>>=) :: EitherR a e -> (e -> EitherR a e) -> EitherR a e. Затем вернитесь к Either: (>>=) :: Either e a -> (e -> Either e a) -> Either e a). Наконец, замените Either e на m | m -> e: (>>=) :: m a -> (e -> m a) -> m a. Это то, что я имею в виду. - person Gabriel Gonzalez; 07.06.2012
comment
Я прочитал статью, которую вы упомянули, «Трансформеры монад, шаг за шагом». Очень хорошая бумага. - person Ralph; 07.06.2012
comment
Хорошо, я отзову свой отрицательный голос. Я все еще не думаю, что это отличный ответ, но я полагаю, что это не совсем неправильно. - person Ben Millwood; 09.06.2012

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

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

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

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

изменить: Еще одно интересное чтение — Объяснение трансформеров монад.

person Riccardo T.    schedule 06.06.2012
comment
Мне нравится, как человек несколько раз заново открывает для себя колесо, изучая Haskell... На самом деле довольно приятно найти решение проблемы и обнаружить, что это общий шаблон проектирования. - person fuz; 06.06.2012
comment
@FUZxxl: Да, это действительно так :) - person Riccardo T.; 06.06.2012

Хорошо, здесь нужно прояснить несколько вещей:

  • Вы не можете «вернуть монаду». Монада — это вид типа, а не типа значения (точнее, монада — это конструктор типа, имеющий экземпляр класса Monad). Я знаю, это звучит педантично, но это может помочь вам разобраться в различиях между вещами и типами вещей в вашей голове, что очень важно.
  • Обратите внимание, что вы не можете делать с State ничего, что было бы невозможно без него, поэтому, если вы не понимаете, как его использовать, не считайте, что вам это нужно! Часто я просто пишу обычный тип функции, который мне нужен, а затем, если я замечаю, что у меня много функций в форме Thing -> (Thing, a), я говорю: «Ага, это немного похоже на State, может быть, это можно упростить до State Thing a». Понимание и работа с простыми функциями — важный первый шаг на пути к использованию State или его друзей.
  • IO, с другой стороны, это единственное, что может выполнять свою работу. Но имя playGame не сразу приходит мне на ум как имя чего-то, что должно выполнять ввод-вывод. В частности, если вам нужны только (псевдо)случайные числа, вы можете сделать это без 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).

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

person Ben Millwood    schedule 06.06.2012
comment
Монада — это разновидность типа, не разновидность значения. Разве не будет справедливо сказать, что на самом деле это конструктор типа? - person Asherah; 07.06.2012
comment
Да, монада — это своего рода конструктор типов, но я не хотел звучать слишком технично — я просто хотел подчеркнуть, что монады принадлежат миру типов. Я отредактирую его, чтобы быть более точным. - person Ben Millwood; 07.06.2012