packages feed

proarrow-0.1.0.0: src/Proarrow/Category/Instance/FinHask.hs

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

{- HLINT ignore "Use const" -}

-- | The category of __finite Haskell types__: objects are types with 'Universe'\/'Finite'
-- instances (wrapped in 'FH'), and a morphism is a function stored extensionally as a finite
-- lookup table ('Data.Map.Map'), so morphisms can be enumerated, shown and compared. A finite,
-- fully inspectable stand-in for "Proarrow.Category.Instance.Hask".
module Proarrow.Category.Instance.FinHask where

import Data.Coerce qualified as P
import Data.Containers.ListUtils (nubOrd)
import Data.Data (Proxy (..))
import Data.Kind (Type)
import Data.List (genericLength)
import Data.List qualified as P
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as M
import Data.Universe.Class (Finite (..), Universe (..))
import Data.Universe.Helpers (Tagged (..), retag)
import Data.Void (Void)
import GHC.TypeNats (KnownNat, Nat, natVal, withKnownNat, withSomeSNat)
import Numeric.Natural (Natural)
import Prelude (Bool (..), ($))
import Prelude qualified as P

import Proarrow.Category.Enriched.Finitary (Finitary (..), finiteSize)
import Proarrow.Category.Monoidal (Monoidal (..), MonoidalProfunctor (..), SymMonoidal (..))
import Proarrow.Category.Monoidal.Cartesian (distLProd, distRProd)
import Proarrow.Category.Monoidal.Closed (Closed (..), uncurry)
import Proarrow.Category.Monoidal.CopyDiscard (CopyDiscard)
import Proarrow.Category.Monoidal.Distributive (Distributive (..))
import Proarrow.Category.Topos (ElementaryTopos, HasEpiMonoFactorization (..), HasSubobjectClassifier (..))
import Proarrow.Colimit.BinaryCoproduct (HasBinaryCoproducts (..))
import Proarrow.Colimit.Coequalizer (HasCoequalizers (..))
import Proarrow.Colimit.Initial (HasInitialObject (..))
import Proarrow.Colimit.Pushout (HasPushouts (..))
import Proarrow.Core (CAT, CategoryOf (..), Is, Profunctor (..), Promonad (..), UN, dimapDefault)
import Proarrow.Limit.BinaryProduct
  ( HasBinaryProducts (..)
  , associatorProd
  , associatorProdInv
  , diag
  , leftUnitorProd
  , leftUnitorProdInv
  , rightUnitorProd
  , rightUnitorProdInv
  , swapProd
  )
import Proarrow.Limit.Equalizer (HasEqualizers (..))
import Proarrow.Limit.Pullback (HasPullbacks (..))
import Proarrow.Limit.Terminal (HasTerminalObject (..))
import Proarrow.Monoid (CocommutativeComonoid, Comonoid (..), Monoid (..))
import Proarrow.Profunctor.Instance.Composition ((:.:) (..))

newtype Fin (n :: Nat) = Fin {unFin :: P.Int}
  deriving newtype (P.Eq, P.Ord, P.Show, P.Num)

instance (KnownNat n) => Universe (Fin n) where
  universe = P.coerce @[P.Int] [0 .. (P.fromIntegral (natVal (Proxy @n)) P.- 1)]

instance (KnownNat n) => Finite (Fin n) where
  cardinality = Tagged (natVal (Proxy @n))

type data FINHASK = FH Type

type FinHask :: CAT FINHASK
data FinHask a b where
  FinHask :: (Ob (FH a), Ob (FH b)) => {unFinHask :: Map a b} -> FinHask (FH a) (FH b)

instance P.Show (FinHask a b) where
  show (FinHask m) = P.show m
deriving instance P.Eq (FinHask a b)
deriving instance P.Ord (FinHask a b)
instance (Ob a, Ob b) => Universe (FinHask a b) where
  universe = fromList P.<$> P.traverse (\a -> (a,) P.<$> universe) universe
instance (Ob a, Ob b) => Finite (FinHask a b) where
  cardinality =
    P.liftA2
      (P.^)
      (retag @_ @_ @(FinHask a b) (cardinality @(UN FH b)))
      (retag @_ @_ @(FinHask a b) (cardinality @(UN FH a)))

(!) :: (P.Ord (UN FH a)) => FinHask a b -> UN FH a -> UN FH b
FinHask m ! a = case M.lookup a m of
  P.Just x -> x
  P.Nothing -> P.error $ "Index " P.++ P.show a P.++ " out of bounds for " P.++ P.show m

arr :: (Ob (FH a), Ob (FH b)) => (a -> b) -> FinHask (FH a) (FH b)
arr f = fromList [(x, f x) | x <- universeF]

reifyList :: [a] -> (forall l. (Ob (FH l)) => Map l a -> r) -> r
reifyList xs k =
  withSomeSNat (genericLength xs) \ @n snat ->
    withKnownNat snat (k @(Fin n) (M.fromList (P.zip universeF xs)))

fromList :: (Ob (FH a), Ob (FH b)) => [(a, b)] -> FinHask (FH a) (FH b)
fromList = FinHask . M.fromList

toList :: (Ob (FH a), Ob (FH b)) => FinHask (FH a) (FH b) -> [(a, b)]
toList (FinHask m) = M.toList m

instance Profunctor FinHask where
  dimap = dimapDefault
  r \\ FinHask{} = r
instance Promonad FinHask where
  id = arr id
  FinHask l . FinHask r = FinHask (P.fmap (l M.!) r)

-- | The category of finite Haskell types, with morphisms stored extensionally as finite lookup
-- tables.
instance CategoryOf FINHASK where
  type (~>) = FinHask
  type Ob a = (Is FH a, Finite (UN FH a), P.Ord (UN FH a), P.Show (UN FH a))

instance HasInitialObject FINHASK where
  type InitialObject = FH Void
  initiate = FinHask M.empty
instance HasBinaryCoproducts FINHASK where
  type FH a || FH b = FH (P.Either a b)
  withObCoprod r = r
  lft = arr P.Left
  rgt = arr P.Right
  FinHask l ||| FinHask r = FinHask (M.mapKeys P.Left l P.<> M.mapKeys P.Right r)

instance HasTerminalObject FINHASK where
  type TerminalObject = FH ()
  terminate = arr \_ -> ()
instance HasBinaryProducts FINHASK where
  type FH a && FH b = FH (a, b)
  withObProd r = r
  fst = arr P.fst
  snd = arr P.snd
  FinHask l &&& FinHask r =
    FinHask
      ( M.mergeWithKey
          (\_ a b -> P.Just (a, b))
          (\_ -> M.empty)
          (\_ -> M.empty)
          l
          r
      )

instance MonoidalProfunctor FinHask where
  one = id
  (**) = (***)

instance Monoidal FINHASK where
  type a ** b = a && b
  type Unit = TerminalObject
  withOb2 @a @b = withObProd @_ @a @b
  leftUnitor = leftUnitorProd
  leftUnitorInv = leftUnitorProdInv
  rightUnitor = rightUnitorProd
  rightUnitorInv = rightUnitorProdInv
  associator @a @b @c = associatorProd @a @b @c
  associatorInv @a @b @c = associatorProdInv @a @b @c

instance SymMonoidal FINHASK where
  swap @a @b = swapProd @a @b

instance Closed FINHASK where
  type a ~~> b = FH (FinHask a b)
  withObExp r = r
  curry f@FinHask{} = arr \a -> arr \b -> f ! (a, b)
  apply = arr (uncurry (!))

-- | Where a value sits in its own type's 'universe'.
position :: forall x. (Finite x, P.Eq x) => x -> Natural
position x = case P.elemIndex x universeF of
  P.Just i -> P.fromIntegral i
  P.Nothing -> P.error "position: not in the universe of its type"

-- | The hom-sets of 'FINHASK' are finite, so its hom-profunctor is finitary. It is numbered in the
-- order of the 'universe' the 'Finite' instance above enumerates, but arithmetically instead of by
-- searching it. A morphism is a table of values indexed by @'universeF' \@a@. Reading that table
-- as a numeral in base @|b|@, most significant digit first, gives @universe@\'s own order, since
-- @universe@ is @'P.traverse' (\a -> (a,) '<$>' universe) universe@ and for lists @'<*>'@ varies
-- its right operand fastest. So it is the /last/ element of @a@ that varies fastest.
instance Finitary FinHask where
  size @a @b = finiteSize @FinHask @a @b
  toIndex @(FH a) @(FH b) f = P.foldl (\acc x -> acc P.* card @b P.+ position (f ! x)) 0 (universeF @a)
  fromIndex @(FH a) @(FH b) i = fromList (P.zip xs (digits (P.length xs) i))
    where
      xs = universeF @a
      digits :: P.Int -> Natural -> [b]
      digits 0 _ = []
      digits n m = case card @b of
        0 -> P.error "fromIndex: the source is inhabited and the target is empty, so there are no morphisms"
        c -> let (q, r) = m `P.divMod` c in digits (n P.- 1) q P.++ [universeF @b `P.genericIndex` r]

-- | How many inhabitants a type has.
card :: forall x. (Finite x) => Natural
card = unTagged (cardinality @x)

instance Distributive FINHASK where
  distL @a @b @c = distLProd @a @b @c
  distR @a @b @c = distRProd @a @b @c
  absorbL = FinHask M.empty
  absorbR = FinHask M.empty

instance (Ob (FH a)) => Comonoid (FH a) where
  counit = terminate
  comult = diag
instance (Ob (FH a)) => CocommutativeComonoid (FH a)

instance CopyDiscard FINHASK

instance Monoid (FH ()) where
  mempty = terminate
  mappend = terminate

-- | >>> let f :: FinHask (FH (Fin 4)) (FH (Fin 3)) = fromList [(0,0), (1,1), (2,1), (3,0)]
-- >>> let g :: FinHask (FH (Fin 4)) (FH (Fin 3)) = fromList [(0,2), (1,0), (2,1), (3,0)]
-- >>> let h :: FinHask (FH (Fin 3)) (FH (Fin 4)) = fromList [(0,3), (1,2), (2,3)]
-- >>> (equalize f g \incl -> let p = factorEqualizer incl h in P.show (incl, p, incl . p)) :: P.String
-- "(fromList [(0,2),(1,3)],fromList [(0,1),(1,0),(2,1)],fromList [(0,3),(1,2),(2,3)])"
instance HasEqualizers FINHASK where
  equalize f@FinHask{} g k =
    let groups = [x | x <- universeF, f ! x P.== g ! x]
    in reifyList groups \e -> k (FinHask e)
  factorEqualizer (FinHask incl) (FinHask h) =
    let invIncl = M.fromList [(v, ky) | (ky, v) <- M.toList incl]
    in FinHask ((invIncl M.!) P.<$> h)

-- | Example 3.84 of Seven Sketches (A: 0=red, 1=blue, 2=black)
-- >>> data Color = Red | Blue | Black deriving (P.Eq, P.Ord, P.Show, P.Enum, P.Bounded, Universe, Finite)
-- >>> let f :: FinHask (FH (Fin 6)) (FH Color) = fromList [(0,Red), (1,Blue), (2,Red), (3,Red), (4,Black), (5,Blue)]
-- >>> let g :: FinHask (FH (Fin 4)) (FH Color) = fromList [(0,Black), (1,Red), (2,Blue), (3,Red)]
-- >>> (pullback f g \(FinHask l) (FinHask r) -> P.show (P.zip (M.elems l) (M.elems r))) :: P.String
-- "[(0,1),(0,3),(1,2),(2,1),(2,3),(3,1),(3,3),(4,0),(5,2)]"
instance HasPullbacks FINHASK where
  pullback (FinHask f) (FinHask g) k =
    let
      gByValue = M.fromListWith (P.flip (P.++)) [(v, [y]) | (y, v) <- M.toList g]
      groups = [(x, y) | (x, v) <- M.toList f, y <- M.findWithDefault [] v gByValue]
    in
      reifyList groups \e -> k (FinHask (P.fst P.<$> e)) (FinHask (P.snd P.<$> e))

instance HasCoequalizers FINHASK where
  coequalize (FinHask @_ @b f) (FinHask g) k =
    let
      find m i = P.maybe i (find m) $ M.lookup i m
      union m (i, j) = let ri = find m i; rj = find m j in if ri P.== rj then m else M.insert ri rj m
      unionFind = P.foldl union M.empty (P.zip (M.elems f) (M.elems g))
      step m x = M.insertWith (P.++) (find unionFind x) [x] m
      groups = M.elems $ P.foldl step M.empty (universeF @b)
    in
      reifyList groups \ce ->
        let invMap = M.fromList $ P.concatMap (\(l, bs) -> P.map (,l) bs) $ M.toList ce
        in k (FinHask invMap)
  factorCoequalizer (FinHask q) (FinHask h) =
    let reps = M.fromListWith (\_ old -> old) [(v, ky) | (ky, v) <- M.toList q]
    in FinHask ((h M.!) P.<$> reps)

-- | Exercise 6.22 of Seven Sketches
-- >>> let l :: FinHask (FH (Fin 4)) (FH (Fin 3)) = fromList [(0,0), (1,0), (2,1), (3,2)]
-- >>> let r :: FinHask (FH (Fin 4)) (FH (Fin 5)) = fromList [(0,0), (1,2), (2,4), (3,4)]
-- >>> (pushout l r \l' r' -> P.show (l', r')) :: P.String
-- "(fromList [(0,1),(1,3),(2,3)],fromList [(0,1),(1,0),(2,1),(3,2),(4,3)])"
instance HasPushouts FINHASK

-- | >>> import Proarrow.Colimit.Pushout (isEpi)
-- >>> let f :: FinHask (FH (Fin 3)) (FH (Fin 3)) = fromList [(0,2), (1,0), (2,1)]
-- >>> (pushout f f \(FinHask g1) (FinHask g2) -> P.show (g1, g2)) :: P.String
-- "(fromList [(0,0),(1,1),(2,2)],fromList [(0,0),(1,1),(2,2)])"
-- >>> isEpi (f :: FinHask (FH (Fin 3)) (FH (Fin 3)))
-- True
-- >>> import Proarrow.Limit.Pullback (isMono)
-- >>> (pullback f f \(FinHask l) (FinHask r) -> P.show (l, r)) :: P.String
-- "(fromList [(0,0),(1,1),(2,2)],fromList [(0,0),(1,1),(2,2)])"
-- >>> isMono f
-- True
-- >>> import Proarrow.Category.Topos (classifyImage, classifyKernelPair, and, or, implies, false)
-- >>> (case factorize f of p :.: q -> P.show (p, q) \\ p \\ q) :: P.String
-- "(fromList [(0,0),(1,1),(2,2)],fromList [(0,2),(1,0),(2,1)])"
-- >>> (classifyImage f, classifyKernelPair f)
-- (fromList [(0,True),(1,True),(2,True)],fromList [((0,0),True),((0,1),False),((0,2),False),((1,0),False),((1,1),True),((1,2),False),((2,0),False),((2,1),False),((2,2),True)])
-- >>> [and, or, implies] :: [FinHask (FH (Bool, Bool)) (FH Bool)]
-- [fromList [((False,False),False),((False,True),False),((True,False),False),((True,True),True)],fromList [((False,False),False),((False,True),True),((True,False),True),((True,True),True)],fromList [((False,False),True),((False,True),True),((True,False),False),((True,True),True)]]
-- >>> false :: FinHask (FH ()) (FH Bool)
-- fromList [((),False)]
instance HasSubobjectClassifier FINHASK where
  type Omega = FH Bool
  true = arr \_ -> True
  classifyGraph f@FinHask{} = arr \(a, b) -> f ! a P.== b

instance HasEpiMonoFactorization FINHASK where
  factorize (FinHask f) = reifyList (nubOrd (M.elems f)) \lb ->
    let invMap = M.fromList [(lb M.! l, l) | l <- universeF]
    in FinHask (P.fmap (invMap M.!) f) :.: FinHask lb

instance ElementaryTopos FINHASK