Как вы используете Control.Applicative для написания более чистого Haskell?

В недавнем ответе на вопрос стиля я написал

main = untilM (isCorrect 42) (read `liftM` getLine)

и

isCorrect num guess =
  case compare num guess of
    EQ -> putStrLn "You Win!" >> return True
    ...

Martijn предлагаемые альтернативы:

main = untilM (isCorrect 42) (read <$> getLine)

EQ -> True <$ putStrLn "You Win!"

Какие общие шаблоны в коде Haskell можно сделать более ясными, используя абстракции из Control.Applicative? Какие полезные правила нужно иметь в виду для эффективного использования Control.Applicative?

Ответ 1

В ответ на ваш вопрос многое можно сказать, так как вы спросили, я предложу это "эмпирическое правило".

Если вы используете do -notation, а ваши сгенерированные значения [1] не используются в выражениях, которые вы упорядочиваете [2], тогда этот код может преобразовать в аппликативный стиль. Аналогичным образом, если вы используете одно или несколько сгенерированных значений в выраженном секвенсе, то вы должны использовать Monad и Applicative недостаточно для достижения того же кода.

Например, рассмотрим следующий код:

do a <- e1
   b <- e2
   c <- e3
   return (f a b c)

Мы видим, что ни в одном из выражений справа от <- не появляется какое-либо из сгенерированных значений (a, b, c). Поэтому мы можем превратить его в использование аппликативного кода. Вот одно возможное преобразование:

f <$> e1 <*> e2 <*> e3

а другой:

liftA3 f e1 e2 e3

С другой стороны, возьмите этот фрагмент кода, например:

do a <- e1
   b <- e2 a
   c <- e3
   return (f b c)

Этот код не может использовать Applicative [3], потому что сгенерированное значение a используется позже в выражении в понимании. Для получения результата нужно использовать Monad - попытайтесь включить его в Applicative, чтобы понять, почему.

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

[1] Те, которые отображаются слева от <-.

[2] Выражения, которые отображаются справа от <-.

[3], строго говоря, его части могли бы, разлагая e2 a.

Ответ 2

В принципе, монады также являются аппликативными функторами [1]. Итак, всякий раз, когда вы используете liftM, liftM2 и т.д., Вы можете объединить вычисления вместе с помощью <*>. В некотором смысле вы можете придумать аппликативные функторы как аналогичные функциям. Чистую функцию f можно снять, выполняя f <$> x <*> y <*> z.

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

import Control.Applicative

ifte condition trueClause falseClause = do
  c <- condition
  if c then trueClause else falseClause

x = ifte (return True) (putStrLn "True") (putStrLn "False")

ifte' condition trueClause falseClause = 
  if condition then trueClause else falseClause

y = ifte' <$> (pure True) <*> (putStrLn "True") <*> (putStrLn "False")

x выводит только True, тогда как y последовательно выводит True и False.

[1] The Typeclassopedia. Очень рекомендуется.

[2] http://www.soi.city.ac.uk/~ross/papers/Applicative.html. Хотя это академическая статья, ее не трудно следовать.

[3] http://learnyouahaskell.com/functors-applicative-functors-and-monoids#applicative-functors. Объясняет сделку очень хорошо.

[4] http://book.realworldhaskell.org/read/using-parsec.html#id652399. Показывает, как монадическая библиотека Parsec также может быть использована аппликативным способом.