Что может делать монада Reader, чего не могут делать аппликативные функции? - PullRequest
7 голосов
/ 22 апреля 2019

Прочитав http://learnyouahaskell.com/functors-applicative-functors-and-monoids#applicative-functors, я могу привести пример использования функций в качестве аппликативных функторов:

Скажем, res - это функция из 4 аргументов и fa, fb, fc, fd - это все функции, которые принимают один аргумент.Тогда, если я не ошибаюсь, это применимое выражение:

f <$> fa <*> fb <*> fc <*> fd $ x

Означает то же самое, что и это необычное выражение:

f (fa x) (fb x) (fc x) (fd x)

Тьфу.Мне потребовалось немало времени, чтобы понять, почему это так, но - с помощью листа бумаги с моими заметками - я смогу доказать это.

Тогда я прочитал http://learnyouahaskell.com/for-a-few-monads-more#reader.И мы снова вернемся к этому, на этот раз в монадическом синтаксисе:

do
    a <- fa
    b <- fb
    c <- fc
    d <- fd
    return (f a b c d)

Хотя мне понадобился еще один лист заметок А4, чтобы доказать это, я теперь довольно уверен, что это опять, означает то же самое:

    f (fa x) (fb x) (fc x) (fd x)

Я в замешательстве.Зачем?Какая польза от этого?

Или, если быть более точным: мне кажется, это просто дублирует функциональность функций как аппликативных, но с более подробным синтаксисом.

Итак, не могли бы выприведите пример, может ли монада Reader выполнять функции аппликативных функций?

На самом деле, я также хотел бы спросить, в чем смысл использования любого из этих двух: аппликативных функций ИЛИ монады Reader - потому чтоХотя возможность применения одного и того же аргумента к четырем функциям (fa, fb, fc, fd) без повторения этого аргумента в четыре раза снижает повторяемость, я не уверен, оправдывает ли это незначительное улучшение этоуровень сложности;я думаю, что мне не хватает чего-то выдающегося;но это достойно отдельного вопроса

Ответы [ 2 ]

8 голосов
/ 22 апреля 2019

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

do
    a <- fa
    if a == 3 
      then  return (f a 1 1 1)
      else  do
          b <- fb
          c <- fc
          d <- fd
          return (f a b c d)

В оригиналеВыражение do, это правда, что вы не делаете ничего, чего не мог бы сделать экземпляр Applicative, и фактически компилятор может это определить.Если вы используете расширение ApplicativeDo, то

do
    a <- fa
    b <- fb
    c <- fc
    d <- fd
    return (f a b c d)

действительно будет десагарно к f <$> fa <*> fb <*> fc <*> fd вместо fa >>= \a -> fb >>= \b -> fc >>= \c -> fd >>= \d -> return (f a b c d).


Это также относится и к другим типам,например

  • Maybe:

    f <$> (Just 3) <*> (Just 5)
      == Just (f 3 5)
      == do
          x <- Just 3
          y <- Just 5
          return (f 3 5)
    
  • []:

    f <$> [1,2] <*> [3,4]
      == [f 1 3, f 1 4, f 2 3, f 2 4]
      == do
          x <- [1,2]
          y <- [3,4]
          return (f x y)
    
4 голосов
/ 23 апреля 2019

Прежде чем перейти к вашему основному вопросу о Reader, я начну с нескольких замечаний по поводу аппликативного-монадного в целом. Пока это аппликативное выражение стиля ...

g <$> fa <*> fb

... действительно эквивалентно этому блоку do ...

do
    x <- fa
    y <- fb
    return (g x y)

... переключение с Applicative на Monad позволяет принимать решения о том, какие вычисления выполнять на основе результатов других вычислений или, другими словами, иметь эффекты, которые зависят от предыдущих результатов (см. Также ответ Чепнера ):

do
    x <- fa
    y <- if x >= 0 then fb else fc
    return (g x y)

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


Примечание в скобках: когда дело доходит до аппликативного-монадного, Reader является особым случаем, в котором Applicative и Monad экземпляры оказываются эквивалентными . Для функции-функтора (то есть ((->) r), то есть Reader r без оболочки нового типа) у нас есть m >>= f = flip f <*> m. Это означает, что если взять второй блок do, который я написал выше (или аналогичный в ответе Чепнера и т. Д.) и , предположим, что используется монада Reader, мы можем перевести его в аппликативный стиль.


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

Начнем с того, что Haskellers часто настороженно относятся к голому функтору функций, ((->) r), и вполне понятно: это может легко привести к излишне загадочному коду по сравнению с «необычным выражением [s]», в котором функции применяется напрямую. Тем не менее, в некоторых избранных случаях это может быть удобно для использования. Для крошечного примера рассмотрим эти две функции из Data.Char:

isUpper :: Char -> Bool
isDigit :: Char -> Bool

Теперь предположим, что мы хотим написать функцию, которая проверяет, является ли символ буквой верхнего регистра или цифрой ASCII. Простая вещь, которую нужно сделать, это что-то вроде:

\c -> isUpper c && isDigit c

Используя аппликативный стиль, мы можем сразу написать его в терминах двух функций - или, я склонен сказать, двух свойств - без необходимости отмечать, где возможный аргумент идет:

(&&) <$> isUpper <*> isDigit

С таким крошечным примером, как этот, писать его таким образом не имеет большого значения, и в значительной степени по вкусу - мне это очень нравится; другие не могут этого вынести. Дело, однако, в том, что иногда мы не особенно обеспокоены тем, что определенное значение является функцией, потому что мы думаем о нем как о чем-то другом - в данном случае как о свойстве - и о том, что оно в конечном счете функция может показаться нам простой деталью реализации.

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


В любом случае, ваши подозрения относительно полезности Reader несколько оправданы, по крайней мере, одним способом.Оказывается, что сам по себе Reader, функтор functions-but-wrapped-in-a-fancy-newtype, на самом деле не так часто используется в дикой природе.Что является чрезвычайно распространенным, так это монадические стеки, которые включают в себя функциональность Reader, обычно с помощью ReaderT и / или класса MonadReader,Обсуждение длинных монадных трансформаторов было бы слишком большим отступлением для места этого ответа, поэтому я просто отмечу, что вы можете работать, например, с ReaderT r IO так же, как с Reader r, за исключением того, что вы также можете ускользнутьв IO вычисления по пути.Нет ничего необычного в том, что какой-то вариант ReaderT сверх IO является основным типом внешнего слоя приложения Haskell.


В заключение заметим, что вам может быть интересно посмотретьчто join из Control.Monad делает для функции-функтора, а затем выясняет, почему это имеет смысл.(Решение можно найти в в этом Q & A .)

...