Понимание функции bind в Haskell
Я знаком с монадами в теории категорий (на самом деле это очень простая концепция), но функция >>=
в Haskell полностью меня озадачивает. Итак, применяя привязку к значению M a
, а функция a -> M u
совпадает с первым применением монады к этой функции, затем оценивая ее по указанному значению и умножая результат: a >>= f
совпадает с join $ (fmap f) $ a
. Но как это естественное описание вычислений? Есть ли какой-нибудь полезный способ взглянуть на него, что поможет мне понять это?
Есть ли какая-нибудь хорошая статья где-то, которая не ориентирована на кого-то нового из джунглей С++?
Ответы
Ответ 1
Рассмотрим оператор композиции монадической функции <=<
. Это аналогично .
, за исключением того, что оно работает над монадическими функциями. Его можно определить просто в терминах >>=
, поэтому изучение одного из нас будет информировать нас о другом.
(<=<) :: (a -> m b) -> (b -> m c) -> a -> m c
(f <=< g) x = g x >>= f
(.) :: (a -> b) -> (b -> c) -> a -> c
(f . g) x = g x |> f
where z |> h = h z
В случае .
сначала выполняется "t25 > ", а затем f
выполняется на выходе g
. В случае <=<
, g
и его эффекты сначала выполняются, а затем f
и его эффекты выполняются. Это немного неправильно, чтобы сказать, что на самом деле происходит "до" другого, так как не все монады работают таким образом.
Возможно, более точно сказать, что f
может воспользоваться дополнительной контекстной информацией, предоставленной g
. Но это не совсем правильно, поскольку g
может потенциально отобрать контекстуальную информацию. Если вы хотите, чтобы 100% правильно описывали монады, вам действительно нужно ходить на яичной скорлупе.
Но почти во всех нетривиальных случаях f <=< g
означает, что эффекты (а также результат) монадической функции g
будут впоследствии влиять на поведение монадической функции f
.
Чтобы задать вопросы о v >>= f = join (fmap f v)
Рассмотрим f :: a -> m b
и v :: m a
. Что значит fmap f v
? Ну fmap :: (c -> d) -> m c -> m d
, и в этом случае c = a
и d = m b
, поэтому fmap f :: m a -> m (m b)
. Теперь, конечно, мы можем применить v :: m a
к этой функции, в результате получим m (m b)
. но что именно означает этот результат типа m (m b)
?
Внутренний m
представляет контекст из f
. Внешний m
представляет контекст, происходящий из v
(n.b. fmap
не должен нарушать этот исходный контекст).
И затем вы join
, что m (m b)
, разбивая эти два контекста вместе на m a
. Это основа определения Monad
: вы должны предоставить способ разбить контексты вместе. Вы можете проверить детали реализации различных экземпляров Monad
, чтобы попытаться понять, как они "разбивают" контексты вместе. Вывод здесь, однако, заключается в том, что "внутренний контекст" ненаблюдаем, пока вы не объедините его с "внешним контекстом". Если вы используете v >>= f
, то нет фактического понятия функции f
, получающей чистое значение a
и создающего простой монадический результат m b
. Вместо этого мы понимаем, что f
действует в исходном контексте v
.
Ответ 2
Хм. Я думаю, что хороший способ подумать о том, что >>=
позволяет вам составлять вычисления; сами вычисления выполнены в виде a -> m b
. Итак, m b
просто представляет результат вычисления.
Итак, вычисление просто берет некоторое значение и дает некоторый результат. Хорошим примером здесь является тип списка: a -> [b]
представляет собой недетерминированное вычисление. Он принимает один вход, но может давать несколько результатов. Сам по себе a -> [b]
как вычисление имеет смысл. Но как бы вы их объединили? Естественным ответом является то, что вы будете выполнять каждое последовательное "вычисление" во всех предыдущих результатах. И это именно то, что >>=
делает для списков.
Одна вещь, которая действительно помогла мне понять практическую ценность этого, - это думать о DFA и NFA. Вы можете представить себе тривиальное письмо DFA в Haskell примерно так:
data State = S1 | S2 | S3 | S4 | Q
data Input = A | B
transition :: State -> Input -> State
transition S1 A = S2
transition S1 B = S3
-- and so on...
Тогда мы могли бы просто сбрасывать ввод:
foldl transition S1 [A, A, B, B]
Теперь, как бы мы взяли этот код и обобщили его на NFA? Ну, переходная функция для NFA может рассматриваться как недетерминированное вычисление. Поэтому мы определяем что-то вроде:
transition S1 A = [S1, S2]
transition S1 B = []
Но теперь нам нужно сделать какую-то странную гимнастику, чтобы использовать foldl
! К счастью, мы можем просто foldM
. Итак, здесь "вычисление", смоделированное монадой, является недетерминированной функцией перехода.
Ответ 3
Возможно, =<<
легче понять с точки зрения вычислений (это просто flip (>>=)
). Он набирает (=<<) :: (Monad m) => (a -> m b) -> m a -> m b
и соответствует типу приложения функции, cf ($) :: (a -> b) -> a -> b
. Таким образом, >>=
- это просто перевернутое функциональное приложение на монадическом уровне.
Кроме того, (>>=)
используется в desugaring do
нотации, а do
синтаксически очень соответствует императивному коду (в подходящей монаде).
Ответ 4
Вот приблизительное представление о том, как оно работает как модель вычисления: конструктор типа M
с экземпляром Monad
представляет собой структуру параметрических данных, а непараметрические части этой структуры могут содержать другую информацию, return
и join
соответствуют некоторому моноиду для тех частей структуры. Функция a -> M b
вводит информацию в эту структуру на основе ввода типа a
. Поэтому, поднимая функцию a -> M b
до M a -> M b
, мы используем параметрическую информацию M
для создания некоторой непараметрической информации, а затем комбинируем ее с информацией, уже имеющейся в значении типа M a
.
Асимметричный характер типа a -> M b
дает неотъемлемое направление потока непараметрической информации, а требование ассоциативности означает, что общий порядок - это единственное, что имеет значение.
Конечным результатом является расширение функций с каким-то контекстом, который имеет встроенное понятие причинно-следственных связей.