Прежде чем перейти к вашему основному вопросу о 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 .)