Есть ли какая-то интуиция для понимания объединения двух функций в Монаде?
join
определяется вместе с bind
, чтобы сгладить объединенную структуру данных в единую структуру.
Из системного представления типа (+) 7 :: Num a => a -> a
можно рассматривать как Functor
, (+) :: Num a => a -> a -> a
можно рассматривать как Functor
of Functor
, как получить некоторую интуицию об этом, а не просто полагаться на систему типов? Почему join (+) 7 === 14
?
Несмотря на то, что можно получить окончательный результат путем ручного перехода по процессу связывания функции, было бы здорово, если бы была указана какая-то интуиция.
Это из упражнений NICTA.
-- | Binds a function on the reader ((->) t).
--
-- >>> ((*) =<< (+10)) 7
-- 119
instance Bind ((->) t) where
(=<<) ::
(a -> ((->) t b))
-> ((->) t a)
-> ((->) t b)
(f =<< a) t =
f (a t) t
-- | Flattens a combined structure to a single structure.
--
-- >>> join (+) 7
-- 14
join ::
Bind f =>
f (f a)
-> f a
join f =
id =<< f
*Course.State> :t join (+)
join (+) :: Num a => a -> a
*Course.State> :t join
join :: Bind f => f (f a) -> f a
*Course.State> :t (+)
(+) :: Num a => a -> a -> a
Ответы
Ответ 1
как получить некоторую интуицию об этом, а не просто полагаться на систему типов?
Я бы сказал, что полагаться на систему типов - отличный способ создать конкретную интуицию. Тип join
:
join :: Monad m => m (m a) -> m a
Специализируется на (->) r
, он становится:
(r -> (r -> a)) -> (r -> a)
Теперь попробуйте определить join
для функций:
-- join :: (r -> (r -> a)) -> (r -> a)
join f = -- etc.
Мы знаем, что результат должен быть функцией r -> a
:
join f = \x -> -- etc.
Однако мы ничего не знаем о том, что такое типы r
и a
, и поэтому мы ничего не знаем в частности о f :: r -> (r -> a)
и x :: r
. Наше невежество означает, что мы можем сделать с ними только одну вещь: передав x
в качестве аргумента, как в f
, так и в f x
:
join f = \x -> f x x
Следовательно, join
для функций пропускает один и тот же аргумент дважды, потому что это единственно возможная реализация. Конечно, эта реализация является лишь правильной монадической join
, поскольку она следует за законами монады:
join . fmap join = join . join
join . fmap return = id
join . return = id
Проверка того, что может быть другим приятным упражнением.
Ответ 2
Следуя традиционной аналогии монады как контекста для вычисления, join
- это метод объединения контекстов. Начните с вашего примера. join (+) 7
. Использование функции в качестве монады подразумевает монаду-читателю. (+ 1)
- это монада-читатель, которая берет среду и добавляет ее к ней. Таким образом, (+)
будет монадом-читателем в монаде-читателе. Монада внешнего читателя берет среду n
и возвращает читателя формы (n +)
, которая примет новую среду. join
просто объединяет две среды, поэтому вы предоставляете их один раз и дважды применяете данный параметр. join (+) === \x -> (+) x x
.
Теперь, в общем, рассмотрим другие примеры. Монада Maybe
представляет потенциальный сбой. Значение Nothing
является неудачным вычислением, тогда как a Just x
является успешным. A Maybe
в пределах Maybe
- это вычисление, которое может выходить из строя дважды. Значение Just (Just x)
, очевидно, является успешным, поэтому объединение, которое дает Just x
. A Nothing
или Just Nothing
указывает на сбой в какой-то момент, поэтому объединение возможного сбоя должно указывать на то, что вычисление завершилось неудачно, т.е. Nothing
.
Аналогичная аналогия может быть сделана для монады списка, для которой join
является просто concat
, монада-писатель, которая использует моноидальный оператор <>
для объединения соответствующих выходных значений или любой другой монады.
join
является фундаментальным свойством монад и является операцией, которая делает ее значительно более сильной, чем функтор или аппликативный функтор. Функторы могут быть сопоставлены, аппликативные могут быть последовательностями, монады могут быть объединены. Категорически, монада часто определяется как join
и return
. Так получилось, что в Haskell нам удобнее определять его в терминах return
, (>>=)
и fmap
, но эти два определения были доказаны как синонимы.
Ответ 3
Интуиция о join
заключается в том, что в нее вложено 2 контейнера..e.g
join [[1]] => [1]
join (Just (Just 1)) => 1
join (a christmas tree decorated with small cristmas tree) => a cristmas tree
и т.д.
Теперь, как вы можете присоединиться к функциям? Фактически функции, можно рассматривать как контейнер.
Например, если вы посмотрите на таблицу хешей. Вы даете ключ, и получаете значение (или нет). Это функция key -> value
(или если вы предпочитаете key -> Maybe value
).
Итак, как бы вы присоединились к 2 HashMap?
Скажем, у меня есть (в стиле python) h={"a": {"a": 1, "b": 2}, "b" : {"a" : 10, "b" : 20 }}
, как я могу присоединиться к нему, или если вы предпочитаете его сгладить?
Учитывая "a"
какое значение я должен получить? h["a"]
дает мне {"a":1, "b":2}
. Единственное, что я могу сделать с этим, - это снова найти "а" в этом новом значении, которое дает мне 1
.
Следовательно, join h
равно {"a":1, "b":20}
.
То же самое для функции.