Monads with Join() вместо Bind()
Монады обычно объясняются по очереди return
и bind
. Однако, я понимаю, вы также можете реализовать bind
в терминах join
(и fmap
?)
В языках программирования, не имеющих первоклассных функций, bind
мучительно неудобно использовать. join
, с другой стороны, выглядит довольно легко.
Я не совсем уверен, что понимаю, как работает join
. Очевидно, что он имеет тип [Haskell]
join :: Monad m => m (m x) -> m x
Для монады списка это тривиально и, очевидно, concat
. Но для общей монады, что, на самом деле, делает этот метод на самом деле? Я вижу, что он делает с сигнатурами типов, но я пытаюсь понять, как я напишу что-то подобное, например, в Java или аналогичном.
(На самом деле, это легко: я бы не стал. Потому что дженерики сломаны.;-) Но в принципе вопрос все еще стоит...)
К сожалению. Похоже, это было задано раньше:
Функция присоединения к Monad
Может ли кто-нибудь набросать некоторые реализации общих монад, используя return
, fmap
и join
? (I.e., не говоря уже о >>=
вообще.) Я думаю, возможно, это могло бы помочь ему погрузиться в мой немой мозг...
Ответы
Ответ 1
Без учета глубины метафоры я могу предложить типичную монаду m
как "стратегию для создания", поэтому тип m value
является стратегией первого класса для создания значения. Различные понятия вычисления или внешнего взаимодействия требуют разных типов стратегии, но общее понятие требует некоторой регулярной структуры, чтобы иметь смысл:
- если у вас уже есть значение, тогда у вас есть стратегия для создания значения (
return :: v -> m v
), состоящего из ничего, кроме создания значения, которое у вас есть;
- Если у вас есть функция, которая преобразует один вид значения в другой, вы можете поднять его до стратегий (
fmap :: (v -> u) -> m v -> m u
), просто ожидая, пока стратегия выполнит свое значение, а затем преобразует его;
- Если у вас есть стратегия для создания стратегии для создания значения, вы можете построить стратегию для создания значения (
join :: m (m v) -> m v
), которое следует внешней стратегии, пока она не выработает внутреннюю стратегию, а затем следует, что внутренняя стратегия все путь к значению.
Приведем пример: двоичные деревья с меткой, помеченные листом...
data Tree v = Leaf v | Node (Tree v) (Tree v)
... представляют собой стратегии для производства вещей, бросая монету. Если стратегия Leaf v
, там ваш v
; если стратегия Node h t
, вы бросаете монету и продолжаете стратегию h
, если монета показывает "голова", t
, если она "хвост".
instance Monad Tree where
return = Leaf
Стратегия создания стратегии - это дерево с помеченными деревом листьями: вместо каждого такого листа мы можем просто пересаживаться в дерево, которое его наклеивает...
join (Leaf tree) = tree
join (Node h t) = Node (join h) (join t)
... и, конечно, мы имеем fmap
, который просто переносит листья.
instance Functor Tree where
fmap f (Leaf x) = Leaf (f x)
fmap f (Node h t) = Node (fmap f h) (fmap f t)
Здесь приведена стратегия создания стратегии для создания Int
.
![tree of trees]()
Бросьте монету: если она "голова", бросьте другую монету, чтобы решить между двумя стратегиями (производя, соответственно, "бросить монету для производства 0 или произвести 1" или "произвести 2" ); если "хвосты" производят третью ( "бросают монету для производства 3 или бросают монету на 4 или 5" ).
Это ясно join
, чтобы сделать стратегию, создающую Int
.
![enter image description here]()
То, что мы используем, - это тот факт, что "стратегия создания ценности" сама по себе может рассматриваться как ценность. В Haskell вложение стратегий как значений является бесшумным, но на английском языке я использую кавычки, чтобы отличать использование стратегии от просто разговоров об этом. Оператор join
выражает стратегию "как-то производить, а затем следовать стратегии", или "если вам говорят о стратегии, вы можете ее использовать".
(Meta. Я не уверен, является ли этот "стратегический" подход подходящим общим способом думать о монадах и различии в значении/вычислении, или же это просто еще одна метательная метафора. Я нахожу листовые помеченные деревьями типы - полезный источник интуиции, что, возможно, не является неожиданностью, поскольку они являются свободными монадами, с достаточной структурой, чтобы быть монадами вообще, но не более.)
PS Тип привязки
(>>=) :: m v -> (v -> m w) -> m w
говорит: "Если у вас есть стратегия для создания v
, и для каждого v следующая стратегия для создания w
, тогда у вас есть стратегия для создания w
". Как мы можем зафиксировать это в терминах join
?
mv >>= v2mw = join (fmap v2mw mv)
Мы можем переложить нашу стратегию v
-producing на v2mw
, производя вместо каждого v
значение w
-производительную стратегию, которая следует из нее; готов к join
!
Ответ 2
join = concat -- []
join f = \x -> f x x -- (e ->)
join f = \s -> let (f', s') = f s in f' s' -- State
join (Just (Just a)) = Just a; join _ = Nothing -- Maybe
join (Identity (Identity a)) = Identity a -- Identity
join (Right (Right a)) = Right a; join (Right (Left e)) = Left e;
join (Left e) = Left e -- Either
join ((a, m), m') = (a, m' 'mappend' m) -- Writer
-- N.B. there is a non-newtype-wrapped Monad instance for tuples that
-- behaves like the Writer instance, but with the tuple order swapped
join f = \k -> f (\f' -> f' k) -- Cont
Ответ 3
Хорошо, так что это не очень хорошая форма, чтобы ответить на ваш собственный вопрос, но я собираюсь записать свое мышление, если оно просветит кого-нибудь еще. (Я сомневаюсь...)
Если монаду можно рассматривать как "контейнер", то как return
, так и join
имеют довольно очевидную семантику. return
генерирует контейнер из 1 элемента, а join
превращает контейнер контейнеров в один контейнер. Ничего сложного в этом.
Итак, давайте сосредоточимся на монадах, которые более естественно воспринимаются как "действия". В этом случае m x
- это какое-то действие, которое дает значение типа x
при его "выполнении". return x
ничего особенного не делает, а затем дает x
. fmap f
выполняет действие, которое дает x
, и создает действие, которое вычисляет x
, а затем применяет к нему f
и возвращает результат. Пока что так хорошо.
Довольно очевидно, что если f
сам генерирует действие, то в итоге вы получаете m (m x)
. То есть, действие, которое вычисляет другое действие. В некотором роде, возможно, даже проще обернуть свой ум вокруг функции >>=
, которая принимает действие и "функцию, которая производит действие" и т.д.
Итак, логически говоря, кажется, что join
выполнит первое действие, предпримет действие, которое оно произведет, а затем запустит это. (Вернее, join
вернет действие, которое делает то, что я только что описал, если вы хотите разделить волосы.)
Это, по-видимому, центральная идея. Чтобы реализовать join
, вы хотите запустить действие, которое затем даст вам другое действие, а затем запустите его. (Какой бы "прогон" не имел значения для этой конкретной монады.)
Учитывая это прозрение, я могу взять удар в написании некоторых реализаций join
:
join Nothing = Nothing
join (Just mx) = mx
Если внешнее действие Nothing
, верните Nothing
, else верните внутреннее действие. Опять же, Maybe
больше контейнера, чем действие, поэтому попробуйте что-то еще...
newtype Reader s x = Reader (s -> x)
join (Reader f) = Reader (\ s -> let Reader g = f s in g s)
Это было... безболезненно. A Reader
- это просто функция, которая принимает глобальное состояние и только затем возвращает результат. Итак, чтобы разблокировать, вы применяете глобальное состояние к внешнему действию, которое возвращает новый Reader
. Затем вы также применяете это состояние к этой внутренней функции.
В некотором смысле, это может быть проще, чем обычно:
Reader f >>= g = Reader (\ s -> let x = f s in g x)
Теперь, какая из них является функцией чтения, а какая - это функция, которая вычисляет следующий читатель...?
Теперь попробуйте добрую старую монаду State
. Здесь каждая функция принимает исходное состояние в качестве входных данных, но также возвращает новое состояние вместе с его выходом.
data State s x = State (s -> (s, x))
join (State f) = State (\ s0 -> let (s1, State g) = f s0 in g s1)
Это было не слишком сложно. Он запускается в основном после запуска.
Сейчас я перестану печатать. Не стесняйтесь указывать все глюки и опечатки в моих примерах...: -/
Ответ 4
Я нашел много объяснений монадов, которые говорят: "вам не нужно ничего знать о теории категорий, на самом деле, просто подумайте о монадах как burritos/space suits/whatever".
Действительно, статья о том, что демистифицированные монады для меня просто сказали, какие категории были, описывали монады (включая объединение и привязку) с точки зрения категорий и не беспокоились о каких-либо фиктивных метафорах:
Я думаю, что статья очень читаема без значительных математических знаний.
Ответ 5
Вызов fmap (f :: a -> m b) (x ::
m
a)
приводит к значениям (y ::
m
(m b))
, поэтому очень естественно использовать join
для возврата значений (z :: m b)
.
Тогда связывание определяется просто как bind ma f = join (fmap f ma)
, таким образом, достигается композиционная способность Клейсли функций сорта (:: a -> m b)
, и это то, чем он действительно является:
ma 'bind' (f >=> g) = (ma 'bind' f) 'bind' g -- bind = (>>=)
= ('bind' g) . ('bind' f) $ ma
= join . fmap g . join . fmap f $ ma
Итак, с flip bind = (=<<)
, у нас есть
((g <=< f) =<<) = (g =<<) . (f =<<) = join . (g <$>) . join . (f <$>)
![Kleisli composition]()
Ответ 6
Вопрос о том, что такое подпись типа в Haskell, скорее напоминает запрос интерфейса Java.
Это, в некотором смысле, "не". (Хотя, конечно, у вас, как правило, есть какая-то цель, связанная с этим, что в основном в вашем уме, и в основном не в реализации.)
В обоих случаях вы объявляете правовые последовательности символов на языке, который будет использоваться в последующих определениях.
Конечно, в Java, я полагаю, вы могли бы сказать, что интерфейс соответствует сигнатуре типа, которая будет реализована буквально в виртуальной машине. Вы можете получить некоторый полиморфизм таким образом - вы можете определить имя, которое принимает интерфейс, и вы можете предоставить другое определение для имени, которое принимает другой интерфейс. Что-то подобное происходит в Haskell, где вы можете предоставить декларацию для имени, которое принимает один тип, а затем другое объявление для этого имени, которое относится к другому типу.
Ответ 7
Это Монада объясняется на одной картинке. 2 функции в зеленой категории не могут быть скомпонованы, когда они отображаются в синюю категорию (строго говоря, они являются одной категорией), они становятся составными. Monad - превращение функции типа T -> Monad<U>
в функцию Monad<T> -> Monad<U>
.
![Монад объяснил на одном снимке.]()