Какова связь между связью и соединением? - PullRequest
7 голосов
/ 31 мая 2019

У меня сложилось впечатление, что (>>=) (используемый Хаскеллом) и join (предпочитаемый математиками) "равны", так как один можно записать в терминах другого:

import Control.Monad (join)

join x = x >>= id
(>>=) x f = join (fmap f x)

Дополнительно каждая монада является функтором , поскольку bind может использоваться для замены fmap:

fmap f x = x >>= (return . f)

У меня есть следующие вопросы:

  1. Существует ли (нерекурсивное) определение fmap в терминах join? (fmap f x = join $ fmap (return . f) x следует из приведенных выше уравнений, но является рекурсивным.)

  2. Является ли "каждая монада функтором", заключение при использовании bind (в определении монады), но предположение при использовании join?

  3. Является ли bind более "мощным", чем join? И что будет означать «более мощный»?

Ответы [ 2 ]

5 голосов
/ 31 мая 2019

Монада может быть или определена в терминах :

  • return :: a -> m a
  • bind :: m a -> (a -> m b) -> m b

или в качестве альтернативы:

  • return :: a -> m a
  • fmap :: (a -> b) -> m a -> m b
  • join :: m (m a) -> m a

На ваши вопросы:

  1. Нет, мы не можем определить fmap в терминах join, так как в противном случае мы могли бы удалить fmap из второго списка выше.
  2. Нет, «каждая монада является функтором» - это утверждение о монадах в целом, независимо от того, определяете ли вы свою конкретную монаду в терминах bind или в терминах join и fmap. Это утверждение легче понять, если вы видите второе определение, но это все.
  3. Да, bind является более "мощным", чем join. Он настолько же «мощный», что и join и fmap вместе взятые, если иметь в виду «мощный», что он способен определять монаду (всегда в сочетании с return).

Интуицию см., Например, этот ответ - bind позволяет объединять или объединять стратегии / планы / вычисления (которые в контексте) вместе. В качестве примера, давайте использовать контекст Maybe (или Maybe монаду):

λ: let plusOne x = Just (x + 1)
λ: Just 3 >>= plusOne
Just 4

fmap также позволяет объединять вычисления в одном контексте, но за счет увеличения вложенности с каждым шагом. [1]

λ: fmap plusOne (Just 3)
Just (Just 4)

Вот почему вам нужно join: разбить два уровня вложенности на один. Помните:

join :: m (m a) -> m a

Наличие только шага раздавливания не очень далеко. Вам также нужно fmap, чтобы иметь монаду - и return, что составляет Just в приведенном выше примере.

[1]: fmap и (>>=) не принимают два аргумента в одном порядке, но не позволяйте этому сбить вас с толку.

3 голосов
/ 01 июня 2019

Есть ли [определение] fmap в терминах join?

Нет, нет. Это можно продемонстрировать, пытаясь это сделать. Предположим, нам дан конструктор произвольного типа T и функции:

returnT :: a -> T a
joinT :: T (T a) -> T a

Исходя из этих данных, мы хотим определить:

fmapT :: (a -> b) -> T a -> T b

Итак, давайте сделаем набросок:

fmapT :: (a -> b) -> T a -> T b
fmapT f ta = tb
    where
    tb = undefined  -- tb :: T b

Нам нужно как-то получить значение типа T b. Сам по себе ta :: T a не подойдет, поэтому нам нужны функции, которые выдают значения T b. Единственными двумя кандидатами являются joinT и returnT. joinT не помогает:

fmapT :: (a -> b) -> T a -> T b
fmapT f ta = joinT ttb
    where
    ttb = undefined  -- ttb :: T (T b)

Это просто пинает банку в будущем, так как в этих обстоятельствах не требуется улучшение T (T b).

Мы могли бы попробовать returnT вместо:

fmapT :: (a -> b) -> T a -> T b
fmapT f ta = returnT b
    where
    b = undefined  -- b :: b

Теперь нам нужно значение b. Единственное, что может дать нам один - это f:

fmapT :: (a -> b) -> T a -> T b
fmapT f ta = returnT (f a)
    where
    a = undefined  -- a :: a

А теперь мы застряли: ничто не может дать нам a. Мы исчерпали все доступные возможности, поэтому fmapT не может быть определено в таких терминах.

Отступление: недостаточно обмануть, используя такую ​​функцию:

extractT :: T a -> a

С extractT мы можем попробовать a = extractT ta, что приведет к:

fmapT :: (a -> b) -> T a -> T b
fmapT f ta = returnT (f (extractT ta))

Однако для fmapT недостаточно иметь правильный тип: он также должен следовать законам функторов. В частности, fmapT id = id должно держаться. С этим определением fmapT id равно returnT . extractT, что, в общем, не равно id (большинство функторов, которые являются экземплярами как Monad, так и Comonad, служат примерами).


Является ли "каждая монада функтором", заключение при использовании bind (в определении монады), но предположение при использовании join?

«Каждая монада является функтором» - это предположение или, точнее, часть определения монады. Чтобы выбрать произвольную иллюстрацию, вот Эмили Рил, Теория категорий в контексте , с. 154

Определение 5.1.1. монада по категории C состоит из

  • эндофунктор T : C → C,

  • a единица естественное преобразование η : 1 C T и

  • a умножение естественное преобразование μ : T 2 T ,

, так что следующие диаграммы коммутируют в C C : [диаграммы законов монады]

Следовательно, монада включает в себя эндофунктор по определению. Для конструктора типа Haskell T, который создает экземпляр Monad, объектным отображением этого endofunctor является сам T, а отображением морфизма является его fmap. То, что T будет экземпляром Functor и, следовательно, будет иметь fmap, в современном Haskell гарантировано, что Applicative (и, соответственно, Functor) является суперклассом Monad.

Это вся история? Что касается Хаскелла. мы знаем, что liftM существует, а также в недалеком прошлом Functor не был суперклассом Monad. Эти два факта - просто Хаскеллизмы? Не совсем. В классической работе Понятия вычисления и монады Эугенио Могги находит следующее определение (стр. 3):

Определение 1.2 ([Man76]) A Тройной Клейсли над категорией C является тройной (T, η, _ *) , где T : Obj ( C ) → Obj ( C ), η A : A → TA для A ∈ Obj ( C ), f *: TA → TB для f: A → TB и выполняются следующие уравнения:

  • η A * = id T A
  • η A ; f * = f для f: A → T B
  • е *; g * = (f; g *) * для f: A → T B и g: B → T C

Важная деталь чДело в том, что T представлен просто как отображение объекта в категории C , а не как эндофунктор в C .Работая в категории Hask , это равносильно принятию конструктора типа T без предположения, что это экземпляр Functor.В коде мы могли бы написать это как:

class KleisliTriple t where
    return :: a -> t a
    (=<<) :: (a -> t b) -> t a -> t b

-- (return =<<) = id
-- (f =<<) . return = f
-- (g =<<) . (f =<<) = ((g =<<) . f =<<)

Отразить привязку в стороне, то есть определение до 12000 * AMP в Haskell.Неудивительно, что статья Могги не заставляет себя долго ждать, чтобы показать, что «существует троекратное соответствие между тройками Клейсли и монадами» (стр. 5), установив, что T может быть расширен доendofunctor (в Haskell этот шаг сводится к определению отображения морфизма liftM f m = return . f =<< m и последующему его отображению в соответствии с законами функтора).

В общем, если вы напишите законные определения return и (>>=) не предполагая fmap, вы действительно получите законную реализацию Functor как следствие.«Существует однозначное соответствие между тройками Клейсли и монадами» является следствием определения тройки Клейсли, в то время как «монада включает эндофунктор» является частью определения монады.Соблазнительно подумать, будет ли более точным описывать то, что Хаскеллерс делал при написании Monad экземпляров, как «создание тройки Клесили», а не «создание монады», но я воздержусь от страха быть увязшим в уныниитерминологическая педантичность - и в любом случае, теперь, когда Functor является суперклассом Monad, нет никаких практических причин для беспокойства по этому поводу.


bind более "мощный", чем join?И что будет означать «более сильный»?

Вопрос с подвохом!

Если принять за чистую монету, ответ будет положительным, если вместе с return, (>>=) позволяет реализовать fmap (через liftM, как отмечено выше), а join - нет.Однако я не чувствую, что стоит настаивать на этом различии.Почему так?Из-за законов монады.Точно так же, как не имеет смысла говорить о законных (>>=) без предположения return, не имеет смысла говорить о законных join без давления return и fmap.

Может сложиться впечатление, что я придаю слишком большое значение законам, используя их, чтобы связать Monad и Functor таким образом.Это правда, что есть случаи законов, которые включают два класса, и это применяется только к типам, которые создают их оба.Foldable предоставляет хороший пример этого: мы можем найти следующий закон в документации Traversable :

Экземпляры суперкласса должны удовлетворять следующему: [...]

В случае Foldable, foldMap должен быть эквивалентен обходу с постоянным аппликативным функтором (foldMapDefault).

То, что этот конкретный закон не всегдаприменить это не проблема, потому что нам не нужно, чтобы характеризовать, что такое Foldable (альтернативы включают «Foldable - это контейнер, из которого мы можем извлечь некоторую последовательность элементов», а «Foldable - этоконтейнер, который можно преобразовать в свободный моноид по типу элемента ").Однако с законами монады это не так: значение класса неразрывно связано со всеми тремя законами монады.

...