it-roy-ru.com

Монады с Join () вместо Bind ()

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

В языках программирования, в которых отсутствуют первоклассные функции, bind крайне неудобно использовать. join, с другой стороны, выглядит довольно просто.

Однако я не совсем уверен, что понимаю, как работает join. Очевидно, он имеет тип [Haskell]

 join :: Monad m => m (m x) -> m x 

Для монады списка это тривиально и очевидно concat. Но для общей монады, что в действительности делает этот метод? Я вижу, что это делает с сигнатурами типов, но я пытаюсь выяснить, как бы я написал что-то вроде этого, скажем, Java или подобное.

(На самом деле, это просто: я бы не стал. Потому что дженерики сломаны. ;-) Но в принципе вопрос все еще стоит ...)


К сожалению. Похоже, об этом уже спрашивали:

функция соединения монады

Может ли кто-нибудь набросать некоторые реализации обычных монад, используя return, fmap и join? (То есть, вообще не упоминая >>=.) Думаю, возможно, это могло бы помочь ему проникнуть в мой тупой мозг ...

59
MathematicalOrchid

Не вдаваясь в глубину метафоры, я мог бы предложить прочитать типичную монаду 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").

Это явно joins, чтобы создать стратегию, создающую Int.

enter image description here

Мы пользуемся тем фактом, что "стратегию создания стоимости" можно рассматривать как ценность. В Haskell встраивание стратегий в качестве значений молчит, но в английском я использую кавычки, чтобы отличить использование стратегии от простого разговора об этом. Оператор join выражает стратегию "каким-то образом производить, а затем следовать стратегии" или "если вы сказали стратегия, то вы можете --- использовать это".

(Мета. Я не уверен, является ли этот "стратегический" подход подходящим для понимания монад и различий между значением и вычислением, или это просто еще одна непристойная метафора. Я считаю полезными древовидные типы с метками листьев источник интуиции, что, возможно, не является сюрпризом, поскольку они бесплатные монады, с достаточной структурой, чтобы быть монадой вообще, но не более.)

PS Тип "связать"

(>>=) :: m v -> (v -> m w) -> m w

говорит: "если у вас есть стратегия для создания v, и для каждого v последующей стратегии для создания w, то у вас есть стратегия для создания w". Как мы можем уловить это с точки зрения join?

mv >>= v2mw = join (fmap v2mw mv)

Мы можем переименовать нашу стратегию создания v с помощью v2mw, создав вместо каждого значения v стратегию w-, которая следует из нее, - готовую к join!

92
pigworker
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
join f = \k -> f (\f' -> f' k) -- Cont
24
Daniel Wagner

Хорошо, так что это не очень хорошая форма, чтобы ответить на ваш собственный вопрос, но я собираюсь записать мои мысли на случай, если это просветит кого-либо еще. (Я сомневаюсь...)

Если монаду можно рассматривать как "контейнер", то и 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, иначе верните внутреннее действие. Опять же, Maybe - это больше контейнер, чем действие, поэтому давайте попробуем что-нибудь еще ...

newtype Reader s x = Reader (s -> x)

join (Reader f) = Reader (\ s -> let Reader g = f s in g s)

Это было ... безболезненно. 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)

Это было не слишком сложно. Это в основном бег, а затем бег.

Я собираюсь прекратить печатать сейчас. Не стесняйтесь указывать все глюки и опечатки в моих примерах ...: - /

14
MathematicalOrchid

Я нашел много объяснений монад, которые говорят: "Вы не должны ничего знать о теории категорий, на самом деле, просто думайте о монадах как о буррито/скафандрах/чем угодно".

Действительно, статья, в которой для меня демистифицированы монады, просто говорила, какие категории были, описывала монады (включая объединения и связывание) в терминах категорий, и не беспокоилась ни о каких фиктивных метафорах:

Я думаю, что статья очень читабельна без большого знания математики.

11
solrize

Вызов fmap (f :: a -> m b) (x ::ma) производит значения (y ::m(m b)), поэтому очень естественная вещь использовать join для получения значений (z :: m b).

Тогда связывание определяется просто как bind ma f = join (fmap f ma), таким образом, достигается Kleisly композиционность функций из разнообразия (:: 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 <$>)

enter image description here

10
Will Ness

Спрашивать, что за сигнатура типа в Haskell делает, очень похоже на вопрос, что такое интерфейс в Java делает.

В каком-то буквальном смысле этого слова нет. (Хотя, конечно, у вас, как правило, есть какая-то цель, связанная с этим, это в основном у вас на уме, и в основном не в реализации.)

В обоих случаях вы объявляете правовые последовательности символов на языке, который будет использоваться в последующих определениях.

Конечно, в Java, я думаю, вы могли бы сказать, что интерфейс соответствует сигнатуре типа, которая будет реализована буквально в ВМ. Таким образом, вы можете получить некоторый полиморфизм - вы можете определить имя, которое принимает интерфейс, и вы можете предоставить другое определение имени, которое принимает другой интерфейс. Нечто подобное происходит в Haskell, где вы можете предоставить объявление для имени, которое принимает один тип, а затем другое объявление для этого имени, которое относится к другому типу.

3
rdm

Это Монада объяснила на одной картинке. Две функции в зеленой категории не могут быть скомпонованы, при сопоставлении с синей категорией (строго говоря, они являются одной категорией) они становятся компонуемыми. Monad - это превращение функции типа T -> Monad<U> в функцию Monad<T> -> Monad<U>.

Monad explained in one picture.

1
Dagang