Экземпляр Functor для общих полиморфных ADT в Haskell?

Когда дело доходит до применения теории категорий для общего программирования, Haskell выполняет очень хорошую работу, например, с библиотеками типа recursion-schemes. Однако одна вещь, о которой я не уверен, заключается в том, как создать общий экземпляр functor для полиморфных типов.

Если у вас есть полиморфный тип, например List или Tree, вы можете создать функтор из (Hask × Hask) в Hask, который их представляет. Например:

data ListF a b = NilF | ConsF a b  -- L(A,B) = 1+A×B
data TreeF a b = EmptyF | NodeF a b b -- T(A,B) = 1+A×B×B

Эти типы являются полиморфными на A, но являются фиксированными точками относительно B, что-то вроде этого:

newtype Fix f = Fix { unFix :: f (Fix f) }
type List a = Fix (ListF a)
type Tree a = Fix (TreeF a)

Но, как известно, списки и деревья также являются функторами в обычном смысле, где они представляют собой "контейнер" из a, который можно сопоставить функции f :: a -> b, чтобы получить контейнер b "s.

Я пытаюсь выяснить, есть ли способ сделать эти типы (неподвижные точки) экземпляром Functor общим способом, но я не уверен, как это сделать. До сих пор я столкнулся с двумя следующими проблемами:


1) Во-первых, должен быть способ определить общий gmap по любой полиморфной неподвижной точке. Зная, что такие типы, как ListF и TreeF, являются Bifunctors, до сих пор я получил это:

{-# LANGUAGE ScopedTypeVariables #-}
import Data.Bifunctor

newtype Fix f = Fix { unFix :: f (Fix f) }

cata :: Functor f => (f a -> a) -> Fix f -> a
cata f = f . fmap (cata f) . unFix

-- To explicitly use inF as the initial algebra
inF :: f (Fix f) -> Fix f
inF = Fix

gmap :: forall a b f. Bifunctor f => (a -> b) -> Fix (f a) -> Fix (f b)
gmap f = cata alg
    where
        alg :: f a (Fix (f b)) -> Fix (f b)
        alg = inF . bimap f id

В Haskell это дает мне следующую ошибку: Could not deduce (Functor (f a)) arising from a use of cata from the context (Bifunctor f).

Я использую пакет bifunctors, который имеет тип WrappedBifunctor, который специально определяет следующий экземпляр, который мог бы решить указанную выше проблему: Bifunctor p => Functor (WrappedBifunctor p a). Однако я не уверен, как "поднять" этот тип внутри Fix, чтобы иметь возможность использовать его

2) Даже если общее определение gmap может быть определено, я не знаю, возможно ли создать общий экземпляр Functor, который имеет fmap = gmap, и может мгновенно работайте для типов List и Tree там (как и любой другой тип, определенный аналогичным образом). Возможно ли это?

Если да, возможно ли сделать это совместимым с recursion-schemes тоже?

Ответ 1

TBH Я не уверен, насколько это полезно для вас, потому что для этих функций с фиксированной точкой все еще требуется дополнительная newtype упаковка, но здесь мы идем:

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

Учитывая следующие две вспомогательные функции:

unwrapFixBifunctor :: (Bifunctor f) => Fix (WrappedBifunctor f a) -> Fix (f a)
unwrapFixBifunctor = Fix . unwrapBifunctor . fmap unwrapFixBifunctor . unFix

wrapFixBifunctor :: (Bifunctor f) => Fix (f a) -> Fix (WrappedBifunctor f a)
wrapFixBifunctor = Fix . fmap wrapFixBifunctor . WrapBifunctor . unFix

вы можете определить gmap без каких-либо дополнительных ограничений на f:

gmap :: (Bifunctor f) => (a -> b) -> Fix (f a) -> Fix (f b)
gmap f = unwrapFixBifunctor . cata alg . wrapFixBifunctor
  where
    alg = inF . bimap f id

Вы можете сделать Fix . f в Functor с помощью newtype

Мы можем реализовать экземпляр Functor для \a -> Fix (f a), выполнив эту "лямбду уровня" на уровне newtype:

newtype FixF f a = FixF{ unFixF :: Fix (f a) }

instance (Bifunctor f) => Functor (FixF f) where
    fmap f = FixF . gmap f . unFixF

Ответ 2

Если вы готовы принять на тот момент, когда имеете дело с бифунториками, вы можете сказать

cata :: Bifunctor f => (f a r -> r) -> Fix (f a) -> r
cata f = f . bimap id (cata f) . unFix

а затем

gmap :: forall a b f. Bifunctor f => (a -> b) -> Fix (f a) -> Fix (f b)
gmap f = cata alg
    where
        alg :: f a (Fix (f b)) -> Fix (f b)
        alg = inF . bimap f id

gmap, я только что изменил ваше ограничение класса, чтобы работать с переменными типами.)

Вы также можете работать с исходной версией cata, но тогда вам нужны как Functor и ограничение Bifunctor на gmap:

gmap :: forall a b f. (Bifunctor f, Functor (f a)) => (a -> b) -> Fix (f a) -> Fix (f b)
gmap f = cata alg
    where
        alg :: f a (Fix (f b)) -> Fix (f b)
        alg = inF . bimap f id

Вы не можете сделать свой gmap экземпляр обычного класса Functor, потому что это должно быть что-то вроде

instance ... => Functor (\ x -> Fix (f x))

и мы не имеем лямбда уровня. Вы можете сделать это, если вы отмените два аргумента f, но затем вы потеряете экземпляр "other" Functor и вам нужно снова определить cata для Bifunctor.

[Вам также может быть интересно прочитать http://www.andres-loeh.de/IndexedFunctors/ для более общего подхода.]

Ответ 3

Пакет bifunctors также предлагает особенно подходящую версию Fix:

newtype Fix p a = In {out :: p (Fix p a) a}

Это делает экземпляр Functor довольно легко:

instance Bifunctor p => Functor (Fix p) where
  fmap f = In . bimap (fmap f) f . out