packages feed

proarrow-0.4.0.0: src/Proarrow/Squares.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

-- | Squares, specialized to profunctors: the counterpart of "Proarrow.Equipment.Squares" in
-- @proarrow-equipment@. Data flows rightward along representable wires; effects flow
-- downward along profunctor wires. Paths of profunctors are read right to left, so
-- @[p, q, r]@ is drawn as @r, q, p@ and has composite @r :.: q :.: p@. Paths of
-- 'Representable' profunctors are read top to bottom.
--
-- 'vId' is the identity for '(===)', and 'hId' is the identity for '(|||)'.
--
-- A square's payload is @'Fold' (p '+++' f) :~> 'Fold' (g '+++' q)@, the fold of each side's
-- concatenated path, instead of @'Fold' f ':.:' 'Fold' p :~> 'Fold' q ':.:' 'Fold' g@.
-- "Proarrow.Path" does the associator\/unitor bookkeeping once, and since @'Nil' '+++' ps@ and
-- @ps '+++' 'Nil'@ both reduce to @ps@, combinators with trivial legs need no unitors.
module Proarrow.Squares where

import Data.Kind (Type)
import Prelude (($))

import Proarrow.Adjunction (Proadjunction)
import Proarrow.Adjunction qualified as Adj
import Proarrow.Core (CAT, CategoryOf (..), Profunctor (..), Promonad (..), lmap, rmap, (//), (:~>), (\\), type (+->))
import Proarrow.Optic (Prostrong (..))
import Proarrow.Path
  ( Fold
  , IsPath
  , IsTight
  , Path (..)
  , SPath (..)
  , Tag (..)
  , appendPath
  , concatFold
  , idN
  , singPath
  , splitFold
  , weakenTight
  , whiskerL
  , whiskerR
  , withAssoc
  , withFoldOb
  , withFoldRep
  , withObAppend
  , type (+++)
  )
import Proarrow.Profunctor.Instance.Composition ((:.:) (..))
import Proarrow.Profunctor.Instance.Identity (Id (..))
import Proarrow.Profunctor.Representable (Representable (..))

infixl 6 |||
infixl 5 ===

-- | The kind of a square @p q f g@.
--
-- > h---p---j
-- > |   |   |
-- > f>--@-->g
-- > |   |   |
-- > i---q---k
type Sq :: Path j h -> Path k i -> Path h i -> Path j k -> Type
data Sq p q f g where
  Sq
    :: (IsPath p, IsPath q, IsTight f, IsTight g)
    => Fold (p +++ f) :~> Fold (g +++ q)
    -> Sq p q f g

-- | Construct a square from its pointwise action. The representable legs act on the
-- input objects through '%'; composition and identity wrappers on those legs are handled
-- automatically. The action must be natural in both objects.
--
-- A traversal crossing, with @[]@ effects and @Maybe@ data:
--
-- >>> import Proarrow.Profunctor.Instance.Star (Star, pattern Star, unStar)
-- >>> import Prelude qualified as P
-- >>> type Crossing = Sq (Star [] ::: Nil) (Star [] ::: Nil) (Star P.Maybe ::: Nil) (Star P.Maybe ::: Nil)
-- >>> let crossing = mkSquare (\(Star f) -> Star (P.traverse f)) :: Crossing
-- >>> unStar (runSquare crossing (Star (\x -> [x, P.not x]))) (P.Just P.True)
-- [Just True,Just False]
-- >>> unStar (runSquare crossing (Star (\x -> [x, P.not x]))) P.Nothing
-- [Nothing]
mkSquare
  :: forall ps qs fs gs
   . (IsPath ps, IsPath qs, IsTight fs, IsTight gs)
  => (forall a b. (Ob a, Ob b) => Fold ps a b -> Fold qs (Fold fs % a) (Fold gs % b))
  -> Sq ps qs fs gs
mkSquare n =
  withFoldOb sps $
    withFoldOb sqs $
      withFoldRep tfs $
        withFoldRep tgs $
          Sq \pf -> case splitFold sps sfs pf of
            f :.: p -> p // concatFold sgs sqs (lmap (index f) (n p) :.: repUniv @(Fold gs))
  where
    sps :: SPath Prof ps
    sps = singPath
    sqs :: SPath Prof qs
    sqs = singPath
    tfs :: SPath Tight fs
    tfs = singPath
    tgs :: SPath Tight gs
    tgs = singPath
    sfs = weakenTight tfs
    sgs = weakenTight tgs

-- | Evaluate a square on a profunctor value. Inverse to 'mkSquare', by naturality and
-- the representability laws. Empty profunctor paths still use the 'Id' wrapper.
runSquare
  :: forall ps qs fs gs a b
   . Sq ps qs fs gs
  -> Fold ps a b
  -> Fold qs (Fold fs % a) (Fold gs % b)
runSquare (Sq n) p =
  withFoldOb sps $
    withFoldOb sqs $
      withFoldRep tfs $
        withFoldRep tgs $
          p // case splitFold sgs sqs (n (concatFold sps sfs (repUniv @(Fold fs) :.: p))) of
            q :.: g -> rmap (index g) q
  where
    sps :: SPath Prof ps
    sps = singPath
    sqs :: SPath Prof qs
    sqs = singPath
    tfs :: SPath Tight fs
    tfs = singPath
    tgs :: SPath Tight gs
    tgs = singPath
    sfs = weakenTight tfs
    sgs = weakenTight tgs

-- | The empty square for an object.
--
-- > K-------K
-- > |       |
-- > |       |
-- > |       |
-- > K-------K
object :: (CategoryOf k) => Sq (Nil :: Path k k) Nil Nil Nil
object = Sq idN

-- | A square between profunctors, with trivial representable legs.
--
-- > K---p---J
-- > |   |   |
-- > |   @   |
-- > |   |   |
-- > K---q---J
vArr :: (Profunctor p, Profunctor q) => p :~> q -> Sq (p ::: Nil) (q ::: Nil) Nil Nil
vArr = Sq

-- | A vertical identity square.
--
-- > J---p---K
-- > |   |   |
-- > |   |   |
-- > |   |   |
-- > J---p---K
vId :: (Profunctor p) => Sq (p ::: Nil) (p ::: Nil) Nil Nil
vId = vArr idN

-- | A square between representable profunctors, with trivial profunctor legs.
--
-- > J-------J
-- > |       |
-- > f>--@-->g
-- > |       |
-- > K-------K
hArr :: (Representable f, Representable g) => f :~> g -> Sq Nil Nil (f ::: Nil) (g ::: Nil)
hArr = Sq

-- | A horizontal identity square.
--
-- > J-------J
-- > |       |
-- > f>----->f
-- > |       |
-- > K-------K
hId :: (Representable f) => Sq Nil Nil (f ::: Nil) (f ::: Nil)
hId = hArr idN

-- | Horizontal composition, with the left square first. Profunctor paths concatenate
-- from right to left, matching the order of profunctor composition.
--
-- > H--p--L--r--M       H--r+p--M
-- > |     |     |       |       |
-- > f--@--g--@--e   =   f---@---e
-- > |     |     |       |       |
-- > I--q--J--s--K       I--s+q--K
(|||) :: forall ps qs fs gs rs ss es. Sq ps qs fs gs -> Sq rs ss gs es -> Sq (rs +++ ps) (ss +++ qs) fs es
Sq l ||| Sq r =
  withObAppend @Prof @rs @ps srs $
    withObAppend @Prof @ss @qs sss $
      withAssoc @es @ss @qs @Tight $
        withAssoc @rs @ps @fs @Prof $
          withAssoc @rs @gs @qs @Prof $
            Sq $
              whiskerR (appendPath srs sgs) (appendPath ses sss) sqs r
                . whiskerL srs (appendPath sps sfs) (appendPath sgs sqs) l
  where
    srs :: SPath Prof rs
    srs = singPath
    sss :: SPath Prof ss
    sss = singPath
    sps :: SPath Prof ps
    sps = singPath
    sqs :: SPath Prof qs
    sqs = singPath
    sfs :: SPath Prof fs
    sfs = weakenTight singPath
    sgs :: SPath Prof gs
    sgs = weakenTight singPath
    ses :: SPath Prof es
    ses = weakenTight singPath

-- | Vertical composition, with the top square first.
--
-- > L---p---M       L---p---M
-- > |       |       |       |
-- > d---@---e       |       |
-- > |       |       |       |
-- > H---q---J   =  d+f--@--e+g
-- > |       |       |       |
-- > f---@---g       |       |
-- > |       |       |       |
-- > I---r---K       I---r---K
(===) :: forall ps qs rs ds es fs gs. Sq ps qs ds es -> Sq qs rs fs gs -> Sq ps rs (ds +++ fs) (es +++ gs)
Sq top === Sq bot =
  withObAppend @Tight @ds @fs tds $
    withObAppend @Tight @es @gs tes $
      withAssoc @ps @ds @fs @Prof $
        withAssoc @es @qs @fs @Tight $
          withAssoc @es @gs @rs @Tight $
            Sq $
              whiskerL ses (appendPath sqs sfs) (appendPath sgs srs) bot
                . whiskerR (appendPath sps sds) (appendPath ses sqs) sfs top
  where
    tds :: SPath Tight ds
    tds = singPath
    tes :: SPath Tight es
    tes = singPath
    sps :: SPath Prof ps
    sps = singPath
    sqs :: SPath Prof qs
    sqs = singPath
    srs :: SPath Prof rs
    srs = singPath
    sds :: SPath Prof ds
    sds = weakenTight tds
    ses :: SPath Prof es
    ses = weakenTight tes
    sfs :: SPath Prof fs
    sfs = weakenTight singPath
    sgs :: SPath Prof gs
    sgs = weakenTight singPath

-- | Bend a representable profunctor from the left edge to its companion on the bottom edge.
--
-- > J-------J
-- > |       |
-- > f>--\   |
-- > |   v   |
-- > K---f---J
toBottom :: (Representable f) => Sq Nil (f ::: Nil) (f ::: Nil) Nil
toBottom = Sq idN

-- | Bend a representable profunctor from the left edge to its conjoint on the top edge.
--
-- > J---f---K
-- > |   ^   |
-- > f>--/   |
-- > |       |
-- > K-------K
toTop :: forall f f'. (Proadjunction f f', Representable f) => Sq (f' ::: Nil) Nil (f ::: Nil) Nil
toTop = Sq (counitNat @f @f')

-- | Bend a companion from the top edge back to a representable profunctor on the right edge.
--
-- > K---f---J
-- > |   v   |
-- > |   \-->f
-- > |       |
-- > K-------K
fromTop :: (Representable f) => Sq (f ::: Nil) Nil Nil (f ::: Nil)
fromTop = Sq idN

-- | Bend a conjoint from the bottom edge back to a representable profunctor on the right edge.
--
-- > J-------J
-- > |       |
-- > |   /-->f
-- > |   ^   |
-- > J---f---K
fromBottom :: forall f f'. (Proadjunction f f', Representable f) => Sq Nil (f' ::: Nil) Nil (f ::: Nil)
fromBottom = Sq (unitNat @f @f')

unitNat :: forall p q. (Proadjunction p q) => Id :~> q :.: p
unitNat (Id f) = rmap f (Adj.unit @p @q) \\ f

counitNat :: forall p q. (Proadjunction p q) => p :.: q :~> Id
counitNat pq = Id (Adj.counit pq)

-- > K-------K
-- > |       |
-- > I>--@   |
-- > |       |
-- > K-------K
hUnitor :: forall k. (CategoryOf k) => Sq Nil Nil ((Id :: CAT k) ::: Nil) Nil
hUnitor = hSplitAll @Nil

-- > K-------K
-- > |       |
-- > |   @-->I
-- > |       |
-- > K-------K
hUnitorInv :: forall k. (CategoryOf k) => Sq Nil Nil Nil ((Id :: CAT k) ::: Nil)
hUnitorInv = hCombineAll @Nil

-- > I---------I
-- > f>---\    |
-- > |    @->gof
-- > g>---/    |
-- > K---------K
hCombine :: forall f g. (Representable f, Representable g) => Sq Nil Nil (f ::: g ::: Nil) (g :.: f ::: Nil)
hCombine = hCombineAll @(f ::: g ::: Nil)

-- > I---------I
-- > |    /--->f
-- > gof>-@    |
-- > |    \--->g
-- > K---------K
hSplit :: forall f g. (Representable f, Representable g) => Sq Nil Nil (g :.: f ::: Nil) (f ::: g ::: Nil)
hSplit = hSplitAll @(f ::: g ::: Nil)

-- | Combine a path of representable profunctors into one composed representable profunctor.
--
-- > J---------J
-- > f>---\    |
-- > |    @--->F
-- > g>---/    |
-- > K---------K
hCombineAll :: forall fs. (IsTight fs) => Sq Nil Nil fs (Fold fs ::: Nil)
hCombineAll = withFoldRep tfs (Sq idN)
  where
    tfs :: SPath Tight fs
    tfs = singPath

-- | Split one composed representable profunctor into a path of representable profunctors.
--
-- > J---------J
-- > |    /--->f
-- > F>---@    |
-- > |    \--->g
-- > K---------K
hSplitAll :: forall fs. (IsTight fs) => Sq Nil Nil (Fold fs ::: Nil) fs
hSplitAll = withFoldRep tfs (Sq idN)
  where
    tfs :: SPath Tight fs
    tfs = singPath

-- > K---I---K
-- > |   |   |
-- > |   @   |
-- > |       |
-- > K-------K
vUnitor :: forall k. (CategoryOf k) => Sq ((Id :: CAT k) ::: Nil) Nil Nil Nil
vUnitor = vSplitAll @Nil

-- > K-------K
-- > |       |
-- > |   @   |
-- > |   |   |
-- > K---I---K
vUnitorInv :: forall k. (CategoryOf k) => Sq Nil ((Id :: CAT k) ::: Nil) Nil Nil
vUnitorInv = vCombineAll @Nil

-- > K-q---p-J
-- > |  \ /  |
-- > |   @   |
-- > |   |   |
-- > K--qop--J
vCombine :: forall p q. (Profunctor p, Profunctor q) => Sq (p ::: q ::: Nil) (q :.: p ::: Nil) Nil Nil
vCombine = vCombineAll @(p ::: q ::: Nil)

-- > K--qop--J
-- > |   |   |
-- > |   @   |
-- > |  / \  |
-- > K-q---p-J
vSplit :: forall p q. (Profunctor p, Profunctor q) => Sq (q :.: p ::: Nil) (p ::: q ::: Nil) Nil Nil
vSplit = vSplitAll @(p ::: q ::: Nil)

-- | Combine a path of profunctors into one composed profunctor.
--
-- > K-q---p-J
-- > |  \ /  |
-- > |   @   |
-- > |   |   |
-- > K---F---J
vCombineAll :: forall ps. (IsPath ps) => Sq ps (Fold ps ::: Nil) Nil Nil
vCombineAll = withFoldOb sp (Sq idN)
  where
    sp :: SPath Prof ps
    sp = singPath

-- | Split one composed profunctor into a path of profunctors.
--
-- > K---F---J
-- > |   |   |
-- > |   @   |
-- > |  / \  |
-- > K-q---p-J
vSplitAll :: forall ps. (IsPath ps) => Sq (Fold ps ::: Nil) ps Nil Nil
vSplitAll = withFoldOb sp (Sq idN)
  where
    sp :: SPath Prof ps
    sp = singPath

-- | The unit of an adjunction.
--
-- > J-------J
-- > |       |
-- > | /-@-\ |
-- > | |   | |
-- > J-q---p-J
unit :: forall p q. (Proadjunction p q) => Sq Nil (p ::: q ::: Nil) Nil Nil
unit =
  vCombineAll @Nil
    === vArr (unitNat @p @q)
    === vSplitAll @(p ::: q ::: Nil)

-- | The counit of an adjunction.
--
-- > K-p---q-K
-- > | |   | |
-- > | \-@-/ |
-- > |       |
-- > K-------K
counit :: forall p q. (Proadjunction p q) => Sq (q ::: p ::: Nil) Nil Nil Nil
counit =
  vCombineAll @(q ::: p ::: Nil)
    === vArr (counitNat @p @q)
    === vSplitAll @Nil

-- | A square introducing an effect @r@ between representable data shapes @f@ and @g@,
-- with no incoming effect. Optics consume and produce squares of this same shape.
--
-- > X-----------X
-- > |           |
-- > f>----@---->g
-- > |     |     |
-- > J-----r-----K
type InputSquare :: (k +-> j) -> (x +-> j) -> (x +-> k) -> Type
type InputSquare r f g = Sq Nil (r ::: Nil) (f ::: Nil) (g ::: Nil)

-- | Absorb a general witness pair around an effect with 'proact'. All three incoming
-- profunctors are vertical or diagonal wires. No representability is required of @p@ or @q@.
--
-- > J--p--r--q--K
-- > |   \ | /   |
-- > |    @@@    |
-- > |     |     |
-- > J-----r-----K
proSquare
  :: forall {j} {k} w (r :: k +-> j) (p :: j +-> j) (q :: k +-> k)
   . (Prostrong w r, w p q, Profunctor p, Profunctor q)
  => Sq (q ::: r ::: p ::: Nil) (r ::: Nil) Nil Nil
proSquare =
  vCombineAll @(q ::: r ::: p ::: Nil)
    === vArr (proact @w @r @p @q)

-- | An optic between representable data shapes, programmed by composing squares.
-- Its legs @s :~> p :.: a@ and @b :~> q :.: t@ introduce the residual profunctors
-- @p@ and @q@, which are absorbed with the input effect by 'proSquare'. All four
-- data shapes share an index category. Constant shapes from the terminal category
-- recover optics between individual objects.
--
-- Wire names appear on borders; each contiguous run of @\@@ marks one transformation. The top central
-- square is the input, the left square unpacks, and the right square rebuilds. The
-- lower square absorbs their three outgoing effects. Residual profunctors stay
-- vertical or diagonal; only representable data wires run horizontally:
--
-- > X-----+-----+-----X
-- > |     |     |     |
-- > s>-@->a>-@->b>-@->t
-- > |  |  |  |  |  |  |
-- > +--p--+--r--+--q--+
-- > |   \    |    /   |
-- > |    @@@@@@@@@    |
-- > |        |        |
-- > J--------r--------K
--
-- The outer boundary is another input-shaped square, now with data legs @s@ and @t@,
-- so it can be used as the input to another optic.
opticSquare
  :: forall {j} {k} {x} w (r :: k +-> j) (p :: j +-> j) (q :: k +-> k) (s :: x +-> j) (t :: x +-> k) (a :: x +-> j) (b :: x +-> k)
   . (Prostrong w r, w p q, Profunctor p, Profunctor q)
  => InputSquare p s a -> InputSquare q b t -> InputSquare r a b -> InputSquare r s t
opticSquare unpack rebuild input =
  unpack ||| input ||| rebuild
    === proSquare @w