Моноидальные функторы Лакса с другой моноидальной структурой

Аппликативные функторы хорошо известны и любимы среди хаскеллеров за их способность применять функции в эффективном контексте.

В терминах теории категорий можно показать, что методы Applicative:

pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b

эквивалентны наличию Functor f с операциями:

unit :: f ()
(**) :: (f a, f b) -> f (a,b)

идея состоит в том, что для записи pure вы просто заменяете () в unit заданным значением, а для записи (<*>) вы сжимаете функцию и аргумент в кортеж, а затем сопоставляете с ним подходящую функцию приложения.

Более того, это соответствие превращает Applicative законы в естественные моноидальные законы относительно unit и (**), так что на самом деле аппликативный функтор - это именно то, что теоретик категорий назвал бы слабым моноидальным функтором (слабым, потому что (**) - просто естественное преобразование, а не изоморфизм ).

Ладно, отлично. Это хорошо известно. Но это только одно семейство слабых моноидальных функторов - тех, которые соблюдают моноидальную структуру продукта. Слабый моноидальный функтор включает в себя два варианта моноидальной структуры в источнике и назначении: вот что вы получите, если превратите произведение в сумму:

class PtS f where
  unit :: f Void
  (**) :: f a -> f b -> f (Either a b)

-- some example instances
instance PtS Maybe where
  unit = Nothing
  Nothing ** Nothing = Nothing
  Just a ** Nothing = Just (Left a)
  Nothing ** Just b = Just (Right b)
  Just a ** Just b = Just (Left a) -- ick, but it does satisfy the laws

instance PtS [] where
  unit = []
  xs ** ys = map Left xs ++ map Right ys

Похоже, что преобразование суммы в другие моноидальные структуры становится менее интересным из-за того, что unit :: Void -> f Void определяется однозначно, так что вы действительно имеете дело с большей полугруппой. Но все равно:

  • Являются ли другие нестрогие моноидальные функторы изученными или полезными?
  • Есть ли для них отличная альтернативная презентация, такая как Applicative?

person Ben Millwood    schedule 26.04.2014    source источник
comment
Когда вы говорите Void в типе PtS.unit, разве вы не имеете в виду Пустой, поскольку это должна быть единица для Either?   -  person Dominique Devriese    schedule 27.04.2014
comment
Неважно, вы, вероятно, намеревались Void представлять пустой тип. Я был сбит с толку, потому что имя void в C-подобных языках соответствует типу модуля, который вы пишете как ().   -  person Dominique Devriese    schedule 27.04.2014
comment
Да, извините за путаницу, но для этого есть разумный прецедент: hackage.haskell.org/package/void < / а>   -  person Ben Millwood    schedule 27.04.2014
comment
Ack, возможно, C использует void неправильно, а не ваше;)   -  person Dominique Devriese    schedule 27.04.2014


Ответы (3)


"Изящное альтернативное представление" для Applicative основано на следующих двух эквивалентностях

pure a = fmap (const a) unit
unit = pure ()

ff <*> fa = fmap (\(f,a) -> f a) $ ff ** fa
fa ** fb = pure (,) <*> fa <*> fb

Уловка для получения этого «аккуратного альтернативного представления» для Applicative такая же, как и уловка для zipWith - заменить явные типы и конструкторы в интерфейсе вещами, в которые можно передать тип или конструктор, чтобы восстановить исходный интерфейс.

unit :: f ()

Заменяется на pure, в который мы можем подставить тип () и конструктор () :: () для восстановления unit.

pure :: a  -> f a
pure    () :: f ()

И аналогично (хотя и не так просто) для замены типа (a,b) и конструктора (,) :: a -> b -> (a,b) на liftA2 для восстановления **.

liftA2 :: (a -> b -> c) -> f a -> f b -> f c
liftA2    (,)           :: f a -> f b -> f (a,b)

Затем Applicative получает красивый оператор <*>, помещая приложение-функцию ($) :: (a -> b) -> a -> b в функтор.

(<*>) :: f (a -> b) -> f a -> f b
(<*>) = liftA2 ($)

Чтобы найти "аккуратную альтернативную презентацию" для PtS, нам нужно найти

  • что-то мы можем заменить тип Void на, чтобы восстановить unit
  • что-то, что мы можем заменить тип Either a b и конструкторы Left :: a -> Either a b и Right :: b -> Either a b, чтобы восстановить **

(Если вы заметили, что у нас уже есть что-то, что конструкторы Left и Right могут быть переданы, вы, вероятно, сможете понять, чем мы можем заменить **, не выполняя шаги, которые я использовал; я не заметил этого, пока не решил это)

единица измерения

Это сразу дает нам альтернативу unit для сумм:

empty :: f a
empty = fmap absurd unit

unit :: f Void
unit = empty

оператор

Мы хотим найти альтернативу (**). Существует альтернатива суммам типа Either, которая позволяет записывать их как функции продуктов. Он проявляется как шаблон посетителя в объектно-ориентированных языках программирования, где суммы не существуют.

data Either a b = Left a | Right b

{-# LANGUAGE RankNTypes #-}
type Sum a b = forall c. (a -> c) -> (b -> c) -> c

Это то, что вы получите, если измените порядок аргументов either и частично примените их.

either :: (a -> c) -> (b -> c) -> Either a b -> c

toSum :: Either a b -> Sum a b
toSum e = \forA forB -> either forA forB e

toEither :: Sum a b -> Either a b
toEither s = s Left Right

Мы видим, что Either a b ≅ Sum a b. Это позволяет нам переписать тип для (**)

(**) :: f a -> f b -> f (Either a b)
(**) :: f a -> f b -> f (Sum a b)
(**) :: f a -> f b -> f ((a -> c) -> (b -> c) -> c)

Теперь понятно, что делает **. Он задерживает fmap привязку чего-либо к обоим своим аргументам и объединяет результаты этих двух сопоставлений. Если мы введем новый оператор <||> :: f c -> f c -> f c, который просто предполагает, что fmaping уже был выполнен, то мы увидим, что

fmap (\f -> f forA forB) (fa ** fb) = fmap forA fa <||> fmap forB fb

Или вернемся к Either:

fa ** fb = fmap Left fa <||> fmap Right fb
fa1 <||> fa2 = fmap (either id id) $ fa1 ** fa2

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

class Functor f => AlmostAlternative f where
    empty  :: f a
    (<||>) :: f a -> f a -> f a

Это почти наверняка то же самое, что и класс Alternative, за исключением того, что мы не требовали, чтобы Functor был Applicative.

Вывод

Это просто Functor, Monoid для всех типов. Это было бы эквивалентно следующему:

class (Functor f, forall a. Monoid (f a)) => MonoidalFunctor f

Ограничение forall a. Monoid (f a) - это псевдокод; Я не знаю способа выразить подобные ограничения в Haskell.

person Cirdec    schedule 27.04.2014
comment
Идеально! Теперь раздражен, что я сам этого не заметил: P (и, черт возьми, вы дали мне нетривиальный выбор, какой ответ принять) - person Ben Millwood; 27.04.2014
comment
(Небольшой пункт: вы описываете суммы как аналог функций продуктов, вы имеете в виду продукты функций?) - person Ben Millwood; 27.04.2014
comment
Нет, я имею в виду не функции продуктов. Сумма - это функция, которая принимает произведение двух функций. Вы получаете данные из суммы, передавая две функции (произведение двух функций) - одна для того, что делать для первого варианта, другая для того, что делать со вторым. Если сумма является первым вариантом, она берет первую функцию от продукта, применяет ее к тому, что содержится в сумме, и возвращает результат. Если сумма является вторым вариантом, она берет вторую функцию от продукта, применяет ее к тому, что содержится в сумме, и возвращает результат. Сумма - это функция, которая принимает произведение функций. - person Cirdec; 27.04.2014
comment
О, я понимаю, что вы имеете в виду, да. - person Ben Millwood; 27.04.2014
comment
Мне было бы интересно посмотреть, можете ли вы продолжить этот аргумент, чтобы поговорить о правильной семантике для Альтернатив / Seminearrings. - person J. Abrahamson; 29.04.2014
comment
@JAbrahamson Я не уверен, что вы имеете в виду / что ищете. Почти полукольцо в структуре функтора не будет иметь pure (поскольку почти полукольцо не обязательно имеет 1), поэтому мы, вероятно, не могли бы добиться большего, чем <**> из примера Applicative в моем ответе, который эквивалентно liftA2 из Applicative. Одностороннее распределение и поглощение для почти полукольца - это просто законы. - person Cirdec; 29.04.2014
comment
@JAbrahamson В полугруппе продукта все еще может быть оператор применения <*>. Вы можете восстановить исходный продукт, используя fa ** fb = fmap (,) fa <*> fb вместо fa ** fb = pure (,) <*> fa <*> fb при отсутствии pure. Я предлагаю продолжить разговор о принадлежности к ближнему полукольцу в другом вопросе или в чате. Возможно, вы спросите, какой должна быть правильная семантика для альтернатив / почти полуколец, а также что вы подразумеваете под правильной семантикой? - person Cirdec; 29.04.2014
comment
Теперь можно выразить forall a. Monoid (f a) в Haskell с помощью -XQuantifiedConstraints - person Iceland_jack; 02.10.2019

Прежде чем говорить о моноидальных функторах, вам необходимо убедиться, что вы находитесь в моноидальной категории. Так получилось, что Hask является моноидальной категорией следующим образом:

  • () как личность
  • (,) как бифунктор
  • Определите изоморфные типы, то есть (a,()) ≅ ((),a) ≅ a и (a,(b,c)) ≅ ((a,b),c).

Как вы заметили, это также моноидальная категория, когда вы обмениваете () на Void и (,) на Either.
Однако моноидальная категория не уведет вас очень далеко - Hask настолько мощен, что он декартово закрыто. Это дает нам каррирование и связанные с ним техники, без которых аппликатив был бы бесполезен.

Моноидальная категория может быть декартово замкнутой, если ее идентичность - терминальный объект, то есть тип на, где существует ровно одна (мы, конечно, не принимаем во внимание ⟂) стрелку. Для любого типа A существует одна функция A -> (), а именно const (). Однако функции A -> Void нет. Вместо этого Void является начальным объектом: существует ровно одна стрелка от, а именно метод absurd :: Void -> a. Тогда такая моноидальная категория не может быть декартово замкнутой.
Теперь, конечно, вы можете легко переключаться между начальной и конечной, поворачивая направление стрелки. Это всегда помещает вас в двойную структуру, поэтому мы получаем кокартезианскую закрытую категорию . Но это означает, что вам также нужно перевернуть стрелки в ваших моноидальных функторах. Они называются решающими функторами, затем (и обобщают комонады ). Благодаря удивительной схеме именования Конора,

class (Functor f) => Decisive f where
  nogood :: f Void -> Void
  orwell :: f (Either s t) -> Either (f s) (f t)
person leftaroundabout    schedule 26.04.2014
comment
Я понимаю, что это не совсем ответ на ваш вопрос. Копроизведения моноидального функтора WRT могут все еще быть в некотором роде интересными, но я полагаю, что такие проблемы, как unit тривиальность, как вы говорите, в значительной степени мешают этому. - person leftaroundabout; 27.04.2014
comment
Я знал, что вы, должно быть, являетесь автором этого поста, к тому времени, когда я закончил читать первое предложение. - person Cirdec; 27.04.2014

Мой опыт в теории категорий очень ограничен, но, черт возьми, ваш класс PtS напоминает мне _ 1_ class, которая выглядит примерно так:

class Applicative f => Alternative f where
  empty :: f a
  (<|>) :: f a -> f a -> f a

Единственная проблема, конечно же, в том, что Alternative является расширением Applicative. Однако, возможно, можно представить, что это представлено отдельно, и тогда комбинация с Applicative очень напоминает функтор с некоммутативной кольцевой структурой с двумя моноидными структурами в качестве операций кольца? Между Applicative и Alternative IIRC также действуют законы о распределении.

person Dominique Devriese    schedule 27.04.2014
comment
+1 за то, что видел проблему прямо. Проработав проблему вручную, я пришел к такому же выводу в отношении своего ответа. - person Cirdec; 27.04.2014