circuits-0.2.0.0: src/Circuit/Algebra.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeAbstractions #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Change-of-base algebras for modular circuit syntax.
--
-- A 'Circuit.Layer.Layer' evals a free construction back into the /same/
-- base arrow. An 'Algebra' generalises this by allowing the target
-- category to differ from the source: it interprets syntax built over
-- @arr@ into morphisms of some other category @arr'@. The map
-- @emb :: arr :~> arr'@ handles the base-arrow generators, while
-- @eval@ handles the recursive sub-terms.
--
-- In this picture, 'Syntax sig arr' is the tree, 'Algebra sig arr arr''
-- is the interpreter, 'alg' evaluates a single constructor, and 'eval'
-- evals the whole tree. When @arr' = arr@ and @emb = id@, an 'Algebra'
-- collapses to the single universal eval that 'Layer' captures.
--
-- Each language feature is a signature functor. A GADT is the free
-- construction over a chosen combination of signatures. This makes the
-- design space a lattice: start with the features you need, add more
-- when you need them, and forget them via algebras.
--
-- Signatures expose the design space as a lattice: start with the
-- features you need, add more when you need them, and forget them via
-- algebras. The direct GADTs in "Circuit.Loop" and "Circuit.Net" are
-- the canonical circuit types; this module gives those constructions as
-- compositional syntax.
--
-- The signatures are:
--
-- * @SigCompose@ — sequential composition
-- * @SigKnot@ — feedback / trace over a tensor @t@
-- * @SigPar@ — parallel composition
-- * @SigSwap@ — symmetric braiding
-- * 'SigBimonoid'— copy, discard, plus, zero
--
-- Examples:
--
-- * @'Syntax' @SigCompose@ arr@ — free category
-- * @'Syntax' (@SigCompose@ ':+:' @SigKnot@ t) arr@ — free traced category
-- * @'Syntax' (@SigCompose@ ':+:' @SigPar@ ':+:' @SigSwap@) arr@ — free monoidal category
-- * @'Syntax' (@SigCompose@ ':+:' @SigKnot@ t ':+:' @SigPar@ ':+:' @SigSwap@ ':+:' 'SigBimonoid') arr@ — Net
module Circuit.Algebra
( -- * Signatures
Sig,
(:+:) (..),
-- * Syntax and algebra
Syntax (..),
Algebra (..),
eval,
evalInto,
-- * Individual signatures
SigCompose (..),
SigKnot (..),
SigPar (..),
SigSwap (..),
SigBimonoid (..),
-- * Common syntax combinations
AlgCat,
AlgLoop,
AlgSym,
AlgBimonoidal,
AlgNet,
-- * Direct <-> algebra isomorphisms
algLoop,
runAlgLoop,
algNet,
runAlgNet,
)
where
import Circuit.Category (Category (..), Discrete (..))
import Circuit.Channel (Channel (..), Strength (..), Traced (..))
import Circuit.Dagger qualified as Dg
import Circuit.Layer (Layer, run)
import Circuit.Loop qualified as C
import Circuit.Net qualified as N
import Circuit.Tensor (Action (..), Tensor (..))
import Data.Kind (Constraint, Type)
import Prelude hiding (id, (.))
-- ---------------------------------------------------------------------------
-- Signature functors
-- | A signature describes a set of constructors for a profunctor.
--
-- * @arr@ — the base arrow (used for constructor constraints)
-- * @rec@ — the recursive arrow type being defined
-- * @a@, @b@ — input and output objects
type Sig = (Type -> Type -> Type) -> (Type -> Type -> Type) -> Type -> Type -> Type
-- | Coproduct of signatures.
data (sig1 :+: sig2) arr rec a b where
L :: sig1 arr rec a b -> (sig1 :+: sig2) arr rec a b
R :: sig2 arr rec a b -> (sig1 :+: sig2) arr rec a b
infixr 6 :+:
-- ---------------------------------------------------------------------------
-- Free construction over a signature.
-- | The free construction over a signature.
data Syntax (sig :: Sig) (arr :: Type -> Type -> Type) a b where
Lift :: arr a b -> Syntax sig arr a b
Op :: sig arr (Syntax sig arr) a b -> Syntax sig arr a b
-- | Algebra for a signature. Interprets operations of a signature over
-- source arrow @arr@ into a target arrow @arr'@.
--
-- * @emb@ maps base arrows of the source into the target.
-- * @rec@ maps recursive sub-terms into the target.
class Algebra (sig :: Sig) (arr :: Type -> Type -> Type) (arr' :: Type -> Type -> Type) where
type Ctx sig arr arr' :: Constraint
type Ctx sig arr arr' = ()
alg ::
(Ctx sig arr arr') =>
(forall x y. arr x y -> arr' x y) ->
(forall x y. rec x y -> arr' x y) ->
sig arr rec a b ->
arr' a b
-- | Coproduct algebra dispatches to the appropriate component.
instance (Algebra sig1 arr arr', Algebra sig2 arr arr') => Algebra (sig1 :+: sig2) arr arr' where
type Ctx (sig1 :+: sig2) arr arr' = (Ctx sig1 arr arr', Ctx sig2 arr arr')
alg emb rec (L op) = alg emb rec op
alg emb rec (R op) = alg emb rec op
-- | Fold a free construction into a target arrow using its algebra.
--
-- The embedding @emb@ maps base arrows of the source into the target.
-- For folding to the same arrow, use 'eval'.
--
-- This is the à la carte analogue of 'Circuit.Layer.bind': @evalInto emb@
-- folds syntax into a target just as @bind h@ folds a 'Circuit.Layer.Layer'
-- construction. For example, folding 'AlgNet' into 'AlgLoop' is just
-- @'evalInto' 'Lift'@, playing the same role as a structural forgetting map
-- built with @bind unit@.
--
-- A signature like @@SigKnot@ t@ is best read as a type-level tag that tracks
-- which constructors are present in the union; the coproduct @(':+:')@ is the
-- union of those tags.
evalInto ::
(Category arr', Algebra sig arr arr', Ctx sig arr arr') =>
(forall x y. arr x y -> arr' x y) ->
Syntax sig arr a b ->
arr' a b
evalInto emb (Lift f) = emb f
evalInto emb (Op op) = alg emb (evalInto emb) op
-- | Fold a free construction into its own base arrow.
eval ::
(Category arr, Algebra sig arr arr, Ctx sig arr arr) =>
Syntax sig arr a b ->
arr a b
eval = evalInto id
-- ---------------------------------------------------------------------------
-- Individual signatures
-- | Sequential composition.
data SigCompose arr rec a b where
SigCompose :: (Ob arr b) => rec b c -> rec a b -> SigCompose arr rec a c
instance Algebra SigCompose arr arr' where
type Ctx SigCompose arr arr' = Discrete arr'
alg ::
forall (rec :: Type -> Type -> Type) (a :: Type) (c :: Type).
(Ctx SigCompose arr arr') =>
(forall x y. arr x y -> arr' x y) ->
(forall x y. rec x y -> arr' x y) ->
SigCompose arr rec a c ->
arr' a c
alg _ rec (SigCompose @_ @b1 @_ @_ @_ g f) =
withOb @arr' @a $
withOb @arr' @b1 $
withOb @arr' @c $
(rec g . rec f)
-- | Feedback loop / trace over tensor @t@.
data SigKnot (t :: Type -> Type -> Type) arr rec a b where
SigKnot :: (Ob arr a) => rec (t a b) (t a c) -> SigKnot t arr rec b c
instance (Traced t arr') => Algebra (SigKnot t) arr arr' where
type Ctx (SigKnot t) arr arr' = (Traced t arr', Discrete arr')
alg ::
forall (rec :: Type -> Type -> Type) (b :: Type) (c :: Type).
(Ctx (SigKnot t) arr arr') =>
(forall x y. arr x y -> arr' x y) ->
(forall x y. rec x y -> arr' x y) ->
SigKnot t arr rec b c ->
arr' b c
alg _ rec (SigKnot @_ @a1 @_ @_ @_ @_ f) =
withOb @arr' @a1 $
withOb @arr' @b $
withOb @arr' @c $
withOb @arr' @(t a1 b) $
withOb @arr' @(t a1 c) $
trace (rec f)
-- | Parallel composition.
data SigPar arr rec a b where
SigPar :: rec a b -> rec c d -> SigPar arr rec (a, c) (b, d)
instance (Tensor (,) arr') => Algebra SigPar arr arr' where
type Ctx SigPar arr arr' = Tensor (,) arr'
alg _ rec (SigPar f g) = par (rec f) (rec g)
-- | Symmetric braiding.
data SigSwap arr rec a b where
SigSwap :: SigSwap arr rec (a, b) (b, a)
instance (Action (,) arr') => Algebra SigSwap arr arr' where
type Ctx SigSwap arr arr' = Action (,) arr'
alg _ _ SigSwap = swap
-- | Bimonoid operations: copy, discard, plus, zero.
--
-- Each constructor carries its own 'Dg.Bimonoid' constraint, resolved at
-- pattern-match time rather than in the algebra context.
data SigBimonoid arr rec a b where
SigCopy :: (Dg.Bimonoid arr a) => SigBimonoid arr rec a (a, a)
SigDiscard :: (Dg.Bimonoid arr a) => SigBimonoid arr rec a ()
SigPlus :: (Dg.Bimonoid arr a) => SigBimonoid arr rec (a, a) a
SigZero :: (Dg.Bimonoid arr a) => SigBimonoid arr rec () a
-- | [Conditional] 'alg' for bimonoid generators sends each generator to the
-- image under @emb@ of the source dictionary. This is the free-PROP fold
-- only when @emb@ is a bimonoid homomorphism; it is automatic for the
-- generator embedding, but must be checked for custom embeddings.
instance Algebra SigBimonoid arr arr' where
alg ::
forall rec i o.
(forall x y. arr x y -> arr' x y) ->
(forall x y. rec x y -> arr' x y) ->
SigBimonoid arr rec i o ->
arr' i o
alg emb _ SigCopy = emb (Dg.copy :: arr i (i, i))
alg emb _ SigDiscard = emb (Dg.discard :: arr i ())
alg emb _ SigPlus = emb (Dg.plus :: arr (o, o) o)
alg emb _ SigZero = emb (Dg.zero :: arr () o)
-- ---------------------------------------------------------------------------
-- Common syntax combinations
-- | Free category.
type AlgCat arr = Syntax SigCompose arr
-- | Free traced monoidal category over tensor @t@.
type AlgLoop t arr = Syntax (SigCompose :+: SigKnot t) arr
-- | Free monoidal category.
type AlgSym arr = Syntax (SigCompose :+: SigPar :+: SigSwap) arr
-- | Free bimonoidal category.
type AlgBimonoidal arr = Syntax (SigCompose :+: SigPar :+: SigSwap :+: SigBimonoid) arr
-- | Free traced PROP with bimonoid.
type AlgNet t arr = Syntax (SigCompose :+: SigKnot t :+: SigPar :+: SigSwap :+: SigBimonoid) arr
-- ---------------------------------------------------------------------------
-- Instances for signature-based categories
instance (Category arr) => Category (AlgCat arr) where
type Ob (AlgCat arr) a = Ob arr a
id = Lift id
f . g = Op (SigCompose f g)
instance (Category arr) => Category (AlgLoop t arr) where
type Ob (AlgLoop t arr) a = Ob arr a
id = Lift id
f . g = Op (L (SigCompose f g))
instance (Category arr, Channel t arr) => Channel t (AlgLoop t arr) where
assoc = Lift assoc
assoc' = Lift assoc'
slide = Lift slide
instance (Category arr, Traced t arr, Discrete arr) => Strength t (AlgLoop t arr) where
strength f = Lift (strength (eval f))
instance (Category arr, Traced t arr, Discrete arr) => Traced t (AlgLoop t arr) where
trace body = Op (R (SigKnot body))
instance (Category arr, Traced t arr, Tensor (,) arr, Discrete arr) => Tensor (,) (AlgLoop t arr) where
par f g = Lift (par (eval f) (eval g))
unitl = Lift unitl
unitl' = Lift unitl'
unitr = Lift unitr
unitr' = Lift unitr'
instance (Category arr, Traced t arr, Action (,) arr, Discrete arr) => Action (,) (AlgLoop t arr) where
swap = Lift swap
instance (Category arr, Channel t arr) => Channel t (AlgCat arr) where
assoc = Lift assoc
assoc' = Lift assoc'
slide = Lift slide
instance (Category arr, Strength t arr, Discrete arr) => Strength t (AlgCat arr) where
strength f = Lift (strength (eval f))
instance (Category arr, Traced t arr, Discrete arr) => Traced t (AlgCat arr) where
trace body = Lift (trace (eval body))
-- | A discrete base yields discrete syntax.
--
-- These instances are needed so that 'evalInto Lift' can fold richer syntax
-- into poorer syntax (e.g. 'AlgLoop' into 'AlgCat', 'AlgNet' into 'AlgLoop').
instance (Category arr, Discrete arr) => Discrete (AlgCat arr) where
withOb @a x = withOb @arr @a x
instance (Category arr, Discrete arr) => Discrete (AlgLoop t arr) where
withOb @a x = withOb @arr @a x
-- ---------------------------------------------------------------------------
-- Direct <-> algebra isomorphisms
-- | Embed the direct 'C.Loop' GADT into the signature-based form.
algLoop :: forall t arr a b. C.Loop t arr a b -> AlgLoop t arr a b
algLoop (C.Lift f) = Lift f
algLoop (C.Knot f) = Op (R (SigKnot (Lift f)))
-- | Project the signature-based circuit back to the direct GADT.
--
-- @SigCompose@ nodes are interpreted using the 'Category' instance of
-- 'C.Loop', so the result is in normal form (at most one 'C.Knot').
runAlgLoop ::
forall t a b.
(Traced t (->)) =>
AlgLoop t (->) a b ->
C.Loop t (->) a b
runAlgLoop (Lift f) = C.Lift f
runAlgLoop (Op op) = go op
where
go ::
forall x y.
(SigCompose :+: SigKnot t) (->) (AlgLoop t (->)) x y ->
C.Loop t (->) x y
go (L (SigCompose g f)) = runAlgLoop g . runAlgLoop f
go (R (SigKnot @_ f)) = C.Knot (run (runAlgLoop f))
-- | Embed the direct 'N.Net' GADT into the signature-based form.
algNet :: forall t arr a b. N.Net t arr a b -> AlgNet t arr a b
algNet (N.Lift f) = Lift f
algNet (N.Compose g f) = Op (L (SigCompose (algNet g) (algNet f)))
algNet (N.Par f g) = Op (R (R (L (SigPar (algNet f) (algNet g)))))
algNet N.Swap = Op (R (R (R (L SigSwap))))
algNet N.Copy = Op (R (R (R (R SigCopy))))
algNet N.Discard = Op (R (R (R (R SigDiscard))))
algNet N.Plus = Op (R (R (R (R SigPlus))))
algNet N.Zero = Op (R (R (R (R SigZero))))
algNet (N.Knot f) = Op (R (L (SigKnot (algNet f))))
-- | Project the signature-based Net back to the direct GADT.
runAlgNet :: forall t arr a b. AlgNet t arr a b -> N.Net t arr a b
runAlgNet = goTop
where
goTop :: forall x y. AlgNet t arr x y -> N.Net t arr x y
goTop (Lift f) = N.Lift f
goTop (Op op) = goOp op
goOp :: forall x y. (SigCompose :+: SigKnot t :+: SigPar :+: SigSwap :+: SigBimonoid) arr (AlgNet t arr) x y -> N.Net t arr x y
goOp (L sc) = goCompose sc
goOp (R rest) = goKnotOrMore rest
goCompose :: forall x y. SigCompose arr (AlgNet t arr) x y -> N.Net t arr x y
goCompose (SigCompose g f) = N.Compose (goTop g) (goTop f)
goKnotOrMore :: forall x y. (SigKnot t :+: SigPar :+: SigSwap :+: SigBimonoid) arr (AlgNet t arr) x y -> N.Net t arr x y
goKnotOrMore (L sk) = goKnot sk
goKnotOrMore (R rest) = goParOrMore rest
goKnot :: forall x y. SigKnot t arr (AlgNet t arr) x y -> N.Net t arr x y
goKnot (SigKnot f) = N.Knot (goTop f)
goParOrMore :: forall x y. (SigPar :+: SigSwap :+: SigBimonoid) arr (AlgNet t arr) x y -> N.Net t arr x y
goParOrMore (L sp) = goPar sp
goParOrMore (R rest) = goSwapOrBimonoid rest
goPar :: forall x y. SigPar arr (AlgNet t arr) x y -> N.Net t arr x y
goPar (SigPar f g) = N.Par (goTop f) (goTop g)
goSwapOrBimonoid :: forall x y. (SigSwap :+: SigBimonoid) arr (AlgNet t arr) x y -> N.Net t arr x y
goSwapOrBimonoid (L SigSwap) = N.Swap
goSwapOrBimonoid (R sb) = goBimonoid sb
goBimonoid :: forall x y. SigBimonoid arr (AlgNet t arr) x y -> N.Net t arr x y
goBimonoid SigCopy = N.Copy
goBimonoid SigDiscard = N.Discard
goBimonoid SigPlus = N.Plus
goBimonoid SigZero = N.Zero