Parsec: применительно к Monads

Я только начинаю с Parsec (имея небольшой опыт работы в Haskell), и я немного запутался в использовании монад или аппликаций. Общее ощущение, которое я испытал после чтения "Real World Haskell", "Write You a Haskell", и вопрос здесь в том, что аппликативы предпочтительнее, но на самом деле я понятия не имею.

Итак, мои вопросы:

  • Какой подход предпочтительнее?
  • Можно ли смешивать монады и аппликаторы (использовать их, когда они более полезны, чем другие).
  • Если последний ответ "да", должен ли я это сделать?

Ответы

Ответ 1

В общем, начните с того, что вам больше всего подходит. Затем рассмотрим следующее.

Рекомендуется использовать Applicative (или даже Functor), когда это возможно. В общем, для компилятора, такого как GHC, проще оптимизировать эти экземпляры, поскольку они могут быть проще, чем Monad. Я думаю, что общий совет сообщества post AMP должен был сделать как можно более общие ваши ограничения. Я бы рекомендовал использовать расширение GHC ApplicativeDo, так как вы можете единообразно использовать нотацию do, но только получая ограничение Applicative, когда это это все, что нужно.

Поскольку тип парсера ParsecT является экземпляром как Applicative, так и Monad, вы можете смешивать и сопоставлять два. Бывают ситуации, когда это становится более читаемым - все это зависит от ситуации.

Также рассмотрите возможность использования megaparsec. megaparsec является более активно поддерживаемым, как правило, более чистой более поздней версией parsec.

ИЗМЕНИТЬ

Две вещи, которые, перечитывая мой ответ и комментарии, я действительно не очень хорошо прояснял:

  • Основное преимущество использования Applicative заключается в том, что для многих типов он допускает гораздо более эффективные реализации (например, (<*>) более эффективен, чем ap).

  • Если вы просто хотите написать что-то вроде (+) <$> parseNumber <*> parseNumber, нет необходимости вбрасываться в ApplicativeDo - это было бы просто более подробным. Я бы использовал ApplicativeDo только тогда, когда вы начинаете называть очень длинные или вложенные аппликативные выражения.

Ответ 2

Возможно, стоит обратить внимание на ключевую смысловую разницу между Applicative и Monad, чтобы определить, когда каждый из них подходит. Сравнить типы:

(<*>) :: m (s -> t) -> m s -> m t
(>>=) :: m s -> (s -> m t) -> m t

Чтобы развернуть <*>, вы выбираете два вычисления, одну из функций, другую аргумента, затем их значения объединяются приложением. Для развертывания >>= вы выбираете одно вычисление, и вы объясняете, как вы будете использовать его результирующие значения для выбора следующего вычисления. Это разница между "пакетным режимом" и "интерактивной" операцией.

Когда дело доходит до разбора, Applicative (расширенный с ошибкой и выбором, чтобы дать Alternative), захватывает контекстно-свободные аспекты вашей грамматики. Вам понадобится дополнительная мощность, которую Monad дает вам, только если вам нужно проверить дерево разбора из части вашего ввода, чтобы решить, какую грамматику вы должны использовать для другой части вашего ввода. Например, вы можете прочитать дескриптор формата, а затем вход в этот формат. Минимизация использования дополнительной мощности монадов говорит о том, какие значения зависимостей необходимы.

Переходя от синтаксического анализа до parallelism, эта идея использования >>= только для существенной зависимости от стоимости покупает вам ясность о возможностях распространения нагрузки. Когда два вычисления объединены с <*>, не нужно ждать другого. Аппликативно-когда-вы-может-но-монадически-когда-вы-должна формула для скорости. Точкой ApplicativeDo является автоматизация анализа зависимостей кода, который был написан в монадическом стиле и, таким образом, случайно чрезмерно эквализирован.

Ваш вопрос также относится к стилю кодирования, о котором мнения могут быть разными. Но позвольте мне рассказать вам историю. Я пришел в Haskell из Standard ML, где я привык писать программы в прямом стиле, даже если они делали непослушные вещи, такие как исключение броска или мутацию ссылок. Что я делал в ML? Работа над реализацией теории ультрачистого типа (которая не может быть названа по юридическим причинам). Когда я работал над теорией этого типа, я не мог писать программы прямого стиля, которые использовали исключения, но я приготовил аппликативные комбинаторы как способ максимально приблизить к прямому стилю.

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

Чтобы сказать, те же самые вещи, которые делают функциональный код более компактным и читаемым, чем императивный код, также делают аппликативный стиль более компактным и читаемым, чем do-notation. Я ценю, что ApplicativeDo - отличный способ сделать более прикладные (а в некоторых случаях и более быстрыми) программы, написанные в монадическом стиле, что у вас нет времени на рефакторинг. Но в противном случае я бы утверждал, что аппликативный-когда-вы-может-но-монадический-когда-ты-должен также лучший способ увидеть, что происходит.

Ответ 3

Следуя за @pigworker (я слишком новичок здесь, чтобы прокомментировать), стоит отметить join $ fM <*> ... <*> ... <*> ... как шаблон. Он привязывает вас к семейству "bind1, bind2, bind3..." так же, как <$> и <*> получит вам "fmap1, fmap2, fmap3".

Как стилистическая вещь, когда вы привыкли к комбинаторам, можно использовать do так же, как вы бы использовали let: как выделить, когда вы хотите что-то назвать. Я, как правило, предпочитаю чаще упоминать вещи в парсерах, потому что это, вероятно, соответствует именам в spec!