packages feed

proarrow-0.1.0.0: src/Proarrow/Limit/BinaryProduct.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- | Binary products: 'HasBinaryProducts' provides @a '&&' b@ with projections 'fst'\/'snd' and pairing
-- @('&&&')@, and 'HasProducts' adds the terminal object. Also 'Cartesian' (the monoidal tensor is the
-- product) and the 'PROD' kind wrapper, which makes @('&&')@ the tensor of a monoidal structure on the
-- same objects.
module Proarrow.Limit.BinaryProduct where

import Data.Kind (Type)
import Prelude (Show, type (~))
import Prelude qualified as P

import Proarrow.Category.Enriched.Thin (DecidableProfunctor (..), Decision (..))
import Proarrow.Category.Instance.Bool (BOOL (..), Booleans (..))
import Proarrow.Category.Instance.Free
  ( Elem (..)
  , FREE (..)
  , Free (..)
  , HasStructure (..)
  , IsFreeOb (..)
  , Lower
  , WithShow
  , withLowerOb
  )
import Proarrow.Category.Instance.Product (Diag, Fst, Snd, (:**:) (..))
import Proarrow.Category.Instance.Prof (Prof (..))
import Proarrow.Category.Instance.Unit qualified as U
import Proarrow.Category.Monoidal (Monoidal (..), MonoidalProfunctor (..), SymMonoidal (..))
import Proarrow.Colimit.Initial (HasInitialObject (..))
import Proarrow.Core (CAT, CategoryOf (..), Hom, Profunctor (..), Promonad (..), UN, WrappedOb, type (+->))
import Proarrow.Functor (Functor (..), FunctorForRep (..))
import Proarrow.Limit.Terminal (HasTerminalObject (..))
import Proarrow.Object (Obj, obj)
import Proarrow.Profunctor.Corepresentable (Corep (..))
import Proarrow.Profunctor.Instance.Product (prod, (:*:) (..))
import Proarrow.Profunctor.Representable (Representable (..), withObRep)
import Proarrow.Tools.Laws (Law (..), Laws (..), (===))

infixl 5 &&
infixl 5 &&&
infixl 5 ***

-- | Binary products: an object @a '&&' b@ with projections 'fst' and 'snd', universal among all
-- pairs of arrows out of a common source. Each such pair factors through it uniquely via '(&&&)'.
--
-- __Laws:__
--
-- * @'fst' . (f '&&&' g) = f@
-- * @'snd' . (f '&&&' g) = g@
-- * Uniqueness: @(f . h) '&&&' (g . h) = (f '&&&' g) . h@
--
-- Checked by 'Proarrow.Testing.Laws.testBinaryProducts'.
class (CategoryOf k) => HasBinaryProducts k where
  -- | The product object.
  type (a :: k) && (b :: k) :: k

  -- | Recovers @'Ob' (a '&&' b)@ from the objecthood of the factors.
  withObProd :: (Ob (a :: k), Ob b) => ((Ob (a && b)) => r) -> r

  -- | The left projection.
  fst :: (Ob (a :: k), Ob b) => (a && b) ~> a

  -- | The right projection.
  snd :: (Ob (a :: k), Ob b) => (a && b) ~> b

  -- | The mediating arrow: pairs two arrows out of a common source.
  (&&&) :: (a :: k) ~> x -> a ~> y -> a ~> x && y

  -- | The product of two arrows, acting on each factor independently.
  (***) :: forall a b x y. (a :: k) ~> x -> b ~> y -> a && b ~> x && y
  l *** r = (l . fst @k @a @b) &&& (r . snd @k @a @b) \\ l \\ r

fst' :: forall {k} (a :: k) a' b. (HasBinaryProducts k) => a ~> a' -> Obj b -> a && b ~> a'
fst' a b = a . fst @k @a @b \\ a \\ b

snd' :: forall {k} (a :: k) b b'. (HasBinaryProducts k) => Obj a -> b ~> b' -> a && b ~> b'
snd' a b = b . snd @k @a @b \\ a \\ b

first :: forall {k} (c :: k) (a :: k) (b :: k). (HasBinaryProducts k, Ob c) => a ~> b -> (a && c) ~> (b && c)
first f = f *** obj @c

second :: forall {k} (c :: k) (a :: k) (b :: k). (HasBinaryProducts k, Ob c) => a ~> b -> (c && a) ~> (c && b)
second f = obj @c *** f

diag :: forall {k} (a :: k). (HasBinaryProducts k, Ob a) => a ~> a && a
diag = id &&& id

data family Product :: k -> k +-> k
instance (HasBinaryProducts k, Ob a) => FunctorForRep (Product a :: k +-> k) where
  type Product a @ b = a && b
  fmap f = second @a f
instance (HasBinaryProducts k, Ob a) => Promonad (Corep (Product a) :: k +-> k) where
  id @b = Corep (snd @k @a @b)
  Corep f . Corep @c g = Corep (f . second @a g . associatorProd @a @a @c . first @c (diag @a))

type HasProducts k = (HasTerminalObject k, HasBinaryProducts k)

instance HasBinaryProducts Type where
  type a && b = (a, b)
  withObProd r = r
  fst = P.fst
  snd = P.snd
  f &&& g = \a -> (f a, g a)

instance HasBinaryProducts () where
  -- a wildcard, not @'()@, so that @a && b@ reduces for an abstract @a@, as on pairs
  type _ && _ = '()
  withObProd r = r
  fst = U.Unit
  snd = U.Unit
  U.Unit &&& U.Unit = U.Unit

instance HasBinaryProducts BOOL where
  type TRU && b = b
  type FLS && b = FLS
  type a && TRU = a
  type a && FLS = FLS
  withObProd @a r = case obj @a of
    Tru -> r
    Fls -> r
  fst @a @b = case obj @a of
    Fls -> Fls
    Tru -> terminate @_ @b
  snd @a @b = case obj @b of
    Fls -> Fls
    Tru -> terminate @_ @a
  Fls &&& _ = Fls
  F2T &&& b = b
  Tru &&& Tru = Tru

instance (HasBinaryProducts j, HasBinaryProducts k) => HasBinaryProducts (j, k) where
  -- Through the projections, as the tensor on pairs is, so that the two agree at abstract pairs.
  type a && b = '(Fst @ a && Fst @ b, Snd @ a && Snd @ b)
  withObProd @'(a1, a2) @'(b1, b2) r = withObProd @j @a1 @b1 (withObProd @k @a2 @b2 r)
  fst @'(a1, a2) @'(b1, b2) = fst @_ @a1 @b1 :**: fst @_ @a2 @b2
  snd @'(a1, a2) @'(b1, b2) = snd @_ @a1 @b1 :**: snd @_ @a2 @b2
  (f1 :**: f2) &&& (g1 :**: g2) = (f1 &&& g1) :**: (f2 &&& g2)

instance (CategoryOf j, CategoryOf k) => HasBinaryProducts (j +-> k) where
  type p && q = p :*: q
  withObProd r = r
  fst = Prof fstP
  snd = Prof sndP
  Prof l &&& Prof r = Prof (prod l r)

instance (HasBinaryProducts k, Representable (p :: j +-> k), Representable q) => Representable (p :*: q) where
  type (p :*: q) % a = (p % a) && (q % a)
  index (p :*: q) = index p &&& index q
  tabulate @b f =
    withObRep @p @b (withObRep @q @b (tabulate (fst @_ @(p % b) @(q % b) . f) :*: tabulate (snd @_ @(p % b) @(q % b) . f)))
  repMap f = repMap @p f *** repMap @q f

-- | A product holds when both components do: the type-level '&&' is 'BOOL'\'s categorical product.
instance (DecidableProfunctor p, DecidableProfunctor q) => DecidableProfunctor (p :**: q) where
  type Holds (p :**: q) '(a1, a2) '(b1, b2) = Holds p a1 b1 && Holds q a2 b2
  decide @'(a1, a2) @'(b1, b2) = case (decide @p @a1 @b1, decide @q @a2 @b2) of
    (Yes x, Yes y) -> Yes (x :**: y)
    (No, _) -> No
    (Yes _, No) -> No
  toHolds (f :**: g) r = toHolds f (toHolds g r)

instance (DecidableProfunctor p, DecidableProfunctor q) => DecidableProfunctor (p :*: q) where
  type Holds (p :*: q) a b = Holds p a b && Holds q a b
  decide @a @b = case (decide @p @a @b, decide @q @a @b) of
    (Yes x, Yes y) -> Yes (x :*: y)
    (No, _) -> No
    (Yes _, No) -> No
  toHolds (p :*: q) r = toHolds p (toHolds q r)

leftUnitorProd :: forall {k} (a :: k). (HasProducts k, Ob a) => TerminalObject && a ~> a
leftUnitorProd = snd @k @TerminalObject

leftUnitorProdInv :: forall {k} (a :: k). (HasProducts k, Ob a) => a ~> TerminalObject && a
leftUnitorProdInv = terminate &&& id

rightUnitorProd :: forall {k} (a :: k). (HasProducts k, Ob a) => a && TerminalObject ~> a
rightUnitorProd = fst @k @_ @TerminalObject

rightUnitorProdInv :: forall {k} (a :: k). (HasProducts k, Ob a) => a ~> a && TerminalObject
rightUnitorProdInv = id &&& terminate

associatorProd :: forall {k} (a :: k) b c. (HasBinaryProducts k, Ob a, Ob b, Ob c) => (a && b) && c ~> a && (b && c)
associatorProd = withObProd @k @a @b ((fst @k @a @b . fst @k @(a && b) @c) &&& (snd @k @a @b *** obj @c))

associatorProdInv :: forall {k} (a :: k) b c. (HasBinaryProducts k, Ob a, Ob b, Ob c) => a && (b && c) ~> (a && b) && c
associatorProdInv = withObProd @k @b @c ((obj @a *** fst @k @b @c) &&& (snd @k @b @c . snd @k @a @(b && c)))

swapProd :: forall {k} (a :: k) b. (HasBinaryProducts k, Ob a, Ob b) => a && b ~> b && a
swapProd = snd @k @a @b &&& fst @k @a @b

type data PROD k = PR k

-- | Lifts a profunctor to the 'PROD'-wrapped kinds, where the monoidal structure is the
-- categorical product.
type Prod :: j +-> k -> PROD j +-> PROD k
data Prod p (a :: PROD k) b where
  Prod :: {unProd :: p a b} -> Prod p (PR a) (PR b)

instance (CategoryOf k) => Functor (PR :: k -> PROD k) where
  map f = Prod f

instance (Profunctor p) => Profunctor (Prod p) where
  dimap (Prod l) (Prod r) (Prod p) = Prod (dimap l r p)
  r \\ Prod f = r \\ f
instance (Promonad p) => Promonad (Prod p) where
  id = Prod id
  Prod f . Prod g = Prod (f . g)

-- | The same category as the category of @k@, but with products as the tensor.
instance (CategoryOf k) => CategoryOf (PROD k) where
  type (~>) = Prod (~>)
  type Ob a = WrappedOb PR a

instance (Representable p) => Representable (Prod p) where
  type Prod p % PR a = PR (p % a)
  index (Prod p) = Prod (index p)
  tabulate (Prod f) = Prod (tabulate f)
  repMap (Prod f) = Prod (repMap @p f)

instance (HasTerminalObject k) => HasTerminalObject (PROD k) where
  type TerminalObject = PR TerminalObject
  terminate = Prod terminate
instance (HasBinaryProducts k) => HasBinaryProducts (PROD k) where
  type a && b = PR (UN PR a && UN PR b)
  withObProd @(PR a) @(PR b) r = withObProd @k @a @b r
  fst @(PR a) @(PR b) = Prod (fst @_ @a @b)
  snd @(PR a) @(PR b) = Prod (snd @_ @a @b)
  Prod f &&& Prod g = Prod (f &&& g)
  Prod f *** Prod g = Prod (f *** g)
instance (HasInitialObject k) => HasInitialObject (PROD k) where
  type InitialObject = PR InitialObject
  initiate = Prod initiate

instance (HasProducts k, cat ~ Hom k) => MonoidalProfunctor (Prod cat) where
  one = id
  f ** g = f *** g

-- | Products as monoidal structure.
instance (HasProducts k) => Monoidal (PROD k) where
  type Unit = TerminalObject
  type a ** b = a && b
  withOb2 @(PR a) @(PR b) r = withObProd @k @a @b r
  leftUnitor = leftUnitorProd
  leftUnitorInv = leftUnitorProdInv
  rightUnitor = rightUnitorProd
  rightUnitorInv = rightUnitorProdInv
  associator @(PR a) @(PR b) @(PR c) = Prod (associatorProd @a @b @c)
  associatorInv @(PR a) @(PR b) @(PR c) = Prod (associatorProdInv @a @b @c)

instance (HasProducts k) => SymMonoidal (PROD k) where
  swap @(PR a) @(PR b) = Prod (swapProd @a @b)

type FromProd :: (k -> Type) -> (PROD k -> Type)
data FromProd f a where
  FromProd :: {unFromProd :: f a} -> FromProd f (PR a)

instance (Functor f) => Functor (FromProd f) where
  map (Prod g) (FromProd f) = FromProd (map g f)

instance MonoidalProfunctor (->) where
  one = id
  f ** g = f *** g

-- | Products as monoidal structure.
instance Monoidal Type where
  type Unit = TerminalObject
  type a ** b = a && b
  withOb2 r = r
  leftUnitor = leftUnitorProd
  leftUnitorInv = leftUnitorProdInv
  rightUnitor = rightUnitorProd
  rightUnitorInv = rightUnitorProdInv
  associator = associatorProd
  associatorInv = associatorProdInv

instance SymMonoidal Type where
  swap = swapProd

instance MonoidalProfunctor Booleans where
  one = id
  f ** g = f *** g

-- | Products as monoidal structure.
instance Monoidal BOOL where
  type Unit = TerminalObject
  type a ** b = a && b
  withOb2 @a @b = withObProd @BOOL @a @b
  leftUnitor = leftUnitorProd
  leftUnitorInv = leftUnitorProdInv
  rightUnitor = rightUnitorProd
  rightUnitorInv = rightUnitorProdInv
  associator @a @b @c = associatorProd @a @b @c
  associatorInv @a @b @c = associatorProdInv @a @b @c

instance SymMonoidal BOOL where
  swap @a @b = swapProd @a @b

data family (*!) (a :: k) (b :: k) :: k
instance (IsFreeOb (a :: FREE cs p), IsFreeOb b, HasBinaryProducts `Elem` cs) => IsFreeOb (a *! b) where
  type Lower f (a *! b) = Lower f a && Lower f b
  lowerOb @k' @f r =
    fromAll @HasBinaryProducts @cs @k' (withLowerOb @f @a (withLowerOb @f @b (withObProd @k' @(Lower f a) @(Lower f b) r)))
instance (HasBinaryProducts `Elem` cs) => HasStructure cs (p :: CAT k) HasBinaryProducts where
  data Struct HasBinaryProducts i o where
    Fst :: (Ob a, Ob b) => Struct HasBinaryProducts (a *! b) a
    Snd :: (Ob a, Ob b) => Struct HasBinaryProducts (a *! b) b
    Prd :: i ~> a -> i ~> b -> Struct HasBinaryProducts i (a *! b)
  foldStructure @f _ (Fst @a @b) = withLowerOb @f @a (withLowerOb @f @b (fst @_ @(Lower f a) @(Lower f b)))
  foldStructure @f _ (Snd @a @b) = withLowerOb @f @a (withLowerOb @f @b (snd @_ @(Lower f a) @(Lower f b)))
  foldStructure go (Prd f g) = go f &&& go g
instance (WithShow a) => Show (Struct HasBinaryProducts a b) where
  showsPrec _ Fst = P.showString "fst"
  showsPrec _ Snd = P.showString "snd"
  showsPrec d (Prd f g) =
    P.showParen (d P.> 5) P.$
      P.showsPrec 6 f . P.showString " &&& " . P.showsPrec 6 g
instance (HasBinaryProducts `Elem` cs) => HasBinaryProducts (FREE cs (p :: CAT k)) where
  type a && b = a *! b
  withObProd r = r
  fst = St Fst Nil
  snd = St Snd Nil
  f &&& g = St (Prd f g) Nil \\ f \\ g

-- | The right adjoint to the diagonal functor.
instance (HasBinaryProducts k) => Representable (Corep Diag :: (k, k) +-> k) where
  type Corep Diag % '(a, b) = a && b
  index (Corep (f :**: g)) = f &&& g
  repUniv @'(a, b) = withObProd @k @a @b (Corep (fst @k @a @b :**: snd @k @a @b))

-- | The universal property of the binary product: the projections recover the components of
-- @f '&&&' g@, and every arrow into the product is the pairing of its components.
instance Laws '[HasBinaryProducts] where
  laws =
    [ Law "fst" \ @a @b @c mor -> do
        f <- mor @a @b "f"
        g <- mor @a @c "g"
        f === fst @_ @b @c . (f &&& g)
    , Law "snd" \ @a @b @c mor -> do
        f <- mor @a @b "f"
        g <- mor @a @c "g"
        g === snd @_ @b @c . (f &&& g)
    , Law "pairing naturality" \ @a @b @c @d mor -> do
        f <- mor @a @b "f"
        g <- mor @a @c "g"
        h <- mor @d @a "h"
        (f . h) &&& (g . h) === (f &&& g) . h
    , Law "pairing the projections" \ @_ @b @c _ ->
        withObProd @_ @b @c (fst @_ @b @c &&& snd @_ @b @c === id)
    , Law "pairing uniqueness" \ @a @b @c mor -> withObProd @_ @b @c do
        p <- mor @a @(b && c) "p"
        p === (fst @_ @b @c . p) &&& (snd @_ @b @c . p)
    ]