proarrow-0.1.0.0: testing/Proarrow/Testing.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE RequiredTypeArguments #-}
-- | Generic property-testing infrastructure for categories: 'Testable' says how to generate and
-- enumerate the objects of a kind, 'TestableProfunctor' and 'TestableType' how to generate values
-- (using @falsify@ generators), and 'TestingEqShow' provides semantic equality and display for
-- values without useful structural 'Eq'\/'Show' (functions, opaque morphisms). Instances for your
-- own category plus the law checks in "Proarrow.Testing.Laws" give it a test suite.
module Proarrow.Testing
( -- * Describing a category
Testable (..)
, TestableProfunctor (..)
, TestableType (..)
, TestableTypeP
, TestingEqShow (..)
, TestObIsOb
, TestOb'
, obFromTestOb
-- * Objecthood witnesses
, WithTestOb
, WithTestOb2
, WithTestObProd
, WithTestObCoprod
, WithTestObExp
, WithTestObDual
, WithTestObRep
, WithTestObCorep
-- * Objects
, Some (..)
, mapSome
, genOb
, genObSmall
, genObSuchThat
, genObSuchThatWith
, genSomeDef
, genSomeFinite
, genSomeList
, MkSomeList (..)
-- * Profunctor elements
, SomeProfunctorElt (..)
, someP
-- * Generators
-- | @falsify@ generators, wrapped so that an empty type is a first-class case rather than a
-- generator that fails at run time: match 'GenEmpty' first, then 'GenNonEmpty'. The two are a
-- @COMPLETE@ set. The representation behind 'GenNonEmpty' is not exported on purpose. Go
-- through the pattern, which is total.
, GenTotal (GenEmpty)
, pattern GenNonEmpty
, invmap
, isGenNonEmpty
, optGen
, oneElem
, genBoth
, genElements
, oneOfTotal
, genP
, genNamed
, genWithNamed
, genSuchThat
, someElem
, someElemNamed
, someElemWith
-- * Generating functions
-- | 'ShowP' supplies the 'Show' instance @falsify@ needs on both parameters of a generated
-- 'Test.Falsify.Fun', derived from 'showP'; 'applyFunP' unwraps on the way back out.
, ShowP (..)
, applyFunP
-- * Assertions
, expect
, testEq
, eqHask
-- * Interactive debugging
-- | Run a generator once in @ghci@ and print what it produced. These trace to stdout and are
-- for exploring a generator by hand, not for use inside a test.
, sampleT
, sampleP
, sampleK
) where
import Data.Falsify.ConcreteFun qualified as ConcreteFun
import Data.Kind (Constraint, Type)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Maybe (mapMaybe)
import GHC.Exts qualified as GHC
import Test.Falsify (Fun, Property, applyFun, discard, genWith, testFailed)
import Test.Falsify.Generator (Function (..), Gen, elem, fun, minimalValue, oneof)
import Prelude hiding (elem, fst, id, snd, (.), (>>))
import Control.Applicative (Alternative (..))
import Control.Monad (ap, unless)
import Debug.Trace (traceM, traceShowM)
import Proarrow.Category.Enriched.Finitary (Finitary (..), FiniteCat, foreachOb)
import Proarrow.Category.Enriched.Finitary.Sheaf (ClosedSieve (..), Plus, plusTable, samePlus)
import Proarrow.Category.Enriched.Finitary.Topos (KnownTables, Tabulated (..), natTable, natTransformations, sieveTable)
import Proarrow.Category.Enriched.Thin (Enumerable)
import Proarrow.Category.Instance.Opposite (OPPOSITE (..), Op (..))
import Proarrow.Category.Instance.Product (Fst, Snd, (:**:) (..))
import Proarrow.Category.Instance.Prof (Prof (..))
import Proarrow.Category.Instance.Sub (SUBCAT (..), Sub (..))
import Proarrow.Category.Instance.Unit (Unit (..))
import Proarrow.Category.Monoidal qualified as M
import Proarrow.Category.Monoidal.Closed qualified as Exponential
import Proarrow.Category.Monoidal.StarAutonomous qualified as SA
import Proarrow.Category.Sheaf (HasFiniteCovers)
import Proarrow.Colimit.BinaryCoproduct qualified as BinaryCoproduct
import Proarrow.Core (CAT, CategoryOf (..), Hom, Is, OB, Profunctor (..), Promonad (..), UN, type (+->))
import Proarrow.Functor (type (@))
import Proarrow.Functor qualified as Rep
import Proarrow.Limit.BinaryProduct (PROD (..), Prod (..))
import Proarrow.Limit.BinaryProduct qualified as BinaryProduct
import Proarrow.Object (Ob')
import Proarrow.Profunctor.Corepresentable (type (%%))
import Proarrow.Profunctor.Instance.Coproduct ((:+:) (..))
import Proarrow.Profunctor.Instance.Costar (Costar, pattern Costar)
import Proarrow.Profunctor.Instance.Exponential ((:~>:) (..))
import Proarrow.Profunctor.Instance.Product (fstP, sndP, (:*:) (..))
import Proarrow.Profunctor.Instance.Ran (Ran (..))
import Proarrow.Profunctor.Instance.Rift (Rift (..))
import Proarrow.Profunctor.Instance.Sieve (Sieve)
import Proarrow.Profunctor.Instance.Star (Star, pattern Star)
import Proarrow.Profunctor.Instance.Terminal (TerminalProfunctor (..))
import Proarrow.Profunctor.Instance.Yoneda (Yo (..))
import Proarrow.Profunctor.Representable (Rep (..), type (%))
import Test.Falsify.Interactive (falsify)
data GenTotal a where
GenEmpty :: ~(forall x. a -> x) -> GenTotal a
GenNENonFun :: Gen a -> GenTotal a
GenFun :: (TestableType a, TestableType b) => ((a -> b) -> p) -> Gen (Fun (ShowP a) (ShowP b)) -> GenTotal p
invmap :: (a -> b) -> (b -> a) -> GenTotal a -> GenTotal b
invmap _ f' (GenEmpty g) = GenEmpty (g . f')
invmap f _ (GenNENonFun g) = GenNENonFun (fmap f g)
invmap f _ (GenFun f' g) = GenFun (f . f') g
flatten :: GenTotal a -> Gen a
flatten (GenNENonFun g) = g
flatten (GenFun f g) = f . applyFunP <$> g
flatten (GenEmpty _) = error "flatten: Match on GenEmpty first"
pattern GenNonEmpty :: Gen a -> GenTotal a
pattern GenNonEmpty g <- (flatten -> g)
where
GenNonEmpty g = GenNENonFun g
{-# COMPLETE GenEmpty, GenNonEmpty #-}
instance Functor GenTotal where
fmap f = invmap f (error "fmap GenTotal")
instance Applicative GenTotal where
pure a = GenNENonFun (pure a)
(<*>) = ap
instance Alternative GenTotal where
empty = GenEmpty (error "empty")
GenEmpty f <|> GenEmpty _ = GenEmpty f
GenEmpty _ <|> g = g
f <|> GenEmpty _ = f
GenNonEmpty g <|> GenNonEmpty h = GenNonEmpty (oneof (g :| [h]))
-- | Uniformly choose among any number of alternatives, dropping the empty ones. Plain '<|>'
-- only combines two generators at 50\/50, so chaining it over more than two alternatives
-- associates pairwise and skews weight towards whichever branch ends up outermost in the
-- resulting tree. Use this whenever there are more than two alternatives to pick fairly among.
oneOfTotal :: [GenTotal a] -> GenTotal a
oneOfTotal gts = case mapMaybe toGen gts of
[] -> empty
g : gs -> GenNonEmpty (oneof (g :| gs))
where
toGen (GenEmpty _) = Nothing
toGen (GenNonEmpty g) = Just g
instance Monad GenTotal where
GenEmpty _ >>= _ = GenEmpty (error ">>= GenEmpty")
GenNonEmpty g >>= f = case f (minimalValue g) of
GenEmpty x -> GenEmpty x
_ -> GenNonEmpty do
p <- fmap f g
case p of
GenEmpty _ -> error ">>= GenEmpty"
GenNonEmpty g' -> g'
class TestingEqShow a where
eqP :: a -> a -> Property Bool
default eqP :: (Eq a) => a -> a -> Property Bool
eqP l r = pure (l == r)
showP :: a -> String
default showP :: (Show a) => a -> String
showP = show
class (TestingEqShow a) => TestableType a where
gen :: GenTotal a
-- | Supplies a 'Show' instance derived from 'showP'.
--
-- falsify's 'Show' instance for 'Fun' needs 'Show' on both parameters, and we
-- only ever have 'TestingEqShow'. Rather than reinterpret a @'Fun' a b@ at the
-- wrapped type after the fact, @GenFun@ generates at
-- @'Fun' ('ShowP' a) ('ShowP' b)@ from the start, so 'show' applies directly and
-- no coercion is involved. 'applyFunP' unwraps on the way back out.
newtype ShowP a = ShowP {unShowP :: a}
instance (TestingEqShow a) => Show (ShowP a) where
show (ShowP a) = showP a
instance (Function a) => Function (ShowP a) where
function = fmap (ConcreteFun.map unShowP ShowP) . function
-- | Apply a generated function, wrapping and unwrapping the 'ShowP' it was
-- generated at. Both directions are ordinary newtype constructor applications.
applyFunP :: Fun (ShowP a) (ShowP b) -> a -> b
applyFunP f = unShowP . applyFun f . ShowP
genP :: (TestableType a) => Property a
genP = case gen of
GenNENonFun g -> genWith (Just . showP) g
GenFun f g -> f . applyFunP <$> genWith (Just . show) g
GenEmpty _ -> discard
genNamed :: (TestableType a) => String -> Property a
genNamed nm = case gen of
GenNENonFun g -> genWithNamed nm (Just . showP) g
GenFun f g -> f . applyFunP <$> genWithNamed nm (Just . show) g
GenEmpty _ -> discard
-- | Check a measured value against the expected one, showing both. For the assertions a worked
-- example makes, which no generic law-checking property covers.
expect :: (Eq a, Show a) => String -> a -> a -> Property ()
expect what want got = unless (got == want) (testFailed (what ++ ", found " ++ show got ++ ", expected " ++ show want))
-- | Check that two values are semantically equal, naming both sides so a failure says which law
-- broke and what the two sides came out as.
testEq :: (TestingEqShow a) => String -> String -> a -> String -> a -> Property ()
testEq nm sl l sr r = do
isEq <- eqP l r
unless isEq $
testFailed $
"Failed "
++ nm
++ ":\n"
++ sl
++ " = "
++ showP l
++ "\n"
++ sr
++ " = "
++ showP r
genWithNamed :: String -> (a -> Maybe String) -> Gen a -> Property a
genWithNamed nm f = genWith (fmap named . f)
where
named s = "for " ++ nm ++ ": " ++ s
-- | 'True' if a type's generator is non-empty. A pure check on 'TestableType's 'gen'. It
-- doesn't sample anything, so it is cheap to call as often as convenient, e.g. once in a
-- 'genSuchThat' predicate and again in the 'gen'\/'genNamed' call that produces a value.
isGenNonEmpty :: forall a. (TestableType a) => Bool
isGenNonEmpty = case gen @a of
GenEmpty _ -> False
_ -> True
-- | Resample @genKey@ (cheaply, within 'Gen') up to @maxTries@ times until @isUsable@ accepts
-- the draw, before ever asking 'Property' to commit to a choice.
--
-- A 'Property'-level 'discard' restarts the whole property and can trip falsify's discard-ratio
-- limit, aborting the run. So when a later dependent draw (e.g. \"a morphism out of this
-- object\") is likely to be empty for a bad choice, reject that choice here. After @maxTries@ the
-- last draw is returned anyway, and the caller's own 'discard' handles it.
genSuchThat :: Gen key -> (key -> Bool) -> Gen key
genSuchThat genKey isUsable = go maxTries
where
go n = do
k <- genKey
if isUsable k || n <= (0 :: Int) then pure k else go (n - 1)
-- | How many times 'genSuchThat' resamples before giving up. There is no principled formula
-- for this, since it depends on how sparse the requirement being searched for is, which
-- 'genSuchThat' cannot know in advance. 100 is comfortably more than the number of candidates a
-- small test object palette usually offers, so a single unlucky pick is very unlikely to exhaust
-- it. It is still cheap, since each attempt is a plain 'Gen' sample and not a 'Property'-level
-- 'discard'.
maxTries :: Int
maxTries = 100
-- | 'genOb', but resampled (see 'genSuchThat') until @isUsable@ accepts the object.
genObSuchThat :: forall k. (Testable k) => (Some k -> Bool) -> Property (Some k)
genObSuchThat = genObSuchThatWith (genSome @k)
-- | 'genObSuchThat' with objects drawn from the given generator, e.g. 'genSomeSmall'.
genObSuchThatWith :: forall k. (Testable k) => Gen (Some k) -> (Some k -> Bool) -> Property (Some k)
genObSuchThatWith objects = genWith (Just . show) . genSuchThat objects
type SomeProfunctorElt :: (j +-> k) -> Type
data SomeProfunctorElt p where
SomeP :: (TestOb a, TestOb b) => p a b -> SomeProfunctorElt p
someP :: forall {k} {j} (p :: k +-> j) a b. (Profunctor p, TestObIsOb j, TestObIsOb k) => p a b -> SomeProfunctorElt p
someP p = SomeP p \\ p
instance
(forall a b. (TestOb (a :: k), TestOb (b :: j)) => TestingEqShow (p a b), Testable k, Testable j)
=> Show (SomeProfunctorElt p)
where
show (SomeP @a @b p) = showP p ++ " @" ++ showOb @k @a ++ " @" ++ showOb @j @b
type TestableTypeP :: (j +-> k) -> Constraint
class (forall a b. (TestOb (a :: k), TestOb (b :: j)) => TestableType (p a b)) => TestableTypeP (p :: j +-> k)
instance (forall a b. (TestOb (a :: k), TestOb (b :: j)) => TestableType (p a b)) => TestableTypeP (p :: j +-> k)
type TestableProfunctor :: forall {j} {k}. j +-> k -> Constraint
class
(Testable j, Testable k, Profunctor p, forall a b. (TestOb (a :: k), TestOb (b :: j)) => TestingEqShow (p a b)) =>
TestableProfunctor (p :: j +-> k)
where
-- | The default implementation generates an object @a@, then an object @b@ for which @p a b@
-- has elements (see 'genObSuchThat'), and then a value of type @p a b@.
genProfunctorElt :: String -> Property (SomeProfunctorElt p)
default genProfunctorElt :: (TestableTypeP p) => String -> Property (SomeProfunctorElt p)
genProfunctorElt nm = do
Some @a <- genOb
Some @b <- genObSuchThat \(Some @b') -> isGenNonEmpty @(p a b')
p <- genNamed @(p a b) nm
pure $ SomeP p
-- | A kind whose objects can be enumerated and displayed.
class (forall (a :: k). (TestOb a) => Ob' a, TestableProfunctor (Hom k), TestableTypeP (Hom k), CategoryOf k) => Testable k where
type TestOb (a :: k) :: GHC.Constraint
type TestOb a = Ob a
showOb :: forall (a :: k). (TestOb a) => String
genSome :: Gen (Some k)
-- | The palette for properties whose cost grows steeply with object size: in practice those
-- that enumerate an internal hom, which is brute force over tables and doubly exponential (an
-- object with hom-sizes @[2,4,2,4]@ has the hom into it at @[1024,256,1024,256]@). Defaults to
-- 'genSome'. Override it only when 'genSome' draws objects too big for
-- 'Proarrow.Testing.Laws.testClosed' to terminate.
--
-- An instance that wraps another kind's palette must forward this too, as the wrapper instances
-- below do, or 'Proarrow.Testing.Laws.testClosed' silently gets the wide one.
genSomeSmall :: Gen (Some k)
genSomeSmall = genSome
{-# MINIMAL showOb, genSome #-}
genOb :: (Testable k) => Property (Some k)
genOb = genWith (Just . show) genSome
-- | 'genOb' from the small palette. See 'genSomeSmall'.
genObSmall :: (Testable k) => Property (Some k)
genObSmall = genWith (Just . show) genSomeSmall
instance (TestableProfunctor p) => TestableProfunctor (Op p) where
genProfunctorElt nm = do
SomeP p <- genProfunctorElt @p nm
pure $ SomeP (Op p)
instance (Testable k) => Testable (OPPOSITE k) where
type TestOb a = (Is OP a, TestOb (UN OP a))
showOb @(OP a) = "OP (" ++ showOb @k @a ++ ")"
genSome = mapSome OP <$> genSome
genSomeSmall = mapSome OP <$> genSomeSmall
-- | The 'PROD' wrapper changes only which tensor a kind carries, so everything transports across it.
instance (TestableProfunctor p) => TestableProfunctor (Prod p) where
genProfunctorElt nm = do
SomeP p <- genProfunctorElt @p nm
pure $ SomeP (Prod p)
instance (Testable k) => Testable (PROD k) where
type TestOb a = (Is PR a, TestOb (UN PR a))
showOb @(PR a) = "PR (" ++ showOb @k @a ++ ")"
genSome = mapSome PR <$> genSome
genSomeSmall = mapSome PR <$> genSomeSmall
instance TestableProfunctor Unit
instance Testable () where
showOb = "()"
genSome = pure (Some @'())
instance (TestableProfunctor p, TestableProfunctor q) => TestableProfunctor (p :**: q) where
genProfunctorElt nm = do
SomeP p <- genProfunctorElt @p (nm ++ "_0")
SomeP q <- genProfunctorElt @q (nm ++ "_1")
pure $ SomeP (p :**: q)
instance (Testable j, Testable k) => Testable (j, k) where
type TestOb a = (a ~ '(Fst @ a, Snd @ a), TestOb (Fst @ a), TestOb (Snd @ a))
showOb @'(a, b) = "(" ++ showOb @j @a ++ ", " ++ showOb @k @b ++ ")"
genSome = do
Some @a <- genSome @j
Some @b <- genSome @k
pure $ Some @'(a, b)
genSomeSmall = do
Some @a <- genSomeSmall @j
Some @b <- genSomeSmall @k
pure $ Some @'(a, b)
class (TestOb a) => TestOb' a
instance (TestOb a) => TestOb' a
class (forall (a :: k). (Ob a) => TestOb' a) => TestObIsOb k
instance (forall (a :: k). (Ob a) => TestOb' a) => TestObIsOb k
-- | Recover @'Ob' a@ from @'TestOb' a@ (the 'Testable' superclass entailment), packaged as a
-- function so that call sites with other quantified givens in scope (e.g. the comonoid supply of a
-- 'Proarrow.Category.Monoidal.CopyDiscard.CopyDiscard' category, whose head has @Ob@ as a
-- superclass) don't have to rely on GHC expanding superclasses of quantified-constraint heads.
-- With such a given in scope, @\\r -> r@ at this type fails with "Could not deduce Ob a", while
-- the same lambda compiles without it (cf. 'Proarrow.Testing.Laws.testSymMonoidal_' versus
-- 'Proarrow.Testing.Laws.testCopyDiscard_').
obFromTestOb :: forall {k} (a :: k) r. (Testable k, TestOb a) => ((Ob a) => r) -> r
-- Seen on GHC 9.10.3, likely a solver limitation. Worth retrying without this helper after a
-- GHC upgrade.
obFromTestOb r = r
-- * Objecthood witnesses
-- | How 'TestOb' is closed under the structure a law-checker is about.
--
-- Every law-checker in "Proarrow.Testing.Laws" that needs one takes it as an explicit rank-2 argument, since in
-- general a category may make only some of its objects testable; the @_@-suffixed variants supply
-- the trivial witness. These synonyms only name the shapes, which would otherwise be spelled out
-- in forty-odd signatures.
type WithTestOb k = forall (a :: k) r. (Ob a) => ((TestOb a) => r) -> r
-- | @'TestOb'@ is closed under the tensor.
type WithTestOb2 k = forall (a :: k) b r. (TestOb a, TestOb b) => ((TestOb (a M.** b)) => r) -> r
-- | @'TestOb'@ is closed under the binary product.
type WithTestObProd k = forall (a :: k) b r. (TestOb a, TestOb b) => ((TestOb (a BinaryProduct.&& b)) => r) -> r
-- | @'TestOb'@ is closed under the binary coproduct.
type WithTestObCoprod k = forall (a :: k) b r. (TestOb a, TestOb b) => ((TestOb (a BinaryCoproduct.|| b)) => r) -> r
-- | @'TestOb'@ is closed under the internal hom.
type WithTestObExp k = forall (a :: k) b r. (TestOb a, TestOb b) => ((TestOb (a Exponential.~~> b)) => r) -> r
-- | @'TestOb'@ is closed under dualization.
type WithTestObDual k = forall (a :: k) r. (TestOb a) => ((TestOb (SA.Dual a)) => r) -> r
-- | @'TestOb'@ is closed under a representable profunctor.
type WithTestObRep k p = forall (a :: k) r. (TestOb a) => ((TestOb (p % a)) => r) -> r
-- | @'TestOb'@ is closed under a corepresentable profunctor.
type WithTestObCorep k p = forall (a :: k) r. (TestOb a) => ((TestOb (p %% a)) => r) -> r
data Some k where
Some :: forall {k} a. (TestOb (a :: k)) => Some k
mapSome :: forall {j} {k}. forall (f :: j -> k) -> (forall a. (TestOb a) => TestOb' (f a)) => Some j -> Some k
mapSome f (Some @a) = Some @(f a)
class MkSomeList (as :: [k]) where
mkSomeList :: [Some k]
instance MkSomeList '[] where
mkSomeList = []
instance (TestOb (a :: k), MkSomeList as) => MkSomeList (a ': as) where
mkSomeList = Some @a : mkSomeList @k @as
instance (Testable k) => Show (Some k) where
show (Some @a) = showOb @k @a
someElem :: (Show a) => [a] -> Property a
someElem = someElemWith show
someElemNamed :: (Show a) => String -> [a] -> Property a
someElemNamed nm = someElemWith (\a -> "for " ++ nm ++ ": " ++ show a)
someElemWith :: (a -> String) -> [a] -> Property a
someElemWith _ [] = discard
someElemWith f (x : xs) = genWith (Just . f) (elem (x :| xs))
genSomeDef :: forall {k} (obs :: [k]). (Testable k, MkSomeList obs) => Gen (Some k)
genSomeDef = genSomeList "the palette is empty" (mkSomeList @k @obs)
-- | The palette of a category that already knows its own objects: @'Proarrow.Category.Enriched.Thin.Objects' k@
-- is the list 'genSomeDef' would otherwise be given by hand, and writing it twice lets the two
-- drift apart. Only for kinds that really are finite categories. A palette like \"four
-- cardinalities out of infinitely many\" is a sample, not an enumeration, and has to stay
-- hand-picked.
genSomeFinite :: forall k. (Enumerable k, TestObIsOb k) => Gen (Some k)
genSomeFinite = genSomeList "the category has no objects" (foreachOb @k \ @a -> [Some @a])
genSomeList :: String -> [Some k] -> Gen (Some k)
genSomeList what [] = error ("genSome: " ++ what)
genSomeList _ (x : xs) = elem (x :| xs)
-- | A generator for a two-component value: if either component has no values then neither does the
-- pair, and otherwise the two are drawn independently.
--
-- 'GenEmpty' carries its proof of emptiness as a function out of the empty type, so reusing a
-- component's proof for the pair means getting at that component first. Hence the two projections
-- alongside the constructor.
genBoth
:: forall a b c. (TestableType a, TestableType b) => (a -> b -> c) -> (c -> a) -> (c -> b) -> GenTotal c
genBoth mk outl outr = case (gen @a, gen @b) of
(GenEmpty f, _) -> GenEmpty (\c -> f (outl c))
(_, GenEmpty g) -> GenEmpty (\c -> g (outr c))
(GenNonEmpty ga, GenNonEmpty gb) -> GenNonEmpty (liftA2 mk ga gb)
optGen :: [a] -> GenTotal a
optGen [] = error "optGen: empty list"
optGen (x : xs) = GenNonEmpty (elem (x :| xs))
-- | Draw from a finitary profunctor's own enumeration, an empty hom-set being 'GenEmpty' rather than
-- an error: a profunctor built by the library can be empty at a pair of objects with nothing wrong.
-- For a hand-written fixture prefer a palette of its own. 'Proarrow.Testing.Laws.testFinitary'
-- says why a generator that /is/ the enumeration makes the round-trip law vacuous.
genElements :: forall {j} {k} (p :: j +-> k) (a :: k) (b :: j). (Finitary p, Ob a, Ob b) => GenTotal (p a b)
genElements = case elements @p @a @b of
[] -> GenEmpty \_ -> error "genElements: no elements at these objects"
xs -> optGen xs
oneElem :: a -> GenTotal a
oneElem x = GenNonEmpty (pure x)
instance (TestableType a, TestingEqShow b) => TestingEqShow (a -> b) where
eqP = eqHask
showP _ = "<function>"
instance (Function a, TestableType a, TestableType b) => TestableType (a -> b) where
gen = case gen @b of
GenEmpty absurd -> case gen @a of
GenEmpty absurda -> oneElem absurda
GenNonEmpty g -> GenEmpty \ab -> absurd (ab (minimalValue g))
GenNonEmpty gb -> GenFun id (fun (ShowP <$> gb))
eqHask :: (TestableType a, TestingEqShow b) => (a -> b) -> (a -> b) -> Property Bool
eqHask l r =
case gen of
GenEmpty _ -> pure True -- There can only be one function of a type with no values
GenNonEmpty ga -> do
a <- genWith (Just . showP) ga
eqP (l a) (r a)
instance (TestableType (p a b)) => TestableType (Prod p (PR a) (PR b)) where
gen = invmap Prod unProd gen
instance (TestingEqShow (p a b)) => TestingEqShow (Prod p (PR a) (PR b)) where
eqP (Prod l) (Prod r) = eqP l r
showP (Prod p) = "Prod (" ++ showP p ++ ")"
instance (TestableType (p b a)) => TestableType (Op p (OP a) (OP b)) where
gen = invmap Op unOp gen
instance (TestingEqShow (p b a)) => TestingEqShow (Op p (OP a) (OP b)) where
eqP (Op l) (Op r) = eqP l r
showP (Op p) = "Op (" ++ showP p ++ ")"
-- | The elements of 'Star' and 'Costar' are arrows, and are compared, shown and drawn as those.
instance (TestingEqShow (a ~> f b)) => TestingEqShow (Star f a b) where
eqP (Star l) (Star r) = eqP l r
showP (Star f) = showP f
instance (Ob b, TestableType (a ~> f b)) => TestableType (Star f a b) where
gen = invmap Star (\(Star f) -> f) gen
instance (TestingEqShow (f a ~> b)) => TestingEqShow (Costar f a b) where
eqP (Costar l) (Costar r) = eqP l r
showP (Costar f) = showP f
instance (Ob a, TestableType (f a ~> b)) => TestableType (Costar f a b) where
gen = invmap Costar (\(Costar f) -> f) gen
instance (TestableType (a ~> (f Rep.@ b)), Ob b) => TestableType (Rep f a b) where
gen = invmap Rep unRep (gen @(a ~> f Rep.@ b))
instance (TestingEqShow (a ~> (f Rep.@ b)), Ob b) => TestingEqShow (Rep f a b) where
eqP (Rep l) (Rep r) = eqP l r
showP (Rep p) = showP p
instance (TestingEqShow (catk a1 b1), TestingEqShow (catj a2 b2)) => TestingEqShow ((catk :**: catj) '(a1, a2) '(b1, b2)) where
eqP (l1 :**: l2) (r1 :**: r2) = liftA2 (&&) (eqP l1 r1) (eqP l2 r2)
showP (l1 :**: l2) = "(" ++ showP l1 ++ ") :**: (" ++ showP l2 ++ ")"
instance (TestableType (catk a1 b1), TestableType (catj a2 b2)) => TestableType ((catk :**: catj) '(a1, a2) '(b1, b2)) where
gen = genBoth (:**:) fstK sndK
-- | An element of a product of profunctors is a pair of elements.
instance (TestingEqShow (p a b), TestingEqShow (q a b)) => TestingEqShow ((p :*: q) a b) where
eqP (l1 :*: l2) (r1 :*: r2) = liftA2 (&&) (eqP l1 r1) (eqP l2 r2)
showP (l :*: r) = "(" ++ showP l ++ ") :*: (" ++ showP r ++ ")"
instance (TestableType (p a b), TestableType (q a b)) => TestableType ((p :*: q) a b) where
gen = genBoth (:*:) fstP sndP
instance
(TestableProfunctor p, TestableProfunctor q, TestableTypeP p, TestableTypeP q)
=> TestableProfunctor (p :*: q)
-- | An element of a coproduct of profunctors is an element of one side, tagged.
instance (TestingEqShow (p a b), TestingEqShow (q a b)) => TestingEqShow ((p :+: q) a b) where
eqP (InjL l) (InjL r) = eqP l r
eqP (InjR l) (InjR r) = eqP l r
eqP _ _ = pure False
showP (InjL l) = "InjL (" ++ showP l ++ ")"
showP (InjR r) = "InjR (" ++ showP r ++ ")"
instance (TestableType (p a b), TestableType (q a b)) => TestableType ((p :+: q) a b) where
gen = case (gen @(p a b), gen @(q a b)) of
(GenEmpty f, GenEmpty g) -> GenEmpty \case InjL l -> f l; InjR r -> g r
(GenEmpty _, GenNonEmpty gr) -> GenNonEmpty (InjR <$> gr)
(GenNonEmpty gl, GenEmpty _) -> GenNonEmpty (InjL <$> gl)
(GenNonEmpty gl, GenNonEmpty gr) -> GenNonEmpty (oneof ((InjL <$> gl) :| [InjR <$> gr]))
instance
(TestableProfunctor p, TestableProfunctor q, TestableTypeP p, TestableTypeP q)
=> TestableProfunctor (p :+: q)
-- | A 'Tabulated' value is its index, so equality and display are the index's.
instance TestingEqShow (Tabulated t lm rm a b) where
eqP (Tabulated i) (Tabulated j) = pure (i == j)
showP (Tabulated i) = show i
instance
( Testable j
, Testable k
, FiniteCat j
, FiniteCat k
, KnownTables j k lm rm
, TestOb (a :: k)
, TestOb (b :: j)
)
=> TestableType (Tabulated t lm rm a b)
where
gen = obFromTestOb @a (obFromTestOb @b (genElements @(Tabulated t lm rm)))
instance
( Testable j
, Testable k
, FiniteCat j
, FiniteCat k
, KnownTables j k lm rm
)
=> TestableProfunctor (Tabulated t lm rm :: j +-> k)
-- | The terminal profunctor has one element at every pair of objects.
instance TestingEqShow (TerminalProfunctor a b) where
-- forcing is the one thing left to check
eqP l r = l `seq` r `seq` pure True
showP _ = "TerminalProfunctor"
instance (Testable j, Testable k, TestOb (a :: k), TestOb (b :: j)) => TestableType (TerminalProfunctor a b) where
gen = obFromTestOb @a (obFromTestOb @b (oneElem TerminalProfunctor))
instance (Testable j, Testable k) => TestableProfunctor (TerminalProfunctor :: j +-> k)
-- | An element of the Yoneda embedding is an arrow into @x@ paired with an arrow out of @b@, so it
-- is testable wherever both categories are. So a representable can be used as a test fixture, at
-- either variance.
instance
(Testable j, Testable k, TestOb (a :: k), TestOb (x :: k), TestOb (b :: j), TestOb (c :: j))
=> TestingEqShow (Yo x (OP b) a c)
where
eqP (Yo f h) (Yo g i) = liftA2 (&&) (eqP f g) (eqP h i)
showP (Yo f h) = "Yo (" ++ showP f ++ ") (" ++ showP h ++ ")"
instance
(Testable j, Testable k, TestOb (a :: k), TestOb (x :: k), TestOb (b :: j), TestOb (c :: j))
=> TestableType (Yo x (OP b) a c)
where
gen = genBoth Yo (\(Yo l _) -> l) (\(Yo _ r) -> r)
instance (Testable j, Testable k, TestOb (x :: k), TestOb (b :: j)) => TestableProfunctor (Yo x (OP b))
-- | A sieve is a table of booleans over the points of the representable, and 'Finitary' numbers the
-- sieves at each pair of objects. So a sieve can be generated by picking one, and compared and
-- shown by its table. Without this, nothing that quantifies over sieves as elements of a
-- profunctor (such as 'Proarrow.Testing.Laws.propNaturalTransformation') can run at 'Sieve'.
--
-- Drawing one enumerates /every/ sieve at that pair of objects, a count exponential in the size of
-- the representable, so this is the generator to look at first if a suite gets slow.
--
-- The 'TestOb' constraints pin @j@ and @k@, which @'Sieve' a b@ does not mention.
instance (FiniteCat j, FiniteCat k, TestOb (a :: k), TestOb (b :: j)) => TestingEqShow (Sieve a b) where
eqP s t = pure (sieveTable s == sieveTable t)
showP s = show (sieveTable s)
instance
(Testable j, Testable k, FiniteCat j, FiniteCat k, TestOb (a :: k), TestOb (b :: j))
=> TestableType (Sieve a b)
where
gen = obFromTestOb @a (obFromTestOb @b (genElements @(Sieve :: j +-> k)))
instance (Testable j, Testable k, FiniteCat j, FiniteCat k) => TestableProfunctor (Sieve :: j +-> k)
-- | An element of the internal hom is a natural transformation out of a weight, which 'Finitary'
-- numbers; compare and show it by that number, as 'Tabulated' is. Drawing one enumerates them
-- all, so an exponential is expensive to quantify over.
instance
(Finitary p, Finitary q, FiniteCat j, FiniteCat k, TestOb (a :: k), TestOb (b :: j))
=> TestingEqShow ((p :~>: q) a b)
where
-- matching on 'Exp' brings the objects into scope, as it does for 'Sieve'
eqP x@Exp{} y = pure (toIndex @(p :~>: q) x == toIndex y)
showP x@Exp{} = show (toIndex @(p :~>: q) x)
instance
(Testable j, Testable k, Finitary p, Finitary q, FiniteCat j, FiniteCat k, TestOb (a :: k), TestOb (b :: j))
=> TestableType ((p :~>: q) a b)
where
gen = obFromTestOb @a (obFromTestOb @b (genElements @(p :~>: q)))
instance
(Testable j, Testable k, Finitary p, Finitary q, FiniteCat j, FiniteCat k)
=> TestableProfunctor (p :~>: q :: j +-> k)
-- | A natural transformation between finitary profunctors, compared and shown by its table and
-- drawn from 'natTransformations'. The hom-sets of a category of finitary profunctors, with or
-- without the 'SUBCAT' wrapper.
instance (Finitary p, Finitary q, FiniteCat j, FiniteCat k) => TestingEqShow (Prof (p :: j +-> k) q) where
eqP (Prof f) (Prof g) = pure (natTable @p @q f == natTable @p @q g)
showP (Prof f) = show (natTable @p @q f)
instance (Finitary p, Finitary q, FiniteCat j, FiniteCat k) => TestableType (Prof (p :: j +-> k) q) where
gen = case natTransformations @p @q of
[] -> GenEmpty \_ -> error "no natural transformations between these profunctors"
fs -> optGen fs
-- | The right Kan lift and extension of finitary profunctors, compared and shown by index, as the
-- internal hom is.
instance
(Testable j, Testable k, Finitary w, Finitary p, FiniteCat i, FiniteCat j, TestOb (a :: k), TestOb (b :: j))
=> TestingEqShow (Rift (OP (w :: k +-> i)) p a b)
where
eqP x@Rift{} y = pure (toIndex @(Rift (OP w) p) x == toIndex y)
showP x@Rift{} = show (toIndex @(Rift (OP w) p) x)
instance
(Testable j, Testable k, Finitary w, Finitary p, FiniteCat i, FiniteCat j, TestOb (a :: k), TestOb (b :: j))
=> TestableType (Rift (OP (w :: k +-> i)) p a b)
where
gen = obFromTestOb @a (obFromTestOb @b (genElements @(Rift (OP w) p)))
instance
(Testable j, Testable k, Finitary w, Finitary p, FiniteCat i, FiniteCat j)
=> TestableProfunctor (Rift (OP (w :: k +-> i)) p :: j +-> k)
instance
(Testable j, Testable k, Finitary v, Finitary p, FiniteCat i, FiniteCat k, TestOb (a :: k), TestOb (b :: j))
=> TestingEqShow (Ran (OP (v :: i +-> j)) p a b)
where
eqP x@Ran{} y = pure (toIndex @(Ran (OP v) p) x == toIndex y)
showP x@Ran{} = show (toIndex @(Ran (OP v) p) x)
instance
(Testable j, Testable k, Finitary v, Finitary p, FiniteCat i, FiniteCat k, TestOb (a :: k), TestOb (b :: j))
=> TestableType (Ran (OP (v :: i +-> j)) p a b)
where
gen = obFromTestOb @a (obFromTestOb @b (genElements @(Ran (OP v) p)))
instance
(Testable j, Testable k, Finitary v, Finitary p, FiniteCat i, FiniteCat k)
=> TestableProfunctor (Ran (OP (v :: i +-> j)) p :: j +-> k)
-- | A closed sieve is a sieve, and is compared and shown as one. Drawing one is dearer still than
-- drawing a sieve: the closed ones are found by taking the 'closure' of every sieve at the pair.
instance (FiniteCat j, FiniteCat k, TestOb (a :: k), TestOb (b :: j)) => TestingEqShow (ClosedSieve t a b) where
eqP (ClosedSieve s) (ClosedSieve u) = eqP s u
showP (ClosedSieve s) = showP s
instance
( Testable j
, Testable k
, HasFiniteCovers t k
, FiniteCat j
, FiniteCat k
, TestOb (a :: k)
, TestOb (b :: j)
)
=> TestableType (ClosedSieve t a b)
where
gen = obFromTestOb @a (obFromTestOb @b (genElements @(ClosedSieve t :: j +-> k)))
instance
(Testable j, Testable k, HasFiniteCovers t k, FiniteCat j, FiniteCat k)
=> TestableProfunctor (ClosedSieve t :: j +-> k)
-- | Compared by 'samePlus' and shown by 'plusTable'. See 'Plus' for what a value stands for.
instance
-- as for 'Sieve', the 'TestOb's are what pin @j@ and @k@
(HasFiniteCovers t k, Finitary p, FiniteCat j, FiniteCat k, TestOb (a :: k), TestOb (b :: j))
=> TestingEqShow (Plus t p a b)
where
eqP x y = pure (samePlus x y)
showP x = show (plusTable x)
instance
(Testable j, Testable k, HasFiniteCovers t k, Finitary p, FiniteCat j, FiniteCat k, TestOb (a :: k), TestOb (b :: j))
=> TestableType (Plus t p a b)
where
gen = obFromTestOb @a (obFromTestOb @b (genElements @(Plus t p :: j +-> k)))
instance
(Testable j, Testable k, HasFiniteCovers t k, Finitary p, FiniteCat j, FiniteCat k)
=> TestableProfunctor (Plus t p :: j +-> k)
-- | A hom-set of a full subcategory of finitary profunctors ('FINITARY', or the sheaves of
-- "Proarrow.Category.Enriched.Finitary.Sheaf") is enumerable, by 'natTransformations', so it can
-- be generated. Without that a category of profunctors would not be testable at all. Equality and
-- display go through the table of indices, there being nothing else to see of a natural
-- transformation. (The table is cheaper than the index into 'elements' would be, which has to
-- search for it.)
instance
(Finitary p, Finitary q, FiniteCat j, FiniteCat k)
=> TestingEqShow (Sub Prof (SUB p :: SUBCAT (ob :: OB (j +-> k))) (SUB q))
where
eqP (Sub l) (Sub r) = eqP l r
showP (Sub f) = showP f
instance
(Finitary (Sub Prof :: CAT (SUBCAT ob)), Finitary p, Finitary q, FiniteCat j, FiniteCat k, ob p, ob q)
=> TestableType (Sub Prof (SUB p :: SUBCAT (ob :: OB (j +-> k))) (SUB q))
where
-- a hom-set is empty whenever @q@ runs out of elements where @p@ has some, and then the
-- properties discard rather than fail
gen = genElements @(Sub Prof) @(SUB p) @(SUB q)
instance (Ob a, Ob b) => TestableType (Unit a b) where
gen = oneElem Unit
instance TestingEqShow (Unit a b) where
showP _ = "Unit"
-- a singleton, so equality is free; forcing is the one thing left to check
eqP l r = l `seq` r `seq` pure True
sampleT :: forall t. (TestableType t) => IO (Maybe String)
sampleT = falsify $ do
p <- genP @t
traceM (showP p)
sampleP :: forall {j} {k} (p :: j +-> k). (Testable j, Testable k, TestableProfunctor p) => IO (Maybe String)
sampleP = falsify $ do
p <- genProfunctorElt @p "p"
traceShowM p
sampleK :: forall k. (Testable k) => IO (Maybe String)
sampleK = falsify @_ @() $ do
Some @a <- genOb @k
traceM $ showOb @k @a