proarrow-0.1.0.0: src/Proarrow/Category/Instance/Ordinal.hs
-- | The finite ordinal @n@ as a thin category: the kind @'ORDINAL' n@ has objects @'OZ', 'OS' 'OZ',
-- ...@ (@n@ of them), with an arrow @a '~>' b@ when and only when @a <= b@ ('LTE'). This is the
-- linear order on @n@ elements. Small enough that (co)equalizers can be computed by explicit case
-- analysis.
module Proarrow.Category.Instance.Ordinal where
import Data.Kind (Constraint, Type)
import Data.Type.Nat (Nat (..), SNat (..), SNatI, snat)
import Prelude (Maybe (..), type (~))
import Proarrow.Category.Enriched.Thin
( AtOb (..)
, DecidableProfunctor (..)
, Decision (..)
, Enumerable (..)
, Finite (..)
, FmapWrap
, Indexed (..)
, IndexedList (..)
, Lookup
, MapWrap
, ThinProfunctor (..)
, mapDecision
, mapWrap
, withLookupMapWrap
)
import Proarrow.Category.Instance.Bool (BOOL (..))
import Proarrow.Category.Monoidal (Monoidal (..), MonoidalProfunctor (..), SymMonoidal (..))
import Proarrow.Category.Monoidal.CopyDiscard (CopyDiscard)
import Proarrow.Category.Monoidal.Distributive (Distributive (..))
import Proarrow.Category.Topos (HasEpiMonoFactorization (..))
import Proarrow.Colimit.BinaryCoproduct (HasBinaryCoproducts (..))
import Proarrow.Colimit.Coequalizer (HasCoequalizers (..), thinCoequalize)
import Proarrow.Colimit.Initial (HasInitialObject (..))
import Proarrow.Colimit.Pushout (HasPushouts (..))
import Proarrow.Core (CAT, CategoryOf (..), Profunctor (..), Promonad (..), dimapDefault, obj)
import Proarrow.Limit.BinaryProduct
( HasBinaryProducts (..)
, HasProducts
, associatorProd
, associatorProdInv
, diag
, leftUnitorProd
, leftUnitorProdInv
, rightUnitorProd
, rightUnitorProdInv
, swapProd
)
import Proarrow.Limit.Equalizer (HasEqualizers (..), thinEqualize)
import Proarrow.Limit.Pullback (HasPullbacks (..))
import Proarrow.Limit.Terminal (HasTerminalObject (..))
import Proarrow.Monoid (CocommutativeComonoid, Comonoid (..))
import Prelude qualified as P
type data ORDINAL n where
OZ :: ORDINAL (S n)
OS :: ORDINAL (S n) -> ORDINAL (S (S n))
type ORDINAL0 = ORDINAL Z
type ORDINAL1 = ORDINAL (S Z)
type ORDINAL2 = ORDINAL (S (S Z))
type ORDINAL3 = ORDINAL (S (S (S Z)))
type LTE :: forall {n :: Nat}. CAT (ORDINAL n)
data LTE a b where
ZEQ :: LTE OZ OZ
ZLT :: LTE OZ b -> LTE OZ (OS b)
SLT :: LTE a b -> LTE (OS a) (OS b)
-- | @'ORDINAL' 'Z'@ is the empty ordinal, so an object of it is a contradiction: @'LTE' a a@ has
-- no constructor that can match at this kind, and the empty case discharges any goal.
absurdL :: forall (a :: ORDINAL Z) b. (Ob a) => a ~> b
absurdL = case obj @a of {}
absurdR :: forall a (b :: ORDINAL Z). (Ob b) => a ~> b
absurdR = case obj @b of {}
type SOrdinal :: forall {n :: Nat}. ORDINAL n -> Type
data SOrdinal a where
SOZ :: SOrdinal OZ
SOS :: (IsOrdinal a) => SOrdinal (OS a)
type IsOrdinal :: forall {n :: Nat}. ORDINAL n -> Constraint
class IsOrdinal (a :: ORDINAL n) where
singOrdinal :: SOrdinal a
instance IsOrdinal OZ where
singOrdinal = SOZ
instance (IsOrdinal b) => IsOrdinal (OS b) where
singOrdinal = SOS
-- | Each ordinal is numbered by itself: @'OZ'@ is zero and @'OS'@ the successor.
type OrdIndex :: forall {n :: Nat}. ORDINAL n -> Nat
type family OrdIndex a where
OrdIndex OZ = Z
OrdIndex (OS a) = S (OrdIndex a)
type OrdAt :: forall (n :: Nat) -> Nat -> Maybe (ORDINAL n)
type family OrdAt n i where
OrdAt Z i = 'Nothing
OrdAt (S n) Z = 'Just OZ
OrdAt (S Z) (S i) = 'Nothing
OrdAt (S (S n)) (S i) = FmapWrap OS (OrdAt (S n) i)
-- | The ordinals of @'ORDINAL' n@, in order.
type OrdObjects :: forall (n :: Nat) -> [ORDINAL n]
type family OrdObjects n where
OrdObjects Z = '[]
OrdObjects (S Z) = '[OZ]
OrdObjects (S (S n)) = OZ ': MapWrap OS (OrdObjects (S n))
instance Indexed (ORDINAL n) where
type Index (a :: ORDINAL n) = OrdIndex a
type At (ORDINAL n) i = OrdAt n i
instance (SNatI n) => Finite (ORDINAL n) where
type Objects (ORDINAL n) = OrdObjects n
finite = withOrdObjects @n SZ P.id
withAtLookup i r = withOrdObjects @n i (P.const r)
-- | An ordinal count is none, one, or more. It needs three cases, and not the two of 'Nat', because
-- @'OS'@ lands in @'ORDINAL' ('S' ('S' n))@, so @'ORDINAL' ('S' 'Z')@ holds only @'OZ'@.
ordSize
:: forall n r
. (SNatI n)
=> ((n ~ Z) => r) -> ((n ~ S Z) => r) -> (forall m. (n ~ S (S m), SNatI m) => r) -> r
ordSize none single more = case snat @n of
SZ -> none
SS @m -> case snat @m of
SZ -> single
SS -> more
-- | The ordinals of @'ORDINAL' n@ together with the proof that the list tabulates 'OrdAt' at one index.
-- The two are produced by the same recursion, so each level builds the shorter list once and both
-- the proof and the longer list use it.
withOrdObjects
:: forall n i r
. (SNatI n)
=> SNat i -> ((Lookup (OrdObjects n) i ~ OrdAt n i) => IndexedList (OrdObjects n) -> r) -> r
withOrdObjects i k =
ordSize @n
(k FNil)
(case i of SZ -> k (FCons FNil); SS -> k (FCons FNil))
( \ @m -> case i of
SZ -> withOrdObjects @(S m) SZ \xs -> k (FCons (mapWrap @OS xs))
SS @i' -> withOrdObjects @(S m) (snat @i') \xs ->
withLookupMapWrap @OS (snat @i') xs (k (FCons (mapWrap @OS xs)))
)
-- | The ordinal at an index, if there is one. 'Enumerable' cannot go through the generic 'atOb',
-- which is defined in terms of the 'withOb' being given here, so the walk is done by recursion
-- on the index instead of on the object list.
ordAtOb :: forall n i. (SNatI n) => SNat i -> AtOb (ORDINAL n) (OrdAt n i)
ordAtOb i =
ordSize @n
AtNothing
(case i of SZ -> AtJust; SS -> AtNothing)
( \ @m -> case i of
SZ -> AtJust
SS @i' -> case ordAtOb @(S m) (snat @i') of
AtNothing -> AtNothing
AtJust -> AtJust
)
instance (SNatI n) => Enumerable (ORDINAL n) where
withIndex @a r = case singOrdinal @a of
SOZ -> r
SOS @a' -> case snat @n of SS -> withIndex @_ @a' r
atOb = ordAtOb
instance Profunctor LTE where
dimap = dimapDefault
r \\ ZEQ = r
r \\ ZLT b = r \\ b
r \\ SLT ab = r \\ ab
instance Promonad LTE where
id @a = case singOrdinal @a of
SOZ -> ZEQ
SOS -> SLT id
ZEQ . ZEQ = ZEQ
ZLT b . ZEQ = ZLT b
SLT ab . ZLT za = ZLT (ab . za)
SLT ab . SLT bc = SLT (ab . bc)
-- | The (thin) category of finite ordinals. An arrow from a to b means that a is less than or equal to b.
instance CategoryOf (ORDINAL n) where
type (~>) = LTE
type Ob a = IsOrdinal a
-- | @a <= b@ on the ordinal, as a 'BOOL'.
type OrdLeq :: forall {n :: Nat}. ORDINAL n -> ORDINAL n -> BOOL
type family OrdLeq a b where
OrdLeq OZ b = TRU
OrdLeq (OS a) OZ = FLS
OrdLeq (OS a) (OS b) = OrdLeq a b
instance ThinProfunctor LTE
instance DecidableProfunctor LTE where
type Holds LTE a b = OrdLeq a b
decide @a @b = case (singOrdinal @a, singOrdinal @b) of
(SOZ, SOZ) -> Yes ZEQ
(SOZ, SOS @b') -> mapDecision ZLT (decide @LTE @OZ @b')
(SOS, SOZ) -> No
(SOS @a', SOS @b') -> mapDecision SLT (decide @LTE @a' @b')
toHolds ZEQ r = r
toHolds (ZLT b) r = toHolds b r
toHolds (SLT ab) r = toHolds ab r
instance HasInitialObject (ORDINAL (S n)) where
type InitialObject = OZ
initiate @a = case singOrdinal @a of
SOZ -> ZEQ
SOS @a' -> ZLT (initiate @_ @a')
instance HasTerminalObject (ORDINAL (S Z)) where
type TerminalObject = OZ
terminate @a = case singOrdinal @a of SOZ -> ZEQ
instance (HasTerminalObject (ORDINAL (S n))) => HasTerminalObject (ORDINAL (S (S n))) where
type TerminalObject = OS TerminalObject
terminate @a = case singOrdinal @a of
SOZ -> ZLT terminate
SOS @a' -> SLT (terminate @_ @a')
instance HasBinaryCoproducts (ORDINAL Z) where
type a || b = a
withObCoprod r = r
lft = absurdR
rgt = absurdR
(|||) = \case {}
instance HasBinaryCoproducts (ORDINAL (S Z)) where
type OZ || OZ = OZ
withObCoprod @a @b r = case (singOrdinal @a, singOrdinal @b) of (SOZ, SOZ) -> r
lft @a @b = case (singOrdinal @a, singOrdinal @b) of (SOZ, SOZ) -> ZEQ
rgt @a @b = case (singOrdinal @a, singOrdinal @b) of (SOZ, SOZ) -> ZEQ
ZEQ ||| ZEQ = ZEQ
-- | Maximum
instance (HasBinaryCoproducts (ORDINAL (S n))) => HasBinaryCoproducts (ORDINAL (S (S n))) where
type OZ || b = b
type a || OZ = a
type OS a || OS b = OS (a || b)
withObCoprod @a @b r = case singOrdinal @a of
SOZ -> r
SOS @a' -> case singOrdinal @b of
SOZ -> r
SOS @b' -> withObCoprod @(ORDINAL (S n)) @a' @b' r
lft @a @b = case singOrdinal @b of
SOZ -> obj @a
SOS @b' -> case singOrdinal @a of
SOZ -> ZLT (initiate @_ @b')
SOS @a' -> SLT (lft @_ @a' @b')
rgt @a @b = case singOrdinal @a of
SOZ -> obj @b
SOS @a' -> case singOrdinal @b of
SOZ -> ZLT (initiate @_ @a')
SOS @b' -> SLT (rgt @_ @a' @b')
ZEQ ||| ZEQ = ZEQ
ZLT ZEQ ||| a = a
a ||| ZLT ZEQ = a
ZLT a@ZLT{} ||| ZLT b@ZLT{} = ZLT (a ||| b)
ZLT a@ZLT{} ||| SLT bc = SLT (a ||| bc)
SLT ab ||| ZLT c@ZLT{} = SLT (ab ||| c)
SLT a ||| SLT b = SLT (a ||| b)
instance HasBinaryProducts (ORDINAL Z) where
type a && b = a
withObProd r = r
fst = absurdR
snd = absurdR
(&&&) = \case {}
instance HasBinaryProducts (ORDINAL (S Z)) where
type OZ && OZ = OZ
withObProd @a @b r = case (singOrdinal @a, singOrdinal @b) of (SOZ, SOZ) -> r
fst @a @b = case (singOrdinal @a, singOrdinal @b) of (SOZ, SOZ) -> ZEQ
snd @a @b = case (singOrdinal @a, singOrdinal @b) of (SOZ, SOZ) -> ZEQ
ZEQ &&& ZEQ = ZEQ
-- | Minimum
instance (HasBinaryProducts (ORDINAL (S n))) => HasBinaryProducts (ORDINAL (S (S n))) where
type OZ && b = OZ
type a && OZ = OZ
type OS a && OS b = OS (a && b)
withObProd @a @b r = case singOrdinal @a of
SOZ -> r
SOS @a' -> case singOrdinal @b of
SOZ -> r
SOS @b' -> withObProd @_ @a' @b' r
fst @a @b = case singOrdinal @b of
SOZ -> initiate @_ @a
SOS @b' -> case singOrdinal @a of
SOZ -> ZEQ
SOS @a' -> SLT (fst @_ @a' @b')
snd @a @b = case singOrdinal @a of
SOZ -> initiate @_ @b
SOS @a' -> case singOrdinal @b of
SOZ -> ZEQ
SOS @b' -> SLT (snd @_ @a' @b')
ZEQ &&& ZEQ = ZEQ
ZLT _ &&& ZEQ = ZEQ
ZEQ &&& ZLT _ = ZEQ
ZLT a &&& ZLT b = ZLT (a &&& b)
SLT a &&& SLT b = SLT (a &&& b)
-- | The meet as tensor and the top as unit: the cartesian monoidal structure. Like the products it
-- is made of, only for a syntactically concrete @n@. 'MonoidalOrdinal' names the context.
type MonoidalOrdinal :: Nat -> Constraint
type MonoidalOrdinal n = (HasProducts (ORDINAL n), Ob (TerminalObject :: ORDINAL n))
-- The second conjunct looks redundant, since 'Ob' 'TerminalObject' is a superclass of
-- 'HasTerminalObject'. It is not: 'Monoidal' needs @'Ob' 'Unit'@ as a superclass of the instance
-- /declaration/, and GHC does not discharge an instance's own superclasses from the superclasses
-- of its context (see "Undecidable instances and loopy superclasses" in the GHC user's guide).
-- Without it the instance fails with @Could not deduce IsOrdinal TerminalObject@.
instance (MonoidalOrdinal n) => MonoidalProfunctor (LTE :: CAT (ORDINAL n)) where
one = id
f ** g = f *** g
instance (MonoidalOrdinal n) => Monoidal (ORDINAL n) where
type Unit = TerminalObject
type a ** b = a && b
withOb2 @a @b = withObProd @(ORDINAL n) @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 (MonoidalOrdinal n) => SymMonoidal (ORDINAL n) where
swap @a @b = swapProd @a @b
-- | Every object is a comonoid by the diagonal and the map to the top. So the chain is
-- 'CopyDiscard', and hence 'Proarrow.Category.Monoidal.Cartesian.Cartesian'.
instance (MonoidalOrdinal n, Ob a) => Comonoid (a :: ORDINAL n) where
counit = terminate
comult = diag
instance (MonoidalOrdinal n, Ob a) => CocommutativeComonoid (a :: ORDINAL n)
instance (MonoidalOrdinal n) => CopyDiscard (ORDINAL n)
instance Distributive (ORDINAL (S Z)) where
distL @a @b @c = case (singOrdinal @a, singOrdinal @b, singOrdinal @c) of (SOZ, SOZ, SOZ) -> ZEQ
distR @a @b @c = case (singOrdinal @a, singOrdinal @b, singOrdinal @c) of (SOZ, SOZ, SOZ) -> ZEQ
absorbL @a = case singOrdinal @a of SOZ -> ZEQ
absorbR @a = case singOrdinal @a of SOZ -> ZEQ
-- | A chain is a distributive lattice: the meet is the minimum and the join the maximum. By
-- recursion on the objects, as the products and coproducts are. A bottom on either side makes
-- both sides the same object, and otherwise both sides are a successor.
instance (Distributive (ORDINAL (S n)), MonoidalOrdinal (S n)) => Distributive (ORDINAL (S (S n))) where
distL @a @b @c = case singOrdinal @a of
SOZ -> ZEQ
SOS @a' -> case singOrdinal @b of
SOZ -> withObProd @_ @a @c (obj @(a && c))
SOS @b' -> case singOrdinal @c of
SOZ -> withObProd @_ @a @b (obj @(a && b))
SOS @c' -> SLT (distL @_ @a' @b' @c')
distR @a @b @c = case singOrdinal @c of
SOZ -> ZEQ
SOS @c' -> case singOrdinal @a of
SOZ -> withObProd @_ @b @c (obj @(b && c))
SOS @a' -> case singOrdinal @b of
SOZ -> withObProd @_ @a @c (obj @(a && c))
SOS @b' -> SLT (distR @_ @a' @b' @c')
absorbL = ZEQ
absorbR = ZEQ
-- | @LTE@ is thin, so equalizers are trivial. @factorEqualizer incl h@ just needs @h@'s domain to be
-- @<=@ @incl@'s domain. Since both share the codomain @x@, this can only fail when @incl@'s
-- domain is @OZ@ (nothing below it) but @h@'s domain is a successor (necessarily above @OZ@).
instance HasEqualizers (ORDINAL n) where
equalize = thinEqualize
factorEqualizer ZEQ ZEQ = ZEQ
factorEqualizer (ZLT _) (ZLT _) = ZEQ
factorEqualizer (SLT @e0 incl) (ZLT _) = ZLT (initiate @_ @e0 \\ incl)
factorEqualizer (SLT incl) (SLT h) = SLT (factorEqualizer incl h)
factorEqualizer (ZLT _) (SLT _) = P.error "factorEqualizer: h's image must lie within incl's image"
-- | Dual to the 'HasEqualizers' instance above.
instance HasCoequalizers (ORDINAL n) where
coequalize = thinCoequalize
factorCoequalizer ZEQ ZEQ = ZEQ
factorCoequalizer ZEQ (ZLT @c0' h) = ZLT (initiate @_ @c0' \\ h)
factorCoequalizer (ZLT _) ZEQ = P.error "factorCoequalizer: h must be constant on q's fibers"
factorCoequalizer (ZLT q) (ZLT h) = SLT (factorCoequalizer q h)
factorCoequalizer (SLT q) (SLT h) = SLT (factorCoequalizer q h)
-- | Pullbacks in a thin category are just meets. Computed directly, not via
-- 'Proarrow.Limit.Pullback.thinPullback', which would need @HasProducts (ORDINAL n)@. That is
-- unavailable for an abstract @n@, since 'HasBinaryProducts' and 'HasTerminalObject' are only
-- resolvable for a syntactically concrete @n@.
instance HasPullbacks (ORDINAL n) where
pullback (ZLT _) (ZLT _) k = k ZEQ ZEQ
pullback (ZLT _) (SLT @b' g) k = k ZEQ (ZLT (initiate @_ @b' \\ g))
pullback (SLT @a' f) (ZLT _) k = k (ZLT (initiate @_ @a' \\ f)) ZEQ
pullback (SLT f) (SLT g) k = pullback f g \p1 p2 -> k (SLT p1) (SLT p2)
pullback ZEQ ZEQ k = k ZEQ ZEQ
-- @p1@ and @k1@ already share a codomain (@a@), which is all 'factorEqualizer' needs to compare
-- @q@ against @p@. @p2@/@k2@ carry no extra information once @p1, p2@ are known to be a pullback.
factorPullback p1 _ k1 _ = factorEqualizer p1 k1
-- | Dual to the 'HasPullbacks' instance above: pushouts in a thin category are joins.
instance HasPushouts (ORDINAL n) where
pushout ZEQ g k = k g (id \\ g)
pushout f ZEQ k = k (id \\ f) f
pushout (ZLT f) (ZLT g) k = pushout f g \q1 q2 -> k (SLT q1) (SLT q2)
pushout (SLT f) (SLT g) k = pushout f g \q1 q2 -> k (SLT q1) (SLT q2)
factorPushout p1 _ k1 _ = factorCoequalizer p1 k1
instance HasEpiMonoFactorization (ORDINAL n)