Monads with Join() вместо Bind()

Монады обычно объясняются по очереди return и bind. Однако, я понимаю, вы также можете реализовать bind в терминах joinfmap?)

В языках программирования, не имеющих первоклассных функций, 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 ::ma) приводит к значениям (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>.

Монад объяснил на одном снимке.