Состав двух функторов является функтором

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

class CFunctor f where
  type Dom f :: * -> * -> *               -- domain category
  type Cod f :: * -> * -> *               -- codomain category
  cmap :: Dom f a b -> Cod f (f a) (f b)  -- map morphisms across categories

с экземплярами, которые выглядят, например,

instance CFunctor Maybe where
  type Dom Maybe = (->)                   -- domain is Hask
  type Cod Maybe = (->)                   -- codomain is also Hask 
  cmap f = \m -> case m of
                   Nothing -> Nothing
                   Just x  -> Just (f x)

В теории категорий всякий раз, когда F: C → D - функтор, а G: D → E - функтор, то композиция GF: C → E также является функтором.

Я хотел бы выразить это в Haskell. Поскольку я не могу написать instance CFunctor (f . g), я ввожу класс-оболочку:

newtype Wrap g f a = Wrap { unWrap :: g (f a) }

При написании экземпляра CFunctor я добираюсь до

instance (CFunctor f, CFunctor g, Cod f ~ Dom g) => CFunctor (Wrap g f) where
  type Dom (Wrap g f) = Dom f
  type Cod (Wrap g f) = Cod g
  cmap = undefined

но я не могу понять, что должно быть реализовано cmap. Любые советы?

PS конечная причина всего этого - также ввести класс Adjunction с методами unit и counit, а затем автоматически выводить экземпляры монады из адъюнкций. Но сначала мне нужно показать компилятору, что композиция из двух функторов также является функтором.

Я знаю, что я мог бы использовать cmap.cmap для объекта типа g (f a), и это сработало бы, но это похоже на обман. Конечно, функтор - это просто функтор, и компилятор не должен иметь знать, что это фактически композиция из двух других функторов?

Ответ 1

Если функторы F : C → D и G : D → E, функторная композиция G ∘ F : C → E является отображением объектов между категориями C и E, что (G ∘ F)(X) = G(F(X)) и отображение между морфизмами такое, что (G ∘ F)(f) = G(F(f)).

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

instance (CFunctor f, CFunctor g, Cod f ~ Dom g) => CFunctor (Wrap g f) where
  type Dom (Wrap g f) = Dom f
  type Cod (Wrap g f) = Cod g
  cmap f = cmap (cmap f)

Однако, составление cmap дважды дает вам Dom f a b -> Cod g (g (f a)) (g (f b)) и cmap в этом случае имеет тип Dom f a b -> Cod g (Wrap g f a) (Wrap g f b).

Мы можем получить от g (f a) до Wrap g f и наоборот, но так как объявление экземпляра не делает никаких предположений о структуре Cod g, нам не повезло.

Поскольку мы знаем, что функтор является отображением между категориями, мы можем использовать тот факт, что Cod g является Category (на стороне Хаскелла это требует ограничения Category (Cod g)), это дает нам несколько операций для работы с

cmap f = lift? unWrap >>> cmap (cmap f) >>> lift? Wrap

Это, однако, требует удобного оператора подъема lift?, который поднимает функцию из категории Hask в категорию Cod g. Написав Cod g как (~>), тип lift? должен быть:

lift? :: (a -> b) -> (a ~> b)

lift? unWrap  :: Wrap g f a ~> g (f a)
cmap (cmap f) :: g (f a)    ~> g (f b)
lift? Wrap    :: g (f b)    ~> Wrap g f b

lift? unWrap >>> cmap (cmap f) >>> lift? Wrap :: Wrap g f a ~> Wrap g f b

Теперь для этого оператора подъема есть как минимум два варианта:

  • Вы можете расширить пролив от Category (Cod g) до Arrow (Cod g), и в этом случае оператор подъема станет arr,
  • или, как упоминает в комментариях Sjoerd Visscher, вы можете использовать тот факт, что Wrap и unWrap являются семантически id во время выполнения, и в этом случае использование unsafeCoerce оправдано.