packages feed

proarrow-0.1.0.0: test/Props/Finitary/Graph.hs

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

-- | The same topos laws as "Props.Finitary", but over a category with something going on in both
-- variances: @'FINITARY' 'GRAPH' 'BOOL'@. A profunctor @'GRAPH' '+->' 'BOOL'@ is a graph for each
-- object of the walking arrow together with a graph homomorphism between them, so this kind is the
-- arrow category of graphs. Being a presheaf category, it is an elementary topos like any other.
module Props.Finitary.Graph (test) where

import Data.List (genericIndex, genericLength)
import Numeric.Natural (Natural)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Falsify (testProperty)
import Prelude hiding (id, (.))

import Examples.Graph (GRAPH (..), GraphHom (..))
import Proarrow.Category.Enriched.Finitary (Finitary (..), factorThrough, sizes)
import Proarrow.Category.Enriched.Finitary.Topos (FIN, FINITARY)
import Proarrow.Category.Instance.Bool (BOOL (..), Booleans (..), IsBool (..))
import Proarrow.Category.Instance.Opposite (OPPOSITE (..))
import Proarrow.Category.Instance.Prof (Prof)
import Proarrow.Category.Instance.Sub (SUBCAT (..), Sub)
import Proarrow.Core (CAT, CategoryOf (..), Profunctor (..), obj, (//), type (+->))
import Proarrow.Limit.BinaryProduct (PROD (..))
import Proarrow.Profunctor.Instance.Coproduct ((:+:))
import Proarrow.Profunctor.Instance.Exponential ((:~>:))
import Proarrow.Profunctor.Instance.Sieve (Sieve)
import Proarrow.Profunctor.Instance.Terminal (TerminalProfunctor)
import Proarrow.Profunctor.Instance.Yoneda (Yo)
import Proarrow.Testing
  ( Testable (..)
  , TestableProfunctor
  , TestableType (..)
  , TestingEqShow (..)
  , expect
  , genSomeDef
  , optGen
  )
import Proarrow.Testing.Laws
  ( testBinaryCoproducts_
  , testBinaryProducts_
  , testCategory
  , testClosed_
  , testCoequalizers_
  , testEqualizers_
  , testFinitary
  , testInitialObject
  , testPullbacks_
  , testPushouts_
  , testTerminalObject
  )
import Props.Bool ()

-- | The kind of finitary graph homomorphisms. Contravariant in 'BOOL' and covariant in 'GRAPH':
-- @'lmap' 'F2T'@ is the homomorphism itself, carrying the graph at 'TRU' to the graph at 'FLS'.
type GHom = FINITARY GRAPH BOOL

-- * Three graph homomorphisms to test over

-- | The identity on the graph with one edge and two distinct endpoints. Nothing happens in the
-- 'BOOL' direction, and the two incidence maps disagree, so 'Src' and 'Tgt' are distinguishable.
type Same :: GRAPH +-> BOOL
data Same a b where
  SameE :: (IsBool a) => Same a E
  SameS :: (IsBool a) => Same a V
  SameT :: (IsBool a) => Same a V

deriving instance Eq (Same a b)
deriving instance Show (Same a b)

instance Profunctor Same where
  dimap ba dg x =
    ba // case (dg, x) of
      (IdE, SameE) -> SameE
      (IdV, SameS) -> SameS
      (IdV, SameT) -> SameT
      (Src, SameE) -> SameS
      (Tgt, SameE) -> SameT
  r \\ x = case x of SameE -> r; SameS -> r; SameT -> r

-- | The elements over each object, in the order the numbering below uses.
sameElements :: forall a b. (Ob a, Ob b) => [Same a b]
sameElements = case obj @b of
  IdE -> [SameE]
  IdV -> [SameS, SameT]

instance Finitary Same where
  size @a @b = genericLength (sameElements @a @b)
  toIndex SameE = 0
  toIndex SameS = 0
  toIndex SameT = 1
  fromIndex @a @b i = sameElements @a @b `genericIndex` i
  elements = sameElements

-- | The homomorphism that folds that edge into a self-loop: both endpoints go to the one vertex.
-- Non-injective in the 'BOOL' direction, and the loop makes 'Src' and 'Tgt' agree at 'FLS' while
-- they still differ at 'TRU'.
type Fold :: GRAPH +-> BOOL
data Fold a b where
  FoldE :: Fold TRU E
  FoldS :: Fold TRU V
  FoldT :: Fold TRU V
  LoopE :: Fold FLS E
  LoopV :: Fold FLS V

deriving instance Eq (Fold a b)
deriving instance Show (Fold a b)

-- | The 'FLS' layer has one element over each object, so an element there is determined by which
-- object the arrow lands on. So the action on it is forced.
atLoop :: GraphHom b d -> Fold FLS d
atLoop IdE = LoopE
atLoop IdV = LoopV
atLoop Src = LoopV
atLoop Tgt = LoopV

instance Profunctor Fold where
  dimap Fls dg _ = atLoop dg
  dimap F2T dg _ = atLoop dg
  dimap Tru IdE FoldE = FoldE
  dimap Tru IdV x = x
  dimap Tru Src FoldE = FoldS
  dimap Tru Tgt FoldE = FoldT
  r \\ x = case x of FoldE -> r; FoldS -> r; FoldT -> r; LoopE -> r; LoopV -> r

foldElements :: forall a b. (Ob a, Ob b) => [Fold a b]
foldElements = case (boolId @a, obj @b) of
  (Tru, IdE) -> [FoldE]
  (Tru, IdV) -> [FoldS, FoldT]
  (Fls, IdE) -> [LoopE]
  (Fls, IdV) -> [LoopV]

instance Finitary Fold where
  size @a @b = genericLength (foldElements @a @b)
  toIndex FoldE = 0
  toIndex FoldS = 0
  toIndex FoldT = 1
  toIndex LoopE = 0
  toIndex LoopV = 0
  fromIndex @a @b i = foldElements @a @b `genericIndex` i
  elements = foldElements

instance (Ob a, Ob b) => TestingEqShow (Same a b)
instance (Ob a, Ob b) => TestingEqShow (Fold a b)

-- | Spelled out instead of taken from 'elements', so that 'testFinitary' compares the numbering
-- against something independent of it, as in "Props.Finitary".
instance (Ob a, Ob b) => TestableType (Same a b) where
  gen = case obj @b of
    IdE -> optGen [SameE]
    IdV -> optGen [SameS, SameT]

instance (Ob a, Ob b) => TestableType (Fold a b) where
  gen = case (boolId @a, obj @b) of
    (Tru, IdE) -> optGen [FoldE]
    (Tru, IdV) -> optGen [FoldS, FoldT]
    (Fls, IdE) -> optGen [LoopE]
    (Fls, IdV) -> optGen [LoopV]

instance TestableProfunctor Same
instance TestableProfunctor Fold

-- | The identity on the graph with one vertex and no edges. Empty over 'E', so hom-sets out of it
-- are the ones that go empty, and the properties discard instead of failing.
type Dot :: GRAPH +-> BOOL
data Dot a b where
  DotV :: (IsBool a) => Dot a V

instance Profunctor Dot where
  -- 'DotV' first: only then does GHC see that @b@ is 'V', so that 'IdV' is the only arrow out of it
  dimap ba dg DotV = case dg of IdV -> ba // DotV
  r \\ DotV = r

dotElements :: forall a b. (Ob a, Ob b) => [Dot a b]
dotElements = case obj @b of
  IdE -> []
  IdV -> [DotV]

instance Finitary Dot where
  size @a @b = genericLength (dotElements @a @b)
  toIndex DotV = 0
  fromIndex @a @b i = dotElements @a @b `genericIndex` i
  elements = dotElements

-- * The arrow category of graphs, as a testable kind

instance TestableProfunctor (Sub Prof :: CAT GHom)

-- | As in "Props.Finitary": objects come from a fixed palette, and are displayed by their table of
-- sizes, here the four numbers @[FLS\/E, FLS\/V, TRU\/E, TRU\/V]@.
instance Testable GHom where
  showOb @(SUB p) = show (sizes @p)

  -- 'Same' twice over gives a palette object of size 2 at every point. Without one, nine of the
  -- ten non-empty hom-sets are singletons, where an equation between parallel arrows holds by
  -- type-correctness alone and the run asserts nothing. Built from the library's ':+:', whose
  -- 'Finitary' instance supplies the numbering.
  genSome = genSomeDef @'[FIN Same, FIN Fold, FIN Dot, FIN TerminalProfunctor, FIN (Same :+: Same)]

  -- The internal hom enumerates candidate tables by brute force, so it cannot afford the object
  -- above: @sizes \@(Same :+: Same)@ is @[2,4,2,4]@, but the hom /into/ it is @[1024,256,1024,256]@,
  -- and enumerating one such hom-set measured 13.6s and 36.6GB. 'testClosed' draws from
  -- 'genSomeSmall', so the two coexist.
  genSomeSmall = genSomeDef @'[FIN Same, FIN Fold, FIN Dot, FIN TerminalProfunctor]

-- | The sizes of @1 ~~> p@ and of @p@ over one object, which Yoneda says must agree.
yoneda :: forall (p :: GRAPH +-> BOOL). (Finitary p) => [(Natural, Natural)]
yoneda = zip (sizes @(TerminalProfunctor :~>: p)) (sizes @p)

test :: TestTree
test =
  testGroup
    "Finitary.Graph"
    [ testCategory @GHom
    , testTerminalObject @GHom
    , testInitialObject @GHom
    , testBinaryProducts_ @GHom
    , testBinaryCoproducts_ @GHom
    , testClosed_ @(PROD GHom)
    , testEqualizers_ @GHom
    , testCoequalizers_ @GHom
    , testPullbacks_ @GHom
    , testPushouts_ @GHom
    , -- 'GRAPH' is the only non-thin finite category here, so it is the only place
      -- 'factorThrough' has anything to decide. In a thin one @f . h@ and @g@ are both the unique
      -- arrow of their hom-set, so the check succeeds whenever the hom-set is non-empty. The sheaf
      -- sites are both thin, so @Props.Sheaf@ cannot exercise this.
      testProperty "factorThrough decides, where there is a choice of arrow" $ do
        expect "Src factors through itself" (Just IdE) (factorThrough Src Src)
        expect "Tgt does not factor through Src" Nothing (factorThrough Tgt Src)
        expect "Src factors through IdV" (Just Src) (factorThrough Src IdV)
    , testFinitary @Same "Same"
    , testFinitary @Fold "Fold"
    , -- the palette object added above, so its numbering is law-checked and not merely used
      testFinitary @(Same :+: Same) "Same + Same"
    , -- as in "Props.Finitary": this checks the table round trip, the counts below check that the
      -- enumeration is complete
      testFinitary @(Sub Prof :: CAT GHom) "GHom"
    , testProperty "the hom-sets have the sizes a hand count gives them" $ do
        -- a lone vertex picks an endpoint of the edge, and the same one in both layers
        expect "Dot -> Same" 2 (size @(Sub Prof) @(FIN Dot) @(FIN Same))
        expect "Dot -> Fold" 2 (size @(Sub Prof) @(FIN Dot) @(FIN Fold))
        -- the edge graph has one endomorphism and one map onto the loop, both forced by 'Src'
        -- and 'Tgt' having to be preserved
        expect "Same -> Same" 1 (size @(Sub Prof) @(FIN Same) @(FIN Same))
        expect "Same -> Fold" 1 (size @(Sub Prof) @(FIN Same) @(FIN Fold))
        -- backwards there is nothing: a loop would need its one vertex to be both endpoints
        expect "Fold -> Same" 0 (size @(Sub Prof) @(FIN Fold) @(FIN Same))
        -- and a graph with no edges cannot receive one
        expect "Same -> Dot" 0 (size @(Sub Prof) @(FIN Same) @(FIN Dot))
        -- the edge graph has no global sections, for the same reason the loop has no map into it
        expect "1 -> Same" 0 (size @(Sub Prof) @(FIN TerminalProfunctor) @(FIN Same))
    , testProperty "the exponential by the terminal object is the profunctor itself" $ do
        expect "Same" [(1, 1), (2, 2), (1, 1), (2, 2)] (yoneda @Same)
        expect "Fold" [(1, 1), (1, 1), (1, 1), (2, 2)] (yoneda @Fold)
        expect "Dot" [(0, 0), (1, 1), (0, 0), (1, 1)] (yoneda @Dot)
    , testProperty "the Yoneda embedding is numbered as a mixed radix" $ do
        -- The weight of every end here. Neither testable kind exercises its two factors together,
        -- 'BOOL' being thin, but over the schema alone both can exceed one. @Yo V (OP E)@ has
        -- @(c -> V)@ paired with @(E -> d)@, which is 2 * 1, 2 * 2, 1 * 1 and 1 * 2.
        expect
          "sizes"
          [2, 4, 1, 2]
          (sizes @(Yo V (OP E)))
        -- and the index agrees with the enumeration where the radix actually carries. The internal
        -- hom depends on this invariant, since it tabulates families against one and reads them
        -- back with the other
        expect "indices" [0, 1, 2, 3] (map (toIndex @(Yo V (OP E)) @E @V) (elements @(Yo V (OP E)) @E @V))
    , testProperty "the subobject classifier counts the sieves of the index category" $
        -- A sieve over @(a, b)@ is a set of pairs @(g : c -> a, h : b -> d)@ closed under
        -- precomposition. Over 'FLS' there is one @g@, over 'TRU' there are two, ordered. Over 'V'
        -- there is one @h@; over 'E' there are three, with 'IdE' above 'Src' and 'Tgt'. Counting the
        -- down-closed subsets of each product gives 5, 2, 14 and 3.
        expect
          "Omega"
          [5, 2, 14, 3]
          (sizes @(Sieve :: GRAPH +-> BOOL))
    ]