packages feed

proarrow-0.1.0.0: test/Props/FinHask.hs

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

{- HLINT ignore "Use const" -}

module Props.FinHask where

import Data.Map.Strict qualified as M
import Data.Type.Equality ((:~:) (..))
import Data.Universe.Class (Finite (..))
import Data.Universe.Helpers (Tagged (..))
import Data.Void (Void)
import GHC.TypeNats (KnownNat, withKnownNat, withSomeSNat)
import Test.Tasty (TestTree, testGroup)
import Type.Reflection (Typeable, typeRep)
import Unsafe.Coerce (unsafeCoerce)
import Prelude (pure, ($))
import Prelude qualified as P

import Proarrow.Category.Enriched.Finitary (elements)
import Proarrow.Category.Instance.FinHask (FINHASK (..), Fin (..), FinHask (..), fromList)
import Proarrow.Core (CategoryOf (..), UN)

import Proarrow.Testing
  ( GenTotal (..)
  , Some (..)
  , Testable (..)
  , TestableProfunctor
  , TestableType (..)
  , TestingEqShow (..)
  , expect
  , genOb
  , genSomeDef
  , oneElem
  , optGen
  , pattern GenNonEmpty
  )
import Proarrow.Testing.Laws
import Proarrow.Tools.DPO (pushoutComplement)
import Props.Hask ()
import Test.Falsify (testFailed)
import Test.Falsify.Generator (minimalValue)
import Test.Tasty.Falsify (testProperty)

test :: TestTree
test =
  testGroup
    "FinHask"
    [ testCategory @FINHASK
    , testTerminalObject @FINHASK
    , testInitialObject @FINHASK
    , testBinaryProducts @FINHASK (\r -> r)
    , testCartesian @FINHASK (\r -> r) (\r -> r)
    , testMonoidal @FINHASK (\r -> r)
    , testSymMonoidal @FINHASK (\r -> r)
    , testCopyDiscard @FINHASK (\r -> r)
    , testBinaryCoproducts @FINHASK (\r -> r)
    , testDistributive @FINHASK (\r -> r) (\r -> r)
    , testClosed @FINHASK (\r -> r) (\r -> r)
    , testEqualizers @FINHASK withTestObFinHaskViaFin
    , testCoequalizers @FINHASK withTestObFinHaskViaFin
    , testEpiMonoFactorization @FINHASK withTestObFinHaskViaFin
    , testSubobjectClassifier @FINHASK (\r -> r)
    , testPullbacks @FINHASK withTestObFinHaskViaFin
    , testPushouts @FINHASK withTestObFinHaskViaFin
    , testFinitary @FinHask "FinHask"
    , testProperty "a pushout complement deletes what the rule does not keep" $
        -- a : Fin 1 -> l : Fin 2 keeps one of two elements; the match is the identity on Fin 2, so
        -- the complement is the one kept element
        pushoutComplement
          (fromList [(0 :: Fin 1, 0 :: Fin 2)])
          (fromList [(0 :: Fin 2, 0 :: Fin 2), (1, 1)])
          (\_ (FinHask d) -> expect "the complement is the kept element" [0 :: Fin 2] (M.elems d))
          (testFailed "should have been glueable")
    , testProperty "a match identifying a kept element with a deleted one is refused" $
        -- both elements of l map to 0, but the rule keeps only one of them
        pushoutComplement
          (fromList [(0 :: Fin 1, 0 :: Fin 2)])
          (fromList [(0 :: Fin 2, 0 :: Fin 1), (1, 0)])
          (\_ _ -> testFailed "should not have been glueable")
          (pure ())
    , testProperty "a match identifying two deleted elements is refused" $
        -- neither element of l is kept, and both map to 0, so no pushout complement exists
        pushoutComplement
          (fromList [] :: FinHask (FH (Fin 0)) (FH (Fin 2)))
          (fromList [(0 :: Fin 2, 0 :: Fin 1), (1, 0)])
          (\_ _ -> testFailed "should not have been glueable")
          (pure ())
    , testProperty "the numbering agrees with the universe" $ do
        -- 'testFinitary'\'s laws are all order-agnostic, so they would accept a numbering that
        -- disagreed with 'universe'; this is what pins the digit order.
        Some @a <- genOb @FINHASK
        Some @b <- genOb @FINHASK
        expect "elements should be the universe, in order" universeF (elements @FinHask @a @b)
    ]

-- | Only for 'testEqualizers', 'testCoequalizers', 'testPullbacks' and 'testPushouts': it assumes
-- the object is @FH (Fin n)@, as the 'FINHASK' equalizer, coequalizer, pullback and pushout
-- constructions (all via @reifyList@) produce, which the types cannot check. @n@ is recovered from
-- @e@'s cardinality and the equality is coerced, borrowing @Fin@'s 'Typeable'\/'TestableType'
-- instances. Elsewhere (e.g. 'testBinaryProducts') this is unsound: same cardinality is not same
-- runtime representation.
withTestObFinHaskViaFin :: forall (e :: FINHASK) r. (Ob e) => ((TestOb e) => r) -> r
withTestObFinHaskViaFin body = case cardinality @(UN FH e) of
  Tagged n -> withSomeSNat n \ @m snat -> withKnownNat snat (case sameAsFin @m of Refl -> body)
  where
    sameAsFin :: forall m. UN FH e :~: Fin m
    sameAsFin = unsafeCoerce Refl

instance Testable FINHASK where
  type TestOb a = (Ob a, Typeable (UN FH a), TestableType (UN FH a))
  showOb @(FH a) = P.show (typeRep @a)
  genSome = genSomeDef @'[FH Void, FH (), FH P.Bool, FH (Fin 3)]

instance (Ob a, Ob b) => TestingEqShow (FinHask a b)
instance (TestOb a, TestOb b) => TestableType (FinHask a b) where
  gen =
    case gen @(UN FH b) of
      GenEmpty absurd -> case gen @(UN FH a) of
        GenEmpty _ -> oneElem (FinHask M.empty)
        GenNonEmpty g -> GenEmpty \(FinHask m) -> absurd (m M.! minimalValue g)
      GenNonEmpty g -> GenNonEmpty (FinHask P.. M.fromList P.<$> P.traverse (\a -> (a,) P.<$> g) universeF)
instance TestableProfunctor FinHask

instance (KnownNat n) => TestingEqShow (Fin n)
instance (KnownNat n) => TestableType (Fin n) where
  gen = case universeF of
    [] -> GenEmpty \(Fin i) -> P.error ("impossible Fin 0 value: " P.++ P.show i)
    xs -> optGen xs