Когда следует использовать аппликативные над монад? - PullRequest
4 голосов
/ 03 марта 2020

Я использую Scala на работе и для более глубокого понимания функционального программирования. Я выбрал Программирование Грэма Хаттона в Haskell (обожаю: :)

В главе о монадах я получил свой первый взгляд в концепцию аппликативных функторов (AF)

На моем (ограниченном) уровне профессионализма - Scala мне никогда не приходилось использовать AF, и я всегда писал код с использованием монад. Я пытаюсь понять понимание «когда использовать AF» и, следовательно, вопрос. Правильно ли это понимание:

Если все ваши вычисления независимы и распараллеливаемы (т. Е. Результат одного не определяет результат другого), то ваши потребности будут лучше обслуживаться AF, если выход должен быть передан в чистую функцию без эффектов. Однако, если у вас есть даже одиночные зависимые AF, это не поможет, и вы будете вынуждены использовать монады. Если вывод должен быть передан в функцию с эффектами (например, возвращая Maybe), вам понадобятся монады.

Например, если у вас есть «монадный» код, такой как:

val result = for {
 x <- callServiceX(...)
 y <- callServiceY(...) //not dependent on X 
} yield f(x,y)

Лучше сделать что-то вроде (синтаксис псевдо-AF для scala, где |@| подобен разделителю между параллельными / асинхронными вызовами).

val result = (callServiceX(...) |@| callServiceY(...)).f(_,_)

  • Если f == pure and callService* are independent AF помогут вам лучше
  • Если f имеет эффекты, т. Е. f(x,y): Option[Response] вам понадобятся монады
  • Если callServiceX(...), y <- callServiceY(...), callServiceZ(y), то есть в цепочке есть даже одна зависимость, используйте монады.

Правильно ли мое понимание? Я знаю, что у AFs / Monads есть намного больше, и я считаю, что понимаю преимущества одного над другим (по большей части). Что я хочу знать, так это процесс принятия решения о том, какой из них использовать в определенном контексте.

Ответы [ 2 ]

5 голосов
/ 03 марта 2020

На самом деле решение не принимается: всегда используйте интерфейс Applicative, если только он не слишком слаб. 1

Это существенное напряжение силы абстракции: больше вычислений может быть выраженным с Монадой; Вычисления, выраженные с помощью Applicative, можно использовать по-разному.

Похоже, вы в основном правы относительно условий, в которых вам нужно использовать Monad. Я не уверен насчет этого:

  • Если f имеет эффекты, например f(x,y) : Option[Response], вам понадобятся монады.

Не обязательно. О каком функторе идет речь? Ничто не мешает вам создать F[Option[X]], если F является аппликативным. Но так же, как и раньше, вы не сможете принимать дальнейшие решения в F в зависимости от того, успешно или нет Option - все «дерево вызовов» действий F должно быть доступно без вычисления каких-либо значений.


1 Читаемость в стороне, то есть. Код Monadi c, вероятно, будет более доступным для людей с традиционным происхождением из-за его императивного вида.

2 голосов
/ 03 марта 2020

Я думаю, вам нужно быть немного осторожнее с такими терминами, как «независимый» или «распараллеливаемый» или «зависимость». Например, в монаде IO рассмотрим вычисление:

foo :: IO (String, String)
foo = do
    line1 <- getLine
    line2 <- getLine
    return (line1, line2)

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

foo = (,) <$> getLine <*> getLine

В качестве более реалистичного c примера, анализатор monadi c для выражения 3 + 4 может выглядеть следующим образом:

expr :: Parser Expr
expr = do
    x <- factor
    op <- operator
    y <- factor
    return $ x `op` y

Три действия здесь взаимозависимы. Успех первого парсера factor определяет, будут ли запущены другие или нет, и его поведение (например, сколько входного потока он поглощает) явно влияет на результаты других парсеров. Было бы неразумно считать эти действия «параллельными» или «независимыми». Тем не менее, это аппликативное действие:

expr = factor <**> operator <*> factor

Или рассмотрим это State Int действие:

bar :: Int -> Int -> State Int Int
bar x y = do
    put (x + y)
    z <- gets (2*)
    return z

Очевидно, что результат действия gets (*2) зависит от вычислений, выполненных в действие put (x + y). Но, опять же, это аппликативное действие:

bar x y = put (x + y) *> gets (2*)

Я не уверен, что существует действительно простой способ думать об этом интуитивно. Грубо говоря, если вы думаете о монади c действие / вычисление m a как о «монади c структура» m, а также «структура значения» a, то аппликативные выражения сохраняют монади c и ценностные структуры раздельные. Например, аппликативное вычисление:

λ> [(1+),(10+)] <*> [3,4,5]
[4,5,6,13,14,15]

имеет структуру monadi c (список), в которой мы всегда имеем:

[f,g] <*> [a,b,c] = [f a, f b, f c, g a, g b, g c]

независимо от фактических значений. Следовательно, результирующая длина списка является произведением длины обоих «входных» списков, первый элемент результата включает в себя первые элементы «входных» списков и т. Д. c. Он также имеет структуру значений, благодаря которой значение 4 в результате явно зависит от значения (1+) и значения 3 на входах.

A monadi c вычисления, с другой стороны, допускают зависимость структуры monadi c от структуры значений, поэтому, например, в:

quux :: [Int]
quux = do
  n <- [1,2,3]
  drop n [10..15]

мы не можем записать вычисление структурного списка независимо от значений , Структура списка (например, длина окончательного списка) зависит от данных уровня значений (фактические значения в списке [1,2,3]). Это тот тип зависимости, который требует монады вместо аппликативного.

...