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

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

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

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

5 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Monoid

Моноид - это совокупность объектов и метод их объединения. Хорошо известны моноиды:

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

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

Кроме того, у каждого моноида есть идентичность, то есть тот элемент "без операции", который не действует, когда вы комбинируете его с чем-то другим:

  • 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-(3-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, сохраняя оба результата, затем сделайте все снова для обоих результатов, а затем оберните окончательный вариант результаты в списке.)

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

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

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

Заметки

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

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

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

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

{-# LANGUAGE RankNTypes, TypeOperators #-}

import Control.Monad (join)

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

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

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

  • Эндофунктор, T: X -> X
  • Естественное преобразование, μ: T × T -> T, где × означает композицию функтора.
  • Естественное преобразование η: I -> T, где I - единичный эндофунктор на 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, Один интуитивный способ, которым я часто пользуюсь, чтобы думать об этом, заключается в следующем:

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

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

Первый пункт - это "endofunctor, T: X -> X ". Ну каждый Functor в Haskell является endofunctor в том, что люди называют "категория Hask", чьи объекты являются типами 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)

Утверждение, что это моноид в категории эндофункторов, означает, что 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.

Ответы здесь отлично справляются с определением как моноидов, так и монад, однако, похоже, они все еще не дают ответа на вопрос:

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

Суть вопроса, которого здесь не хватает, - это другое понятие "моноид", точнее так называемая категоризация - понятие моноида в моноидальной категории. К сожалению, сама книга Мак Лейна очень сбивает с толку:

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

Основная путаница

Почему это сбивает с толку? Поскольку он не определяет, что такое "моноид в категории эндофункторов"X. Вместо этого в этом предложении предлагается взять моноид внутри набора всех эндофункторов вместе с композицией функторов как бинарной операцией и тождественным функтором как моноидальной единицей. Что отлично работает и превращает в моноид любое подмножество эндофункторов, которое содержит тождественный функтор и закрыто при композиции функторов.

Тем не менее, это неправильная интерпретация, которую книга не может прояснить на данном этапе. Монадаf- это фиксированный эндофунктор, а не подмножество эндофункторов, замкнутое по композиции. Обычная конструкция - использоватьfчтобы сгенерировать моноид, взяв набор всехk-складные композиции f^k = f(f(...)) из f с собой, в том числе k=0 что соответствует идентичности f^0 = id. А теперь наборS всех этих сил для всех k>=0 действительно является моноидом, "в котором произведение × заменено композицией эндофункторов и единицей, установленной тождественным эндофунктором".

И все еще:

  • Этот моноид S можно определить для любого функтора f или даже буквально для любой собственной карты X. Это моноид, порожденныйf.
  • Моноидальная структура S заданный композицией функторов, и тождественный функтор не имеет ничего общего с f быть или не быть монадой.

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

(Строгие) моноидальные категории

Переходя к главе VII о моноидах (которая идет после главы VI о монадах), мы находим определение так называемой строгой моноидальной категории как тройной(B, *, e), где B это категория, *: B x B-> Bбифунктор (функтор по отношению к каждому компоненту с другим компонентом фиксированной) иe это единичный объект в B, удовлетворяющие законам ассоциативности и единицы:

(a * b) * c = a * (b * c)
a * e = e * a = a

для любых объектов a,b,c из B, и те же тождества для любых морфизмов a,b,c с e заменен на id_e, тождественный морфизм e. Теперь поучительно заметить, что в нашем интересном случае, гдеB это категория эндофункторов X с естественными преобразованиями как морфизмы, * состав функтора и e тождественного функтора, все эти законы выполняются, что можно непосредственно проверить.

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

Моноиды в моноидальных категориях

Наконец, в разделе 3 "Моноиды" главы VII дано собственное определение:

Моноид c в моноидальной категории (B, *, e) является объектом B с двумя стрелками (морфизмами)

mu: c * c -> c
nu: e -> c

делая 3 диаграммы коммутативными. Напомним, что в нашем случае это морфизмы в категории эндофункторов, которые являются естественными преобразованиями, соответствующими в точностиjoin а также returnдля монады. Связь становится еще яснее, когда мы делаем композицию* более явный, заменяющий c * c по c^2, где c это наша монада.

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

Вывод

Таким образом, любая монада по определению является эндофунктором, следовательно, объектом категории эндофункторов, где монадическая join а также returnоператоры удовлетворяют определению моноида в этой конкретной (строгой) моноидальной категории. Наоборот, любой моноид в моноидальной категории эндофункторов по определению является тройкой(c, mu, nu) состоящий из объекта и двух стрелок, например естественных преобразований в нашем случае, удовлетворяющих тем же законам, что и монада.

Наконец, обратите внимание на ключевое различие между (классическими) моноидами и более общими моноидами в моноидальных категориях. Две стрелкиmu а также nuвыше больше не являются двоичной операцией и единицей в наборе. Вместо этого у вас есть один фиксированный эндофункторc. Композиция функтора* и сам по себе функтор тождества не обеспечивает полной структуры, необходимой для монады, несмотря на это сбивающее с толку замечание в книге.

Другой подход - сравнить со стандартным моноидом. C всех собственных карт набора A, где двоичная операция - это композиция, которую можно увидеть для отображения стандартного декартова произведения C x C в C. Переходя к категорированному моноиду, мы заменяем декартово произведениеx с композицией функтора *, а бинарная операция заменяется естественным преобразованием mu от c * c к c, это коллекция join операторы

join: c(c(T))->c(T)

для каждого объекта T(введите программирование). А элементы идентичности в классических моноидах, которые можно отождествить с изображениями карт из фиксированного одноточечного набора, заменяются наборомreturn операторы

return: T->c(T) 

Но теперь декартовых произведений больше нет, поэтому нет пар элементов и, следовательно, нет бинарных операций.

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

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

Тот факт, что 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 *,

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

В этом случае внутри функторов * -> * ака 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 (т. е. любой параметризованный тип для любого параметризованного типа)
  • эндо + функтор :: f a -> f b (т. е. любой один параметризованный тип к тому же параметризованному типу) ... сказал по-другому,
  • моноид в категории эндофунктор

Итак, где же сила этой конструкции? Чтобы оценить всю динамику, мне нужно было увидеть, что типичные рисунки моноида (единичный объект с чем-то похожим на стрелку, :: single object -> single object), не иллюстрирует, что мне разрешено использовать стрелку, параметризованную любым количеством моноидных значений, из объекта одного типа, разрешенного в Monoid. Определение эквивалентности стрелки endo, ~ identity игнорирует значение типа функтора, а также тип и значение самого внутреннего слоя "полезной нагрузки". Таким образом, эквивалентность возвращается 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 важно уточнить применимость этого утверждения. Мощь и универсальность этой конструкции не имеет абсолютно никакого отношения к монаде как таковой. Другими словами, конструкция не полагается на то, что делает монаду уникальной.

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

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

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

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

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

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

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

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

- E

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

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


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

Другие вопросы по тегам