Подтвердить что ты не робот

Понимание `ap` в бессточной функции в Haskell

Я могу понять основы точечных функций в Haskell:

addOne x = 1 + x

Как мы видим x по обе стороны уравнения, мы его упрощаем:

addOne = (+ 1)

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

Позвольте мне взять в качестве основного примера функцию average, написанную как:

average xs = realToFrac (sum xs) / genericLength xs

Может показаться невозможным упростить xs, но http://pointfree.io/ выходит с:

average = ap ((/) . realToFrac . sum) genericLength

Это работает.

Насколько я понимаю, это означает, что average совпадает с вызовом ap для двух функций, состав (/) . realToFrac . sum и genericLength

К сожалению, функция ap не имеет никакого отношения ко мне, docs http://hackage.haskell.org/package/base-4.8.1.0/docs/Control-Monad.html#v:ap:

ap :: Monad m => m (a -> b) -> m a -> m b

In many situations, the liftM operations can be replaced by uses of ap,
which promotes function application.

      return f `ap` x1 `ap` ... `ap` xn

is equivalent to

      liftMn f x1 x2 ... xn

Но запись:

let average = liftM2 ((/) . realToFrac . sum) genericLength

не работает, (дает сообщение об ошибке очень длинного типа, спрашивает, и я его включу), поэтому я не понимаю, что говорят документы.


Как работает выражение ap ((/) . realToFrac . sum) genericLength? Не могли бы вы объяснить ap проще, чем документы?

4b9b3361

Ответ 1

Когда монада m равна (->) a, как в вашем случае, вы можете определить ap следующим образом:

ap f g = \x -> f x (g x)

Мы видим, что это действительно "работает" в вашем беспроблемном примере.

average = ap ((/) . realToFrac . sum) genericLength
average = \x -> ((/) . realToFrac . sum) x (genericLength x)
average = \x -> (/) (realToFrac (sum x)) (genericLength x)
average = \x -> realToFrac (sum x) / genericLength x

Мы также можем вывести ap из общего закона

ap f g = do ff <- f ; gg <- g ; return (ff gg)

то есть обессоливая do -notation

ap f g = f >>= \ff -> g >>= \gg -> return (ff gg)

Если подставить определения монадских методов

m >>= f = \x -> f (m x) x
return x = \_ -> x

мы получаем предыдущее определение ap назад (для нашей конкретной монады (->) a). Действительно:

app f g 
= f >>= \ff -> g >>= \gg -> return (ff gg)
= f >>= \ff -> g >>= \gg -> \_ -> ff gg
= f >>= \ff -> g >>= \gg _ -> ff gg
= f >>= \ff -> \x -> (\gg _ -> ff gg) (g x) x
= f >>= \ff -> \x -> (\_ -> ff (g x)) x
= f >>= \ff -> \x -> ff (g x)
= f >>= \ff x -> ff (g x)
= \y -> (\ff x -> ff (g x)) (f y) y
= \y -> (\x -> f y (g x)) y
= \y -> f y (g y)

Ответ 2

Любой лямбда-член может быть переписан эквивалентным термином, который использует только набор подходящих комбинаторов и никаких абстракций лямбда. Этот процесс называется устранение абстракции. Во время процесса вы хотите удалить лямбда-абстракции изнутри. Итак, на одном шаге у вас есть λx.M, где M уже свободен от лямбда-абстракций, и вы хотите избавиться от x.

  • Если M есть x, вы заменяете λx.x на id (id обычно обозначается символом I в комбинационной логике).
  • Если M не содержит x, вы заменяете термин const M (const обычно обозначается символом K в комбинационной логике).
  • Если M есть PQ, то это термин λx.PQ, вы хотите "нажать" x внутри обеих частей приложения функции, чтобы вы могли рекурсивно обрабатывать обе части. Это достигается с помощью комбинатора S, определенного как λfgx.(fx)(gx), т.е. Он принимает две функции и передает x для обоих из них и вместе применяет результаты. Вы можете легко убедиться, что λx.PQ эквивалентен S(λx.P)(λx.Q), и мы можем рекурсивно обрабатывать оба субтермала.

    Как описано в других ответах, комбинатор S доступен в Haskell как ap (или <*>), специализированном для монады-читателя.

Появление монады-читателя не случайно: при решении задачи замены λx.M эквивалентной функцией в основном поднимается M :: a на монаду-читателю r -> a (на самом деле достаточно аппликативной части читателя), где r - тип x. Если мы пересмотрим этот процесс выше:

  • Единственный случай, который действительно связан с монадой-читателем, - это когда M x. Затем мы "поднимем" x до id, чтобы избавиться от переменной. Другие случаи, приведенные ниже, - это просто механические применения для подъема выражения к аппликативному функтору:
  • В другом случае λx.M, где M не содержит x, он просто поднимает M на аппликатор читателя, который равен pure M. Действительно, для (->) r pure эквивалентно const.
  • В последнем случае <*> :: f (a -> b) -> f a -> f b - это приложение функции, снятое с монады/аппликативного. И это именно то, что мы делаем: мы поднимаем обе части P и Q к аппликатору читателя, а затем используем <*> для их связывания.

Процесс может быть дополнительно улучшен за счет добавления большего количества комбинаторов, что позволяет сократить конечный термин. Чаще всего используются комбинаторы B и C, которые в Haskell соответствуют функциям (.) и flip. И снова (.) - это просто fmap/<$> для аппликатора читателя. (Я не знаю такой встроенной функции для выражения flip, но ее можно было бы рассматривать как специализацию f (a -> b) -> a -> f b для аппликатора читателя.)

Некоторое время назад я написал короткую статью об этом: The Monad Reader Issue 17, The Reader Monad и Abstraction Elimination.суб >