proarrow-0.1.0.0: src/Proarrow/Tools/Laws.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
-- the identity laws compose with id on purpose
{- HLINT ignore "Redundant id" -}
-- | Laws stated as code, polymorphic in the category. A law of the structures @cs@ takes five
-- object variables and a supply of named arbitrary arrows, and returns an equation between two
-- arrows. The @proarrow:testing@ library checks laws by running them with random objects and
-- arrows, in a category whose arrows also carry their own description, so that a failing law
-- prints as the code it was built from.
--
-- A derived operation (a function defined from the class methods, like
-- 'Proarrow.Category.Monoidal.StarAutonomous.doubleNeg') would print as its definition. 'label'
-- names it instead.
--
-- A class's laws are an instance of 'Laws' for the list of structures they mention, the same list
-- the class's free-category structure requires, e.g.
-- @'Laws' 'Proarrow.Category.Monoidal.SymMonoidalStructures'@. The instances live next to their
-- classes.
module Proarrow.Tools.Laws where
import Data.Kind (Constraint, Type)
import Prelude (Applicative, Monad, String, pure, (++))
import Proarrow.Category.Enriched.Dagger (DaggerProfunctor (..))
import Proarrow.Category.Instance.Free (All)
import Proarrow.Core (CategoryOf (..), Hom, Kind, Profunctor (..), Promonad (..), type (+->))
import Proarrow.Profunctor.Corepresentable (Corepresentable (..), withObCorep)
import Proarrow.Profunctor.Representable (Representable (..), withObRep)
-- * Laws
-- | The laws of the structures @cs@. A class with a new kind of object also needs support in the
-- testing library before its laws can be checked, see "Proarrow.Testing.Laws.Run".
--
-- For example, the laws of a functor on objects @Sq@ with action @sq@ on arrows:
--
-- @
-- instance Laws '[HasSquare] where
-- laws =
-- [ Law "sq identity" \\ \@a _ -> withObSq \@_ \@a (sq (obj \@a) '===' id)
-- , Law "sq composition" \\ \@a \@b \@c mor -> do
-- f <- mor \@a \@b "f"
-- g <- mor \@b \@c "g"
-- sq (g . f) '===' sq g . sq f
-- ]
-- @
type Laws :: [Kind -> Constraint] -> Constraint
class Laws cs where
-- | The laws, each tested as its own property.
laws :: [Law cs]
-- | A named law.
type Law :: [Kind -> Constraint] -> Type
data Law cs = Law String (LawBody cs)
-- | The name of a law, used as its test's name.
lawName :: Law cs -> String
lawName (Law name _) = name
-- | The body of a 'Law': given five object variables and a supply of named arbitrary arrows,
-- produce an 'Equation'. A body binds as many of the variables as it uses, e.g. @\\ \@a \@b mor -> ...@,
-- and gives each arrow it asks for the name to print it as.
type LawBody :: [Kind -> Constraint] -> Type
type LawBody cs =
forall {k} (a :: k) (b :: k) (c :: k) (d :: k) (e :: k) m
. (Labelled k, All cs k, Monad m, Ob a, Ob b, Ob c, Ob d, Ob e)
=> (forall (x :: k) y. (Ob x, Ob y) => String -> m (x ~> y))
-> m (Equation k)
-- * Equations
infix 1 :=:
-- | Two parallel elements of a profunctor claimed to be equal, or an equation between arrows of its
-- codomain ('InK') or domain ('InJ').
type ProEquation :: forall {j} {k}. (j +-> k) -> Type
data ProEquation p where
(:=:) :: forall {j} {k} (p :: j +-> k) a b. p a b -> p a b -> ProEquation p
InK :: forall {j} {k} (p :: j +-> k). Equation k -> ProEquation p
InJ :: forall {j} {k} (p :: j +-> k). Equation j -> ProEquation p
-- | Two parallel arrows claimed to be equal: an equation between elements of the hom profunctor.
type Equation :: Kind -> Type
type Equation k = ProEquation (Hom k)
-- | The two sides of an equation between arrows. At the hom profunctor 'InK' and 'InJ' only wrap
-- another equation between arrows of the same category, and are looked through.
withSides :: forall {k} r. Equation k -> (forall (a :: k) b. a ~> b -> a ~> b -> r) -> r
withSides (l :=: r) f = f l r
withSides (InK e) f = withSides e f
withSides (InJ e) f = withSides e f
-- | The equations that an equation between arrows of @i@ can be the result of: those of a
-- profunctor law whose codomain ('InK') or domain ('InJ') is @i@. An 'Equation' is one of these,
-- at the hom profunctor. When the codomain and the domain are the same category either
-- constructor says the same, and 'InK' is used.
type ArrowEquation :: Kind -> Type -> Constraint
class ArrowEquation i r where
-- | An equation between arrows of @i@, as an @r@.
fromArrowEquation :: Equation i -> r
instance ArrowEquation k (ProEquation (p :: j +-> k)) where
fromArrowEquation = InK
instance {-# INCOHERENT #-} ArrowEquation j (ProEquation (p :: j +-> k)) where
fromArrowEquation = InJ
infix 1 ===
-- | An equation between arrows as the result of a law body, in whichever category of the law the
-- arrows are in: @l '===' r = 'pure' ('fromArrowEquation' (l ':=:' r))@.
(===) :: forall {i} m r (a :: i) b. (Applicative m, ArrowEquation i r) => a ~> b -> a ~> b -> m r
l === r = pure (fromArrowEquation (l :=: r :: Equation i))
infix 1 =:=
-- | An equation between elements as the result of a profunctor law body:
-- @l '=:=' r = 'pure' (l ':=:' r)@.
(=:=) :: forall {j} {k} m (p :: j +-> k) a b. (Applicative m) => p a b -> p a b -> m (ProEquation p)
l =:= r = pure (l :=: r)
-- * Inverses
-- | A pair of arrows claimed to be inverse to each other, see 'inverses'.
type Inverses :: Kind -> Type
data Inverses k where
Inverses :: forall {k} (a :: k) b. a ~> b -> b ~> a -> Inverses k
-- | The body of a law that asks for no arrows: given five object variables, an @r k@.
type PureLawBody :: [Kind -> Constraint] -> (Kind -> Type) -> Type
type PureLawBody cs r =
forall {k} (a :: k) (b :: k) (c :: k) (d :: k) (e :: k). (Labelled k, All cs k, Ob a, Ob b, Ob c, Ob d, Ob e) => r k
-- | @g . f = id@ and @f . g = id@ for @'Inverses' f g@.
leftInverse, rightInverse :: (CategoryOf k) => Inverses k -> Equation k
leftInverse (Inverses f g) = (g . f :=: id) \\ f
rightInverse (Inverses f g) = (f . g :=: id) \\ f
-- | The two laws saying that a pair of arrows @f@, @g@ are inverse to each other: @g@ is a left
-- and a right inverse of @f@.
inverses :: forall cs. String -> PureLawBody cs Inverses -> [Law cs]
inverses name body = [side " left inverse" leftInverse, side " right inverse" rightInverse]
where
side :: String -> (forall k. (CategoryOf k) => Inverses k -> Equation k) -> Law cs
side suffix eqn = Law (name ++ suffix) \ @a @b @c @d @e _ -> pure (eqn (body @a @b @c @d @e))
-- * Bijections
-- | Two maps between hom-sets claimed to be inverse to each other, see 'bijection', with how to
-- ask for an arrow of either hom-set.
type Bijection :: (Type -> Type) -> Kind -> Type
data Bijection m k where
Bijection
:: forall {k} m (a :: k) (b :: k) (c :: k) (d :: k)
. m (a ~> b) -> m (c ~> d) -> (a ~> b -> c ~> d) -> (c ~> d -> a ~> b) -> Bijection m k
-- | The body of a 'bijection': given five object variables and a supply of named arbitrary arrows,
-- the two maps, with how to ask for an arrow of each hom-set.
type BijectionBody :: [Kind -> Constraint] -> Type
type BijectionBody cs =
forall {k} (a :: k) (b :: k) (c :: k) (d :: k) (e :: k) m
. (Labelled k, All cs k, Monad m, Ob a, Ob b, Ob c, Ob d, Ob e)
=> (forall (x :: k) y. (Ob x, Ob y) => String -> m (x ~> y))
-> Bijection m k
-- | The two laws saying that maps @to@ and @from@ between hom-sets are inverse to each other:
-- @from (to f) = f@ and @to (from g) = g@. Each asks only for the arrow it needs, so an empty
-- hom-set on the other side discards nothing.
bijection :: forall cs. String -> BijectionBody cs -> [Law cs]
bijection name body =
[ Law (name ++ " left inverse") \ @a @b @c @d @e mor -> case body @a @b @c @d @e mor of
Bijection askF _ to from -> do
f <- askF
f === from (to f)
, Law (name ++ " right inverse") \ @a @b @c @d @e mor -> case body @a @b @c @d @e mor of
Bijection _ askG to from -> do
g <- askG
g === to (from g)
]
-- * The laws of a category
-- | 'id' is a unit for composition, which is associative.
instance Laws '[CategoryOf] where
laws =
[ Law "left identity" \ @a @b mor -> do
f <- mor @a @b "f"
f === id . f
, Law "right identity" \ @a @b mor -> do
f <- mor @a @b "f"
f === f . id
, Law "associativity" \ @a @b @c @d mor -> do
f <- mor @a @b "f"
g <- mor @b @c "g"
h <- mor @c @d "h"
h . (g . f) === (h . g) . f
]
-- * Profunctor laws
-- | The laws of the profunctor class @c@, for any profunctor @p@ with @c p@. The instances for
-- classes that "Proarrow.Category.Instance.Free" depends on live here.
type ProLaws :: forall {j} {k}. ((j +-> k) -> Constraint) -> Constraint
class ProLaws c where
-- | The laws, each tested as its own property.
proLaws :: [ProLaw c]
-- | A named profunctor law, about one element of the profunctor ('ProLaw') or three ('ProLaw3').
type ProLaw :: forall {j} {k}. ((j +-> k) -> Constraint) -> Type
data ProLaw c = ProLaw String (ProLawBody c) | ProLaw3 String (ProLawBody3 c)
-- | The name of a profunctor law, used as its test's name.
proLawName :: ProLaw c -> String
proLawName (ProLaw name _) = name
proLawName (ProLaw3 name _) = name
-- | The body of a 'ProLaw': given a profunctor @p :: j '+->' k@, six object variables alternating
-- between @k@ and @j@, an element @p :: p a b@ between the first two, and a supply of named
-- arbitrary arrows for each of @k@ and @j@, produce a 'ProEquation'. The element picks its
-- endpoints, so that a test can draw it where @p@ has elements, and a test draws the other
-- variables so that there are arrows @e '~>' c '~>' a@ and @b '~>' d '~>' f@. A body binds as many
-- of the variables as it uses, e.g. @\\ \@_ \@a \@b p morK _ -> ...@.
type ProLawBody :: forall {j} {k}. ((j +-> k) -> Constraint) -> Type
type ProLawBody (cl :: (j +-> k) -> Constraint) =
forall (p :: j +-> k) (a :: k) (b :: j) (c :: k) (d :: j) (e :: k) (f :: j) m
. (cl p, Labelled j, Labelled k, Monad m, Ob a, Ob b, Ob c, Ob d, Ob e, Ob f)
=> p a b
-> (forall (x :: k) y. (Ob x, Ob y) => String -> m (x ~> y))
-> (forall (x :: j) y. (Ob x, Ob y) => String -> m (x ~> y))
-> m (ProEquation p)
-- | The body of a 'ProLaw3': a 'ProLawBody' with three elements @p :: p a b@, @p' :: p c d@ and
-- @p'' :: p e f@, which pick all six object variables. A law that needs arbitrary objects uses the
-- endpoints of an element it does not otherwise use.
type ProLawBody3 :: forall {j} {k}. ((j +-> k) -> Constraint) -> Type
type ProLawBody3 (cl :: (j +-> k) -> Constraint) =
forall (p :: j +-> k) (a :: k) (b :: j) (c :: k) (d :: j) (e :: k) (f :: j) m
. (cl p, Labelled j, Labelled k, Monad m, Ob a, Ob b, Ob c, Ob d, Ob e, Ob f)
=> p a b
-> p c d
-> p e f
-> (forall (x :: k) y. (Ob x, Ob y) => String -> m (x ~> y))
-> (forall (x :: j) y. (Ob x, Ob y) => String -> m (x ~> y))
-> m (ProEquation p)
-- | 'dimap' preserves identities and composition, and 'lmap' and 'rmap' are its two halves.
instance ProLaws Profunctor where
proLaws =
[ ProLaw "dimap identity" \p _ _ -> p =:= dimap id id p
, ProLaw "dimap composition" \ @_ @a @b @c @d @e @f p morK morJ -> do
g <- morK @c @a "g"
h <- morJ @b @d "h"
g' <- morK @e @c "g'"
h' <- morJ @d @f "h'"
dimap (g . g') (h' . h) p =:= dimap g' h' (dimap g h p)
, ProLaw "lmap" \ @_ @a @_ @c p morK _ -> do
g <- morK @c @a "g"
lmap g p =:= dimap g id p
, ProLaw "rmap" \ @_ @_ @b @_ @d p _ morJ -> do
h <- morJ @b @d "h"
rmap h p =:= dimap id h p
]
-- | 'index' and 'tabulate' are inverse and natural, and 'repUniv' is @'tabulate' 'id'@.
instance ProLaws Representable where
proLaws =
[ ProLaw "tabulate . index" \p _ _ -> p =:= tabulate (index p)
, ProLaw "index . tabulate" \ @p @a @b _ morK _ -> withObRep @p @b do
g <- morK @a @(p % b) "g"
g === index (tabulate @p @b g)
, ProLaw "index naturality" \ @p @a @b @c @d p morK morJ -> do
g <- morK @c @a "g"
h <- morJ @b @d "h"
index (dimap g h p) === repMap @p h . index p . g
, ProLaw "repUniv" \ @p @_ @b _ _ _ -> withObRep @p @b (repUniv @p @b =:= tabulate id)
]
-- | 'coindex' and 'cotabulate' are inverse and natural, and 'corepUniv' is @'cotabulate' 'id'@.
instance ProLaws Corepresentable where
proLaws =
[ ProLaw "cotabulate . coindex" \p _ _ -> p =:= cotabulate (coindex p)
, ProLaw "coindex . cotabulate" \ @p @a @b _ _ morJ -> withObCorep @p @a do
h <- morJ @(p %% a) @b "h"
h === coindex (cotabulate @p @a h)
, ProLaw "coindex naturality" \ @p @a @b @c @d p morK morJ -> do
g <- morK @c @a "g"
h <- morJ @b @d "h"
coindex (dimap g h p) === h . coindex p . corepMap @p g
, ProLaw "corepUniv" \ @p @a _ _ _ -> withObCorep @p @a (corepUniv @p @a =:= cotabulate id)
]
-- | 'dagger' is an involution that reverses 'dimap'. At the hom profunctor these are the laws of a
-- dagger category.
instance ProLaws DaggerProfunctor where
proLaws =
[ ProLaw "dagger involution" \p _ _ -> p =:= dagger (dagger p)
, ProLaw "dagger dimap" \ @_ @a @b @c @d p morK morJ -> do
g <- morK @c @a "g"
h <- morJ @b @d "h"
dagger (dimap g h p) =:= dimap (dagger h) (dagger g) (dagger p)
]
-- * Naming arrows
-- | Categories whose arrows can be given a name, for printing laws. Naming leaves the arrow as it
-- is.
type Labelled :: Kind -> Constraint
class (CategoryOf k) => Labelled k where
-- | @'label' s f@ is @f@, printed as @s@.
label :: String -> (a :: k) ~> b -> a ~> b