it-swarm.com.ru

Монада - это просто моноид в категории эндофункторов, в чем проблема?

Кто первым сказал следующее?

Монада - это просто моноид в категории эндофункторов, в чем проблема?

И на менее важной ноте, правда ли это, и если да, то могли бы вы дать объяснение (надеюсь, что оно может быть понято кем-то, кто не имеет большого опыта работы с Haskell)?

675
Roman A. Taycher

Эта конкретная фраза написана Джеймсом Ири из его весьма занимательной краткой, неполной и в основном неправильной истории языков программирования, в которой он вымышленно приписывает ее Филиппу Wadler.

Оригинальная цитата написана Сондерсом Мак Лейном в категориях для рабочего математика , одного из основополагающих текстов теории категорий. Здесь это в контексте , что, вероятно, лучшее место, чтобы узнать, что именно это означает.

Но я сделаю удар. Оригинальное предложение таково:

В общем, монада в X - это просто моноид в категории эндофункторов X, с произведением ×, замененным композицией эндофункторов и единицей, установленной единичным эндофунктором.

X здесь есть категория. Endofunctors - это функторы от категории к себе (которая обычно all Functorname__s), что касается функциональных программистов, поскольку они в основном имеют дело только с одной категорией, категория типов - но я отвлекся). Но вы можете представить другую категорию, которая называется "endofunctors on X". Это категория, в которой объекты являются эндофункторами, а морфизмы - естественными преобразованиями.

И из этих эндофункторов, некоторые из них могут быть монадами. Какие из них монады? Именно те, которые моноидальны в определенном смысле. Вместо того, чтобы описать точное отображение от монад к моноидам (поскольку Mac Lane делает это намного лучше, чем я мог надеяться), я просто добавлю их соответствующие определения рядом и позволю вам сравнить:

Моноид это ...

  • Набор, S
  • Операция, •: S × S → S
  • Элемент S, e: 1 → S

... удовлетворяя этим законам:

  • (a • b) • c = a • (b • c) , для всех a , b и c в S
  • e • a = a • e = a , для всех a в S

Монада это ...

  • Endofunctor, T: X → X (в Haskell, конструктор типов типа * -> * с экземпляром Functorname__)
  • Естественное преобразование μ: T × T → T , где × означает композицию функтора ( μ в Хаскеле называется joinNAME _
  • Естественное преобразование η: I → T , где I - тождественный эндофунктор на X ( η известен как returnNAME _ в Хаскеле)

... удовлетворяя этим законам:

  • μ ∘ Tμ = μ ∘ μT
  • μ ∘ Tη = μ ∘ ηT = 1 (тождественное естественное преобразование)

Немного прищурившись, вы сможете увидеть, что оба эти определения являются экземплярами одного и того же абстрактная концепция .

737
Tom Crockett

Интуитивно, я думаю, что сказочный математический словарь говорит о том, что:

Monoid

A monoid - это набор объектов и метод их объединения. Хорошо известны моноиды:

  • числа, которые вы можете добавить
  • списки, которые вы можете объединить
  • наборы, которые вы можете объединить

Есть и более сложные примеры.

Кроме того, каждый моноид имеет identity, то есть тот элемент "no-op", который не действует при его объединении. с чем-то еще:

  • 0 + 7 == 7 + 0 == 7
  • [] ++ [1,2,3] == [1,2,3] ++ [] == [1,2,3]
  • {} union {Apple} == {Apple} union {} == {Apple}

Наконец, моноид должен быть ассоциативный. (вы можете уменьшить длинную строку комбинаций в любом случае, если только вы не измените порядок объектов слева направо). Дополнение в порядке ((5 + 3) +1 == 5+ (3+ 1)), но вычитание не ((5-3) -1! = 5-1)).

Монада

Теперь давайте рассмотрим особый вид множества и особый способ объединения объектов.

Объекты

Предположим, что ваш набор содержит объекты специального вида: --- (функции. И эти функции имеют интересную сигнатуру: они не переносят числа в числа или строки в строки. Вместо этого каждая функция переносит число в список чисел в двухэтапном процессе.

  1. Вычислить 0 или более результатов
  2. Объедините эти результаты в один ответ как-нибудь.

Примеры:

  • 1 -> [1] (просто оберните ввод)
  • 1 -> [] (отменить ввод, обернуть небытие в списке)
  • 1 -> [2] (добавьте 1 к входу и оберните результат)
  • 3 -> [4, 6] (добавьте 1 к входу, умножьте ввод на 2 и оберните несколько результатов )

Объединение объектов

Кроме того, наш способ объединения функций является особенным. Простой способ объединить функцию - это композиция : давайте возьмем наши примеры выше и скомпонуем каждую функцию с собой:

  • 1 -> [1] -> [[1]] (дважды оберните вход)
  • 1 -> [] -> [] (отменить ввод, обернуть пустоту в списке, дважды)
  • 1 -> [2] -> [UH-OH! ] (мы не можем "добавить 1" в список! ")
  • 3 -> [4, 6] -> [UH-OH! ] (мы не можем добавить 1 список!)

Не слишком углубляясь в теорию типов, дело в том, что вы можете объединить два целых числа, чтобы получить целое число, но вы не всегда можете составить две функции и получить функцию одного типа. (Функции с типом a -> a будут составлять, но a-> [a] не будет.)

Итак, давайте определим другой способ объединения функций. Когда мы объединяем две из этих функций, мы не хотим "оборачивать" результаты.

Вот что мы делаем. Когда мы хотим объединить две функции F и G, мы следуем этому процессу (называемому привязкой ):

  1. Вычислите "результаты" из F, но не объединяйте их.
  2. Вычислите результаты применения G к каждому из результатов F по отдельности, получая коллекцию результатов.
  3. Сгладьте 2-уровневую коллекцию и объедините все результаты.

Возвращаясь к нашим примерам, давайте объединим (свяжем) функцию с самой собой, используя этот новый способ "связывания" функций:

  • 1 -> [1] -> [1] (дважды обернуть вход)
  • 1 -> [] -> [] (отменить ввод, обернуть пустоту в списке, дважды)
  • 1 -> [2] -> [3] (добавьте 1, затем снова добавьте 1 и оберните результат.)
  • 3 -> [4,6] -> [5,8,7,12] (добавьте 1 к вводу, а также умножьте ввод на 2, сохраняя оба результата, затем сделайте все снова для обоих результатов, а затем оберните окончательный вариант результаты в списке.)

Этот более сложный способ объединения функций является ассоциативным (как следует из того, как составление функций ассоциативно, когда вы не делаете причудливую упаковку).

Связывая все это вместе,

  • монада - это структура, которая определяет способ объединения (результатов) функций,
  • аналогично тому, как моноид является структурой, которая определяет способ объединения объектов,
  • где метод комбинации является ассоциативным,
  • и там, где есть специальное 'No-op', которое может быть объединено с любым чем-то , чтобы привести к чему-то без изменений.

Заметки

Есть много способов "обернуть" результаты. Вы можете создать список, или набор, или отбросить все, кроме первого результата, отметив, что результатов нет, прикрепить коляску состояний, распечатать сообщение журнала и т.д. И т.д.

Я немного расстроился с определениями в надежде донести основную идею до интуитивно понятной.

Я немного упростил ситуацию, настаивая на том, что наша монада работает с функциями типа a -> [a] . На самом деле, монады работают с функциями типа a -> m b , но обобщение - это своего рода техническая деталь, которая не является основным понятием.

503
misterbee

Во-первых, расширения и библиотеки, которые мы собираемся использовать:

{-# LANGUAGE RankNTypes, TypeOperators #-}

import Control.Monad (join)

Из них RankNTypes является единственным, который абсолютно необходим для нижеследующего. однажды я написал объяснение RankNTypes, которое некоторым людям показалось полезным , поэтому я буду ссылаться на это.

Цитируя отличный ответ Тома Крокетта , мы имеем:

Монада это ...

  • Endofunctor, T: X -> X
  • Естественное преобразование, μ: T × T -> T, где × означает композицию функтора
  • Естественное преобразование, η: I -> T, где I - тождество endofunctor on X

... удовлетворяя этим законам:

  • μ (μ (T × T) × T)) = μ (T × μ (T × T))
  • μ (η (T)) = T = μ (T (η))

Как мы можем перевести это на код на Haskell? Что ж, давайте начнем с понятия естественного преобразования :

-- | A natural transformations between two 'Functor' instances.  Law:
--
-- > fmap f . eta g == eta g . fmap f
--
-- Neat fact: the type system actually guarantees this law.
--
newtype f :-> g =
    Natural { eta :: forall x. f x -> g x }

Тип формы f :-> g аналогичен типу функции, но вместо того, чтобы думать о нем как о функции между двумя типами (типа *), думать о нем как о морфизм между двумя функторами (каждый из них * -> *). Примеры:

listToMaybe :: [] :-> Maybe
listToMaybe = Natural go
    where go [] = Nothing
          go (x:_) = Just x

maybeToList :: Maybe :-> []
maybeToList = Natural go
    where go Nothing = []
          go (Just x) = [x]

reverse' :: [] :-> []
reverse' = Natural reverse

По сути, в Haskell естественные преобразования - это функции из некоторого типа f x в другой тип g x, так что переменная типа x является "недоступной" для вызывающей стороны. Так, например, sort :: Ord a => [a] -> [a] нельзя превратить в естественное преобразование, потому что он "требователен" к тому, какие типы мы можем создать для a. Один интуитивный способ, которым я часто пользуюсь, чтобы думать об этом, заключается в следующем:

  • Функтор - это способ работы с content ​​чего-либо, не затрагивая structure.
  • Естественное преобразование - это способ воздействовать на структуру чего-либо, не касаясь и не глядя на содержимое.

Теперь, со всем этим, давайте рассмотрим пункты определения.

Первый пункт - это "endofunctor, T: X -> X". Ну, каждый Functor в Haskell является endofunctor в том, что люди называют "категорией Hask", чьи объекты являются типами Haskell (типа *) и чьи морфизмы являются функциями Haskell. Это звучит как сложное утверждение, но на самом деле оно очень тривиально. Все это означает, что Functor f :: * -> * дает вам возможность создать тип f a :: * для любого a :: * и функцию fmap f :: f a -> f b из любого f :: a -> b, и что они подчиняются законам функторов.

Второе предложение: функтор Identity в Haskell (который поставляется с платформой, так что вы можете просто импортировать его) определяется следующим образом:

newtype Identity a = Identity { runIdentity :: a }

instance Functor Identity where
    fmap f (Identity a) = Identity (f a)

Таким образом, естественное преобразование η: I -> T из определения Тома Крокетта может быть записано таким образом для любого Monad экземпляра t:

return' :: Monad t => Identity :-> t
return' = Natural (return . runIdentity)

Третье предложение: состав двух функторов в Haskell может быть определен следующим образом (который также поставляется с платформой):

newtype Compose f g a = Compose { getCompose :: f (g a) }

-- | The composition of two 'Functor's is also a 'Functor'.
instance (Functor f, Functor g) => Functor (Compose f g) where
    fmap f (Compose fga) = Compose (fmap (fmap f) fga)

Поэтому естественное преобразование μ: T × T -> T из определения Тома Крокетта можно записать так:

join' :: Monad t => Compose t t :-> t
join' = Natural (join . getCompose)

Утверждение, что это моноид в категории endofunctors, означает, что Compose (частично применяется только к его первым двум параметрам) является ассоциативным, и что Identity является его единичным элементом. Т.е. справедливы следующие изоморфизмы:

  • Compose f (Compose g h) ~= Compose (Compose f g) h
  • Compose f Identity ~= f
  • Compose Identity g ~= g

Это очень легко доказать, потому что Compose и Identity оба определены как newtype, а отчеты Haskell определяют семантику newtype как изоморфизм между определяемым типом и типом аргумента в конструкторе данных newtype. Например, давайте докажем Compose f Identity ~= f:

Compose f Identity a
    ~= f (Identity a)                 -- newtype Compose f g a = Compose (f (g a))
    ~= f a                            -- newtype Identity a = Identity a
Q.E.D.
78
Luis Casillas

Примечание: Нет, это не так. В какой-то момент был дан комментарий к этому ответу от самого Дана Пипони, в котором говорилось, что причина и следствие здесь были совершенно противоположными, что он написал свою статью в ответ на шутку Джеймса Ири. Но, похоже, он был удален, возможно, каким-то навязчивым тидиром.

Ниже мой оригинальный ответ.


Вполне возможно, что Ири прочитал От моноидов до монад пост, в котором Дэн Пипони (sigfpe) выводит монады из моноидов в Хаскелле, с большим обсуждением теории категорий и явным упоминанием "категории эндофункторов". на Hask ". В любом случае, любой, кто интересуется, что значит монада в категории эндофункторов, может извлечь пользу из прочтения этого вывода.

5
hobbs

Я пришел к этому посту, чтобы лучше понять вывод печально известной цитаты из Мак Лэйна Теория категорий для рабочего математика.

При описании того, что является чем-то, часто одинаково полезно описать, что это не так.

Тот факт, что Mac Lane использует описание для описания монады, можно предположить, что она описывает нечто уникальное для монад. Потерпите меня. Чтобы развить более широкое понимание этого утверждения, я считаю, что необходимо четко указать, что он не описывает что-то уникальное для монад; утверждение одинаково описывает Аппликатив и Стрелки среди других. По той же причине у нас может быть два моноида на Int (Sum и Product), у нас может быть несколько моноидов на X в категории эндофункторов. Но есть еще больше сходства.

И Monad, и Applicative соответствуют критериям:

  • endo => любая стрелка или морфизм, который начинается и заканчивается в одном и том же месте
  • functor => любая стрелка или морфизм между двумя категориями

    (например, изо дня в день Tree a -> List b, но в категории Tree -> List)

  • моноид => один объект; то есть, один тип, но в этом контексте только в отношении внешнего уровня; поэтому мы не можем иметь Tree -> List, только List -> List.

В операторе используется "Категория ..." Это определяет область действия оператора. В качестве примера Категория Functor описывает область действия f * -> g *, то есть Any functor -> Any functor, например, Tree * -> List * или Tree * -> Tree *.

То, что не указано в категориальном утверждении, описывает где все и вся разрешено.

В этом случае внутри функторов не указан * -> * aka a -> b, что означает Anything -> Anything including Anything else. Когда мое воображение переходит к Int -> String, оно также включает Integer -> Maybe Int или даже Maybe Double -> Either String Int, где a :: Maybe Double; b :: Either String Int.

Таким образом, утверждение сводится к следующему:

  • область действия функтора :: f a -> g b (т.е. любой параметризованный тип для любого параметризованного типа)
  • endo + functor :: f a -> f b (т.е. любой один параметризованный тип к одному и тому же параметризованному типу) ... сказал по-другому,
  • моноид в категории эндофунктор

Итак, где же сила этой конструкции? Чтобы оценить всю динамику, мне нужно было увидеть, что типичные рисунки моноида (один объект с чем-то вроде стрелки идентификации, :: single object -> single object) не показывают, что мне разрешено использовать стрелку, параметризованную с помощью любой число значений моноидов из объекта типа one, разрешенного в Monoid. Эндо, ~ определение стрелки идентичности игнорирует функтор значение типа и как тип, так и значение самого внутреннего слоя "полезной нагрузки". Таким образом, эквивалентность возвращает true в любой ситуации, когда совпадают типы функторов (например, Nothing -> Just * -> Nothing эквивалентен Just * -> Just * -> Just *, поскольку оба они Maybe -> Maybe -> Maybe).

Боковая панель: ~ снаружи является концептуальным, но это самый левый символ в f a. Он также описывает, что "Хаскелл" читает первым (большая картинка); так что тип "снаружи" по отношению к значению типа. Взаимосвязь между слоями (цепочкой ссылок) в программировании нелегко установить в категории. Категория набора используется для описания типов (Int, Strings, Maybe Int и т.д.), Которые включают в себя категорию функторов (параметризованные типы). Цепочка ссылок: тип функтора, значения функтора (элементы набора этого функтора, например, Nothing, Just) и, в свою очередь, все остальное, на которое указывает значение каждого функтора. В категории отношение описывается по-разному, например, return :: a -> m a считается естественным преобразованием из одного функтора в другой, в отличие от всего, что упоминалось до сих пор.

Возвращаясь к основному потоку, в общем, для любого определенного тензорного произведения и нейтрального значения, в итоге утверждение описывает удивительно мощную вычислительную конструкцию, порожденную ее парадоксальной структурой:

  • снаружи он выглядит как отдельный объект (например, :: List); статический
  • но внутри, позволяет много динамики
    • любое количество значений того же типа (например, Empty | ~ NonEmpty), что и fodder для функций любой арности. Тензорный продукт уменьшит любое количество входных данных до одного значения ... для внешнего уровня (~ fold, который ничего не говорит о полезной нагрузке)
    • бесконечный диапазон оба тип и значения для самого внутреннего слоя

В Haskell важно уточнить применимость этого утверждения. Мощь и универсальность этой конструкции не имеет абсолютно никакого отношения к монаде per se. Другими словами, конструкция не полагается на то, что делает монаду уникальной.

При попытке выяснить, следует ли создавать код с общим контекстом для поддержки вычислений, которые зависят друг от друга, по сравнению с вычислениями, которые могут выполняться параллельно, это печально известное утверждение, как бы оно ни описывалось, не является контрастом между выбором Аппликатив, Стрелы и Монады, а скорее это описание того, насколько они одинаковы. Для данного решения утверждение является спорным.

Это часто неправильно понимают. Далее в заявлении описывается join :: m (m a) -> m a как тензорное произведение для моноидального эндофунктора. Однако в нем не сформулировано, как в контексте этого утверждения также можно было бы выбрать (<*>). Это действительно пример шести/полдюжины. Логика объединения значений абсолютно одинакова; один и тот же вход генерирует одинаковый выход из каждого (в отличие от моноидов Sum и Product для Int, поскольку они генерируют разные результаты при объединении Ints).

Итак, резюмируем: моноид в категории эндофункторов описывает:

   ~t :: m * -> m * -> m *
   and a neutral value for m *

И (<*>), и (>>=) обеспечивают одновременный доступ к двум значениям m для вычисления единственного возвращаемого значения. Логика, используемая для вычисления возвращаемого значения, точно такая же. Если бы не различные формы функций, которые они параметризуют (f :: a -> b vs. k :: a -> m b) и положение параметра с одинаковым типом возврата вычислений (т. Е. a -> b -> b против b -> a -> b для каждого соответственно), я подозреваю, что мы могли бы параметризовать моноидальная логика, тензорное произведение, для повторного использования в обоих определениях. В качестве упражнения, чтобы понять это, попробуйте реализовать ~t, и в итоге вы получите (<*>) и (>>=) в зависимости от того, как вы решите его определить forall a b.

Если моя последняя точка концептуально верна как минимум, тогда она объясняет точную и единственную вычислительную разницу между Applicative и Monad: функции, которые они параметризуют. Другими словами, разница внешняя в реализации классов этих типов.

В заключение, по моему собственному опыту, печально известная цитата Мак Лэйна предоставила мне замечательный мем "goto", который мне мог бы помочь во время навигации по Category, чтобы лучше понять идиомы, используемые в Haskell. Ему удается захватить всю мощь мощных вычислительных возможностей, которые чудесно доступны в Haskell.

Тем не менее, есть ирония в том, что я впервые неправильно понял применимость заявления за пределами монады, и то, что я надеюсь передать здесь. Все, что он описывает, оказывается тем же, что и между Применимым и Монадой (и Стрелками среди других). Чего не говорится, так это небольшого, но полезного различия между ними.

- E

3
Edmund's Echo