Присоединенные функторы определяют преобразователи монад, но где лифт?

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

{-# LANGUAGE MultiParamTypeClasses #-}

import           Control.Monad

newtype Three g f m a = Three { getThree :: g (m (f a)) }

class (Functor f, Functor g) => Adjoint f g where
  counit :: f (g a) -> a
  unit   :: a -> g (f a)

instance (Adjoint f g, Monad m) => Monad (Three g f m) where
  return  = Three . fmap return . unit
  m >>= f = Three $ fmap (>>= counit . fmap (getThree . f)) (getThree m)

instance (Adjoint f g, Monad m) => Applicative (Three g f m) where
  pure  = return
  (<*>) = ap

instance (Adjoint f g, Monad m) => Functor (Three g f m) where
  fmap = (<*>) . pure

Учитывая, что Adjoint ((,) s) ((->) s), Three ((->) s) ((,) s) эквивалентен StateT s.

Очень круто, но меня озадачивает пара вещей:

  • Как мы можем улучшить монадический m a до монадического Three g f m a? Для конкретного случая Three ((->) s) ((,) s), конечно, очевидно, как это сделать, но кажется желательным иметь рецепт, который работает для любого Three g f при условии, что Adjoint f g. Другими словами, похоже, что должен быть аналог lift, определение которого требует только unit, counit и return и >>= входной монады. Но я не могу найти его (я видел определение с использованием sequence, но это немного похоже на мошенничество, поскольку требует, чтобы f было Traversable).

  • В таком случае, как мы можем обновить g a до Three g f m a (предоставляется Adjoint f g)? Опять же, для конкретного случая Three ((->) s) ((,) s) очевидно, как это сделать, но мне интересно, есть ли аналог gets, который требует только unit, counit и return и >>= входной монады.


person Simon C    schedule 16.03.2018    source источник
comment
Примечание: Ваша (>>=) реализация может быть упрощена до m >>= f = Three $ fmap (>>= counit . fmap (getThree . f)) (getThree m)   -  person duplode    schedule 17.03.2018
comment
Я написал продолжение этого вопроса, который изначально планировался как ответ здесь, но стал слишком большим. Спасибо, что привлекли внимание к этой теме!   -  person duplode    schedule 23.06.2019


Ответы (2)


lift в ответ Бенджамина Ходжсона настроен как:

lift mx = let mgfx = fmap unit mx
              gmfx = distributeR mgfx
          in Three gmfx
-- or
lift = Three . distributeR . fmap unit

Как вы знаете, это не единственная правдоподобная стратегия, которую мы можем здесь использовать:

lift mx = let gfmx = unit mx
              gmfx = fmap sequenceL gfmx
          in Three gmfx
-- or
lift = Three . fmap sequenceL . unit

Отсюда требование Traversable для Эдварда Соответствующий MonadTrans экземпляр Кметта исходит из. Тогда возникает вопрос, является ли полагаться на это, как вы выразились, «обманом». Я собираюсь возразить, что это не так.

Мы можем адаптировать план игры Бенджамина относительно Distributive и правого сопряжения и попытаться определить, являются ли левые сопряжения Traversable. Взгляд на Data.Functor.Adjunction показывает, что у нас есть неплохой набор инструментов для работы с:

unabsurdL :: Adjunction f u => f Void -> Void
cozipL :: Adjunction f u => f (Either a b) -> Either (f a) (f b)
splitL :: Adjunction f u => f a -> (a, f ())
unsplitL :: Functor f => a -> f () -> f a

Эдвард услужливо сообщает нам, что unabsurdL и cozipL свидетельствуют о том, что «[а] левый сопряженный должен быть заселен, [и что] левый сопряженный должен быть заселен ровно одним элементом», соответственно. Это, однако, означает, что splitL точно соответствует разложению по форме и содержанию, которое характеризует Traversable функторы. Если мы добавим к этому тот факт, что splitL и unsplitL инвертированы, немедленно следует реализация sequence:

sequenceL :: (Adjunction f u, Functor m) => f (m a) -> m (f a)
sequenceL = (\(mx, fu) -> fmap (\x -> unsplitL x fu) mx) . splitL

(Обратите внимание, что от m требуется не более Functor, как ожидается для перемещаемых контейнеров, которые содержат ровно одно значение.)

Все, что здесь отсутствует, - это проверка эквивалентности обеих реализаций lift. Это несложно, только немного трудоемко. Вкратце, определения distributeR и sequenceR здесь можно упростить до:

distributeR = \mgx ->
    leftAdjunct (\fa -> fmap (\gx -> rightAdjunct (const gx) fa) mgx) ()   
sequenceL =
    rightAdjunct (\mx -> leftAdjunct (\fu -> fmap (\x -> fmap (const x) fu) mx) ())

Мы хотим показать, что distributeR . fmap unit = fmap sequenceL . unit. Пройдя еще несколько раундов упрощений, мы получим:

distributeR . fmap unit = \mgx ->
    leftAdjunct (\fa -> fmap (\gx -> rightAdjunct (const (unit gx)) fa) mgx) ()
fmap sequenceL . unit = \mx ->
    leftAdjunct (\fu -> fmap (\x -> fmap (const x) fu) mx) ()

Мы можем показать, что это действительно одно и то же, выбрав \fu -> fmap (\x -> fmap (const x) fu) mx - аргумент для leftAdjunct во второй правой части - и вставив rightAdjunct unit = counit . fmap unit = id в него:

\fu -> fmap (\x -> fmap (const x) fu) mx
\fu -> fmap (\x -> fmap (const x) fu) mx
\fu -> fmap (\x -> (counit . fmap unit . fmap (const x)) fu) mx
\fu -> fmap (\x -> rightAdjunct (unit . const x) fu) mx
\fu -> fmap (\x -> rightAdjunct (const (unit x)) fu) mx
-- Sans variable renaming, the same as
-- \fa -> fmap (\gx -> rightAdjunct (const (unit gx)) fa) mgx

Вывод состоит в том, что путь Traversable к вашему MonadTrans так же безопасен, как и маршрут Distributive, и опасения по поводу него, в том числе упомянутые в документации Control.Monad.Trans.Adjoint, больше никого не должны беспокоить.

P.S .: Стоит отметить, что приведенное здесь определение lift может быть записано как:

lift = Three . leftAdjunct sequenceL

То есть lift sequenceL передается через изоморфизм присоединения. Дополнительно от ...

leftAdjunct sequenceL = distributeR . fmap unit

... если применить rightAdjunct с обеих сторон, мы получим ...

sequenceL = rightAdjunct (distributeR . fmap unit)

... и если мы составим fmap (fmap counit) слева от обеих сторон, мы в конечном итоге получим:

distributeR = leftAdjunct (fmap counit . sequenceL)

Итак, distributeR и sequenceL взаимоопределимы.

person duplode    schedule 17.03.2018
comment
Хороший ответ! Не думал о том, чтобы сделать это так - person Benjamin Hodgson♦; 17.03.2018
comment
@BenjaminHodgson Между прочим, я не думаю, что узнал бы здесь разложение формы и содержания, если бы не прочитал тему, которую вы написали об этом в документации SO - это побудило меня потратить некоторое время на размышления об этом вопросе. тогда. - person duplode; 18.03.2018
comment
Значит, Документы не были полностью бесполезными ???? - person Benjamin Hodgson♦; 18.03.2018

Как мы можем улучшить монадический m a на монадический Three g f m a?

Хороший вопрос. Пора поиграть в теннис!

-- i'm using Adjuction from the adjunctions package because I'll need the fundeps soon
lift :: Adjunction f g => m a -> Three g f m a
lift mx = Three _

Отверстие набрано g (m (f a)). У нас есть mx :: m a в объеме, и, конечно же, unit :: a -> g (f a) и fmap :: (a -> b) -> m a -> m b.

lift mx = let mgfx = fmap unit mx
          in Three $ _ mgfx

Теперь это _ :: m (g (f a)) -> g (m (f a)). Это distribute, если g - это Distributive.

lift mx = let mgfx = fmap unit mx
              gmfx = distributeR mgfx
          in Three gmfx
-- or
lift = Three . distributeR . fmap unit

Итак, теперь нам просто нужно доказать, что правая часть присоединения всегда Distributive:

distributeR :: (Functor m, Adjunction f g) => m (g x) -> g (m x)
distributeR mgx = _

Поскольку нам нужно вернуть g, очевидным выбором методов из Adjunction является _ 18_, который использует unit для создания g (f a), а затем разрушает внутренний f a, fmap наложив функцию.

distributeR mgx = leftAdjunct (\fa -> _) _

Я собираюсь сначала атаковать первую дыру, ожидая, что ее заполнение может кое-что сказать мне о второй. Первое отверстие имеет тип m a. Единственный способ получить m любого типа - это fmap написать что-то поверх mgx.

distributeR mgx = leftAdjunct (\fa -> fmap (\gx -> _) mgx) _

Теперь первая дыра имеет тип a, а у нас gx :: g a в области видимости. Если бы у нас был f (g a), мы могли бы использовать counit. Но у нас есть f x (где x в настоящее время - переменная неоднозначного типа) и g a в области видимости.

distributeR mgx = leftAdjunct (\fa -> fmap (\gx -> counit (fa $> gx)) mgx) _

Оказывается, оставшаяся дыра имеет неоднозначный тип, поэтому мы можем использовать все, что захотим. (Это будет проигнорировано $>.)

distributeR mgx = leftAdjunct (\fa -> fmap (\gx -> counit (fa $> gx)) mgx) ()

Этот вывод мог показаться волшебным трюком, но на самом деле вы просто станете лучше печатать теннис с практикой. Навык игры заключается в способности смотреть на типы и применять интуицию и факты об объектах, с которыми вы работаете. Глядя на типы, я мог сказать, что мне нужно будет поменять m и g, а обход m не был вариантом (потому что m не обязательно Traversable), поэтому что-то вроде distribute было необходимо.

Помимо предположения, что мне нужно будет реализовать distribute, я руководствовался некоторыми общими знаниями о том, как работают адъюнкции.

В частности, когда вы говорите о * -> *, единственными интересными дополнениями являются (однозначно изоморфные) присоединение _47 _ / _ 48_. В частности, это означает, что любое правое присоединение к Hask всегда _ 50_, о чем свидетельствует _ 51_ и _ 52_. Я также знаю, что все Representable функторы Distributive (на самом деле логически верно и обратное, как описано в _ 55_ документы, хотя классы не эквивалентны по мощности), за _ 56_.


В таком случае, как мы можем обновить g a до Three g f m a (предоставляется Adjoint f g)?

Я оставлю это как упражнение. Я подозреваю, что вам придется снова опираться на изоморфизм g ~ ((->) s). На самом деле я не ожидаю, что это будет верным для всех дополнений, только тех, что на Hask, из которых только одно.

person Benjamin Hodgson♦    schedule 16.03.2018
comment
Спасибо, я дошел до этапа «необходимость распространения», но я знаю о Distributive вещах меньше, чем Traversables, и поэтому застрял там. In re: Что заставляет вас думать, что это возможно? Что ж, если единственные интересные правые сопряжения в Hask изоморфны Reader, и Monad m => g a -> Three g f m a легко охарактеризовать с помощью Reader-y g, вы можете ожидать, что это возможно. Или я запуталась? - person Simon C; 16.03.2018
comment
@SimonC Да, я думаю, что на самом деле вы правы, я действительно удалял это предложение, пока вы публиковали этот комментарий :). Я ожидаю, что это будет несложно реализовать, когда вы освоите tabulateAdjunction и indexAdjunction. - person Benjamin Hodgson♦; 16.03.2018
comment
Спасибо. Это делает мой второй вопрос SO, в котором вы любезно указали мне на Distributive! :) Для ясности: вы ожидаете, что рецепт lift будет работать для всех дополнений или только для одного на Hask? - person Simon C; 16.03.2018
comment
Кто-то вроде Эда Кметта был бы в гораздо лучшем положении, чтобы авторитетно ответить на этот вопрос, чем я. Моя интуиция выглядит примерно так: учитывая, что каждая монада генерируется присоединением (просто не обязательно присоединением в Hask), меня ничуть не удивило бы, если бы каждая монада преобразователь также была сгенерирована пристройка. - person Benjamin Hodgson♦; 16.03.2018
comment
На самом деле это кажется правдой, см. stackoverflow.com/questions/24515876/ - person Benjamin Hodgson♦; 16.03.2018
comment
Я согласен, это кажется правдой. Это не совсем доказательство, но тот факт, что return и >>= для Three g f m построены с использованием только fmaps, unit, counit и _7 _ / _ 8_ (ни один из которых в принципе не является специфичным для Hask) заставил меня подумать, что конструкция трансформатора была полностью Общее. Для меня было менее ясно, что существование lift рецепта должно быть, поскольку для меня не было очевидно, что способность преобразовывать монадический m в монадический t m абсолютно подразумевает существование lift рецепта, когда сопряженные функторы не жить полностью на Hask. - person Simon C; 16.03.2018
comment
Для меня главный вопрос - как мне найти преобразователь монад для данной базовой монады. Я думаю, что конструкция с Adjoint f g не будет работать в целом, потому что мы не сможем найти никаких сопряженных Hask эндофункторов f, g таких, что их состав равен данной монаде. Пример монады, для которой этот преобразователь монад (я считаю) недоступен: type L z a = Either a (z -> a), где z - фиксированный тип. return= Left и bind могут быть определены, законы остаются в силе, но преобразователь монад для L (я полагаю) не может быть найден с помощью сопряженного рецепта. - person winitzki; 22.01.2019
comment
@winitzki Я написал продолжение этого вопроса, в котором делается попытка решить более общую проблему. Что касается вашего типа, в некотором смысле это зависит от того, что вы хотите от трансформатора. С одной стороны, привязать эффекты к обеим сторонам Either действительно сложно; с другой стороны. newtype LM z a = Ready a | Waiting (r -> m a) - сам по себе отличный трансформатор. - person duplode; 23.06.2019
comment
@duplode Спасибо за интересное продолжение. Я был занят поиском преобразователя монад для построения монады со свободными точками над данной монадой g: я определяю type L g a = Either a (g a), и я проверял, что L g является законной монадой, если g таковой. Если t m является преобразователем монад для g (с иностранной монадой m), тогда я думаю, что g (Either a (t m a)) является преобразователем монад для L. Я не закончил расчеты, но это кажется многообещающим. Ваш LM соответствует Either a (t m a), но это нарушает законы трансформатора (m a -> Either a (t m a) не сохраняет монаду). - person winitzki; 25.06.2019
comment
@winitzki Хорошая уловка о return законе о трансформаторах для LM! Интересно, что было обсуждение аналогичной ситуации в каналах средство отслеживания проблем некоторое время назад. - person duplode; 25.06.2019
comment
Обратите внимание, что g (Either a (t m a)) оборачивает ваш LM дополнительным слоем g. Точно так же правильные преобразователи ListT и FreeT должны добавлять дополнительный слой монад снаружи. Мне было бы интересно узнать, следует ли это каким-либо образом из какой-либо конструкции с добавками. - person winitzki; 25.06.2019
comment
@duplode Я систематически искал монады, трансформер которых составляет за пределами чужой монады. Монада Reader является одним из примеров, но я нашел более широкий класс монад с этим свойством, которые я называю жесткими монадами. В общем, они не Distributive. Например, если p - фиксированный тип, а R определяется type R a = (a -> p) -> a, то R - жесткая монада, преобразователь которой работает с чужой монадой m как t m a = (m a -> p) -> m a. См. stackoverflow.com/questions/39649497/ для моего сообщения о жестких функторах. - person winitzki; 26.06.2019
comment
@winitzki Ой, здорово! Хотя я прочитал этот пост три года назад (как говорят мне маркеры голосов), тогда я недостаточно хорошо понимал проблему, чтобы полностью оценить последствия. Это понятие жесткости используется в некоторых экспериментах, которые я проводил в последнее время с Distributive, хотя мне не удалось идентифицировать что-либо подобное еще одному шагу в иерархии классов. Я подумаю об этом еще немного, а потом прокомментирую ваш вопрос. - person duplode; 26.06.2019