proarrow-0.1.0.0: src/Proarrow/Category/Enriched/Finitary.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
-- | Profunctors whose hom-sets are finite and numbered: @p a b@ is in bijection with an initial
-- segment of the naturals. This makes limits and colimits computable: an element is an index, so a
-- subset or quotient of a hom-set is a table of indices, which can be reified into a fresh object.
--
-- The numbering is a /value/, as in "Proarrow.Category.Instance.FinHask". A type-level size could
-- only be a formula in the sizes it is built from, which rules out constructions whose count
-- depends on how arrows compose, such as the exponential and the subobject classifier.
--
-- A 'Proarrow.Category.Enriched.Thin.DecidableProfunctor' is the special case where every size is
-- zero or one; 'decidableSize' and 'decidableFromIndex' build that instance.
--
-- This module is only the vocabulary. The category @'Proarrow.Category.Enriched.Finitary.Topos.FINITARY' j k@
-- and everything computed in it live in "Proarrow.Category.Enriched.Finitary.Topos". The class
-- and 'Elt' are needed by 'Proarrow.Limit.Power.Powered', "Proarrow.Category.Enriched" and
-- others that the topos half itself depends on.
module Proarrow.Category.Enriched.Finitary where
import Data.Kind (Constraint)
import Data.List (elemIndex, find, genericIndex, genericTake)
import Data.Maybe (isJust)
import Data.Type.Nat (snat)
import Data.Type.Nat qualified as N
import Data.Universe.Class qualified as U
import Data.Universe.Helpers qualified as U
import Numeric.Natural (Natural)
import Prelude (Maybe (..), compare, show, (+), (-), (<), (==))
import Prelude qualified as P
import Proarrow.Category.Enriched.Thin
( DecidableProfunctor (..)
, Decision (..)
, Enumerable (..)
, Finite (..)
, Indexed (..)
, IndexedList (..)
)
import Proarrow.Category.Instance.Bool (Booleans)
import Proarrow.Category.Instance.Opposite (OPPOSITE (..), Op (..))
import Proarrow.Category.Instance.Ordinal (LTE)
import Proarrow.Category.Instance.Product ((:**:) (..))
import Proarrow.Category.Instance.Unit (Unit (..))
import Proarrow.Core (CategoryOf (..), Hom, Profunctor (..), Promonad (..), type (+->))
import Proarrow.Functor (FunctorForRep (..), withMappedOb)
import Proarrow.Profunctor.Corepresentable (Corep (..))
import Proarrow.Profunctor.Instance.Coproduct ((:+:) (..))
import Proarrow.Profunctor.Instance.Initial (InitialProfunctor)
import Proarrow.Profunctor.Instance.Product ((:*:) (..))
import Proarrow.Profunctor.Instance.Terminal (TerminalProfunctor (..))
import Proarrow.Tools.Laws (ProLaw (..), ProLaws (..), (=:=))
-- | A profunctor with finite, numbered hom-sets. 'toIndex' and 'fromIndex' are inverse for indices
-- below 'size'; @fromIndex@ of anything else is an error, as is 'toIndex' of an element that is not
-- one the instance can produce (which only an unlawfully built value can be).
--
-- An instance whose elements are found by searching should define 'elements' and read 'size' off
-- it, rather than let the default call 'fromIndex' once per element and repeat the search each time.
type Finitary :: forall {j} {k}. j +-> k -> Constraint
class (Profunctor p) => Finitary (p :: j +-> k) where
-- | How many elements the hom-set has.
size :: (Ob (a :: k), Ob (b :: j)) => Natural
-- | Where an element sits in 'elements'. Takes its objects like the others do, so that an
-- instance that has to search can bind the search outside the argument lambda and a caller can
-- share it with @let toIndexP = 'toIndex' \@p \@a \@b@.
toIndex :: (Ob (a :: k), Ob (b :: j)) => p a b -> Natural
-- | The element at a position.
fromIndex :: (Ob (a :: k), Ob (b :: j)) => Natural -> p a b
-- | All elements of a hom-set, in index order.
elements :: (Ob (a :: k), Ob (b :: j)) => [p a b]
elements @a @b = P.map (fromIndex @p) (indices (size @p @a @b))
-- | 'fromIndex' recovers an element from its index. The other laws, that 'elements' has 'size'
-- entries numbered in order, are not equations between elements.
instance ProLaws Finitary where
proLaws = [ProLaw "fromIndex . toIndex" \ @p @a @b p _ _ -> p =:= fromIndex @p @a @b (toIndex p)]
-- | @[0 .. n-1]@, which @n@ being a 'Natural' rules out writing directly.
indices :: Natural -> [Natural]
indices n = genericTake n [0 ..]
-- | A finitary profunctor's hom-set sizes, one per pair of objects, the outer index running over
-- @k@ and the inner over @j@. Cheap enough to display an object by: a presheaf on the graph
-- schema, say, shows as @[2,4,2,4]@.
sizes :: forall {j} {k} (p :: j +-> k). (Finitary p, Enumerable j, Enumerable k) => [Natural]
sizes = foreachOb @k \ @a -> foreachOb @j \ @b -> [size @p @a @b]
-- | The position of an object in its kind's object list.
objIndex :: forall {k} (a :: k). (Enumerable k, Ob a) => Natural
objIndex = withIndex @k @a (N.snatToNatural (snat @(Index a)))
-- | Everything an enumeration of a kind's objects can do at each of them, concatenated.
foreachOb :: forall k r. (Enumerable k) => (forall (a :: k). (Ob a) => [r]) -> [r]
foreachOb f = go (finite @k)
where
go :: forall (as :: [k]). IndexedList as -> [r]
go FNil = []
go (FCons @a as) = withOb @k @a (f @a) P.++ go as
-- * Thin profunctors
-- | A decidable profunctor has one element where it holds and none where it does not. These cannot
-- be @default@ method bodies: 'size' and 'fromIndex' do not mention their objects except in a
-- constraint, so GHC cannot tie a default body's objects to the instance's.
decidableSize :: forall {j} {k} (p :: j +-> k) (a :: k) (b :: j). (DecidableProfunctor p, Ob a, Ob b) => Natural
decidableSize = case decide @p @a @b of
Yes _ -> 1
No -> 0
decidableFromIndex
:: forall {j} {k} (p :: j +-> k) (a :: k) (b :: j). (DecidableProfunctor p, Ob a, Ob b) => Natural -> p a b
decidableFromIndex _ = case decide @p @a @b of
Yes x -> x
No -> P.error "fromIndex: the profunctor does not hold here"
-- * Hom-sets as finite sets
-- | An element of a hom-set of @p@, viewed as an element of a /finite set/: every instance the
-- @universe@ package asks for is supplied by the numbering, with 'toIndex' standing in for equality
-- and ordering. So a finitary profunctor is a profunctor enriched in
-- 'Proarrow.Category.Instance.FinHask.FINHASK'.
newtype Elt (p :: j +-> k) (a :: k) (b :: j) = Elt {unElt :: p a b}
instance (Finitary p, Ob a, Ob b) => U.Universe (Elt (p :: j +-> k) a b) where
universe = P.map Elt (elements @p)
instance (Finitary p, Ob a, Ob b) => U.Finite (Elt (p :: j +-> k) a b) where
cardinality = U.Tagged (size @p @a @b)
instance (Finitary p) => P.Eq (Elt (p :: j +-> k) a b) where
Elt x == Elt y = (toIndex x == toIndex y) \\ x
instance (Finitary p) => P.Ord (Elt (p :: j +-> k) a b) where
compare (Elt x) (Elt y) = P.compare (toIndex x) (toIndex y) \\ x
instance (Finitary p) => P.Show (Elt (p :: j +-> k) a b) where
show (Elt x) = P.show (toIndex x) \\ x
-- | A category whose hom-sets are finite: the 'Finitary' counterpart of
-- 'Proarrow.Category.Enriched.Thin.Decidable', and one half of 'FiniteCat'.
class (CategoryOf k, Finitary (Hom k)) => LocallyFinite k
instance (CategoryOf k, Finitary (Hom k)) => LocallyFinite k
-- | How an arrow factors through another into the same object: @'factorThrough' g f@ is an @h@
-- with @g = f '.' h@, if there is one. Only the hom-set @x '~>' y@ is searched, so only the
-- hom-sets need to be finite. An element is in the image of @f@ iff it factors through @f@.
factorThrough
:: forall {k} (x :: k) y a. (LocallyFinite k, Ob x, Ob y, Ob a) => x ~> a -> y ~> a -> Maybe (x ~> y)
factorThrough g f = find (\h -> toIndex @(Hom k) @x @a (f . h) == gi) (elements @(Hom k) @x @y)
where
-- hoisted out of the lambda, as 'toIndex' asks: an instance that searches only searches once
gi = toIndex g
-- | Whether an arrow factors through another, which is 'factorThrough' with the witness dropped.
-- 'Proarrow.Category.Enriched.Finitary.Sheaf.generatedSieve' is the caller: a cover's sieve is the
-- arrows that factor through one of its legs.
factorsThrough :: forall {k} (x :: k) y a. (LocallyFinite k, Ob x, Ob y, Ob a) => x ~> a -> y ~> a -> P.Bool
factorsThrough g f = isJust (factorThrough g f)
-- | A finite category: finitely many objects, and finitely many arrows between them. The first is
-- 'Enumerable', the second does not follow from it, and the enumeration below needs both.
class (Enumerable k, Finitary (Hom k)) => FiniteCat k
instance (Enumerable k, Finitary (Hom k)) => FiniteCat k
-- | A profunctor between categories with finite hom-sets is finitary iff it is enriched in finite
-- sets. These build a 'Finitary' instance from 'U.Finite' hom-sets, the counterparts of
-- 'decidableSize' and 'decidableFromIndex' one level up.
--
-- 'finiteToIndex' and 'finiteFromIndex' search 'U.universeF', which is fine for small hom-sets.
-- For large ones compute the index arithmetically, as
-- 'Proarrow.Category.Instance.FinHask.FinHask' does.
finiteSize :: forall {j} {k} (p :: j +-> k) (a :: k) (b :: j). (U.Finite (p a b)) => Natural
finiteSize = U.unTagged (U.cardinality @(p a b))
finiteToIndex :: forall {j} {k} (p :: j +-> k) (a :: k) (b :: j). (U.Finite (p a b), P.Eq (p a b)) => p a b -> Natural
finiteToIndex x = case elemIndex x U.universeF of
Just i -> P.fromIntegral i
Nothing -> P.error "toIndex: not in the universe of the hom-set"
finiteFromIndex :: forall {j} {k} (p :: j +-> k) (a :: k) (b :: j). (U.Finite (p a b)) => Natural -> p a b
finiteFromIndex i = genericIndex (U.universeF @(p a b)) i
-- | The one-object category has one arrow.
instance Finitary Unit where
size = 1
toIndex Unit = 0
fromIndex _ = Unit
-- | @'Proarrow.Category.Instance.Bool.BOOL'@ is thin, so each hom-set holds at most the one arrow.
instance Finitary Booleans where
size @a @b = decidableSize @Booleans @a @b
toIndex _ = 0
fromIndex @a @b = decidableFromIndex @Booleans @a @b
-- | The ordinals are thin too, so the same three lines serve. So a chain can be used as a site: a
-- cover there can have a leg that is itself covered, which no coverage on a two-object category
-- can arrange.
instance Finitary LTE where
size @a @b = decidableSize @LTE @a @b
toIndex _ = 0
fromIndex @a @b = decidableFromIndex @LTE @a @b
-- * Products and coproducts
-- | The terminal profunctor has one element everywhere.
instance (CategoryOf j, CategoryOf k) => Finitary (TerminalProfunctor :: j +-> k) where
size = 1
toIndex TerminalProfunctor = 0
fromIndex _ = TerminalProfunctor
-- | A pair of indices as one index, row-major: the first factor varies slowest. Shared by the two
-- instances that number a pair of independent choices (the product profunctor and the Yoneda
-- embedding), because 'ExpWeight' nests one inside the other, so they have to agree.
pairIndex :: Natural -> Natural -> Natural -> Natural
pairIndex n i j = i P.* n + j
-- | The inverse, given the size of the second factor.
unpairIndex :: Natural -> Natural -> (Natural, Natural)
unpairIndex 0 _ = P.error "fromIndex: a factor of the pair has no elements"
unpairIndex n i = i `P.divMod` n
instance (Finitary p, Finitary q) => Finitary (p :*: q) where
size @a @b = size @p @a @b P.* size @q @a @b
toIndex @a @b (x :*: y) = pairIndex (size @q @a @b) (toIndex x) (toIndex y)
fromIndex @a @b i = let (l, r) = unpairIndex (size @q @a @b) i in fromIndex l :*: fromIndex r
-- Spelled out, not left to the default: that would ask @p@ for its size once per element, and
-- when @p@ is itself an enumeration ('Sieve', or a nested internal hom) a size is a whole search.
elements @a @b = [x :*: y | x <- elements @p @a @b, y <- elements @q @a @b]
-- | The product of two finitary profunctors on the product of their kinds, numbered as ':*:' is.
instance (Finitary p, Finitary q) => Finitary (p :**: q) where
size @'(a1, a2) @'(b1, b2) = size @p @a1 @b1 P.* size @q @a2 @b2
toIndex @'(_, a2) @'(_, b2) (x :**: y) = pairIndex (size @q @a2 @b2) (toIndex x) (toIndex y)
fromIndex @'(_, a2) @'(_, b2) i = let (l, r) = unpairIndex (size @q @a2 @b2) i in fromIndex l :**: fromIndex r
elements @'(a1, a2) @'(b1, b2) = [x :**: y | x <- elements @p @a1 @b1, y <- elements @q @a2 @b2]
-- | A corepresentable over finite hom-sets is finitary, numbered as the hom-set @f '@' a ~> b@.
-- (It lives here and not with 'Corep': "Proarrow.Profunctor.Corepresentable" is below this module.)
instance (FunctorForRep f, LocallyFinite j) => Finitary (Corep (f :: k +-> j)) where
size @a @b = withMappedOb @f @a (size @(Hom j) @(f @ a) @b)
toIndex (Corep g) = toIndex g \\ g
fromIndex @a @_ i = withMappedOb @f @a (Corep (fromIndex i))
elements @a @b = withMappedOb @f @a (P.map Corep (elements @(Hom j) @(f @ a) @b))
-- | The opposite of a finitary profunctor is finitary, at the same sizes read the other way round.
-- Taking @p = 'Hom' k@ this makes @'OPPOSITE' k@ a 'FiniteCat' whenever @k@ is one, so everything
-- computed for a finite site is available on the opposite category too. (This instance lives here
-- rather than with 'Op' because "Proarrow.Category.Instance.Opposite" sits below this module in the
-- import graph.)
instance (Finitary p) => Finitary (Op p) where
size @(OP a) @(OP b) = size @p @b @a
toIndex @(OP a) @(OP b) (Op x) = toIndex @p @b @a x
fromIndex @(OP a) @(OP b) i = Op (fromIndex @p @b @a i)
elements @(OP a) @(OP b) = P.map Op (elements @p @b @a)
-- | The initial profunctor has no elements anywhere.
instance (CategoryOf j, CategoryOf k) => Finitary (InitialProfunctor :: j +-> k) where
size = 0
toIndex = \case {}
fromIndex _ = P.error "fromIndex: the initial profunctor has no elements"
-- | The indices of @p@ first, then those of @q@.
instance (Finitary p, Finitary q) => Finitary (p :+: q) where
size @a @b = size @p @a @b + size @q @a @b
toIndex @a @b = \case
InjL x -> toIndex x
InjR y -> size @p @a @b + toIndex y
fromIndex @a @b i = if i < size @p @a @b then InjL (fromIndex i) else InjR (fromIndex (i - size @p @a @b))