packages feed

proarrow-0.1.0.0: test/Props/Dot.hs

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

module Props.Dot where

import Control.Monad (replicateM)
import Data.Containers.ListUtils (nubOrd)
import Data.List qualified as List
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Ord (comparing)
import Data.Proxy (Proxy (..))
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Type.Equality ((:~:) (..))
import Data.Void (absurd)
import GHC.TypeLits (Symbol, decideSymbol, symbolVal)
import Test.Falsify.Generator (Gen, elem)
import Test.Tasty (TestTree, testGroup)
import Prelude hiding (elem, fst, id, snd, (.))

import Proarrow.Category.Monoidal (withOb2)
import Proarrow.Category.Monoidal.Strictified (IsList (..))
import Proarrow.Core (CategoryOf (..), Promonad (..), UN)
import Proarrow.Tools.Diagrams.Dot
  ( DOT (..)
  , Dot (..)
  , DotData (..)
  , Fin (..)
  , NodeKind (..)
  , SymRefl (..)
  , Vec (..)
  , getData
  , len
  , names
  , node
  , nodeOf
  , portOf
  )

import Proarrow.Testing
  ( GenTotal (..)
  , Some (..)
  , Testable (..)
  , TestableProfunctor
  , TestableType (..)
  , TestingEqShow (..)
  , genSomeDef
  , oneElem
  , pattern GenNonEmpty
  )
import Proarrow.Testing.Laws

test :: TestTree
test =
  testGroup
    "Dot"
    [ testCategory @DOT
    , testMonoidal_ @DOT
    , testSymMonoidal_ @DOT
    , testCopyDiscard_ @DOT
    , testMonoid_ @(D '[])
    , testMonoid_ @(D '["A"])
    , testMonoid_ @(D '["A", "B"])
    , testCommutativeMonoid_ @(D '["A", "B"])
    , testComonoid_ @(D '["A", "B"])
    , testHypergraph @DOT (\ @a @b r -> withOb2 @DOT @a @b r)
    , testClosed_ @DOT
    , testStarAutonomous_ @DOT
    , testCompactClosed_ @DOT
    , testTraced_ @DOT
    ]

foldSome :: [Some Symbol] -> Some DOT
foldSome [] = Some @(D '[])
foldSome [Some @n] = Some @(D '[n])
foldSome (Some @n : Some @m : rest) = case foldSome (Some @m : rest) of
  Some @(D ns) -> withIsList2 @'[n] @ns (Some @(D (n ': ns)))

instance Testable Symbol where
  genSome = genSomeDef @'["A", "B", "C", "D", "E"]
  showOb @s = symbolVal (Proxy @s)

instance (Ob a, Ob b) => TestingEqShow (SymRefl a b)
instance (Ob a, Ob b) => TestableType (SymRefl a b) where
  gen = case decideSymbol (Proxy @a) (Proxy @b) of
    Right Refl -> oneElem SymRefl
    Left f -> GenEmpty \SymRefl -> absurd (f Refl)
instance TestableProfunctor SymRefl
instance Testable DOT where
  genSome = do
    num <- elem [0 .. 2]
    somes <- replicateM num (genSome @Symbol)
    pure $ foldSome somes
  showOb @ns = List.intercalate "," $ unVec $ names @(UN D ns)

-- | Two diagrams are equal when they mean the same relation ('meaning'), under a few different
-- meanings of the labelled nodes.
instance (Ob a, Ob b) => TestingEqShow (Dot a b) where
  eqP l r = let dl = getData l; dr = getData r in pure (all (\seed -> meaning seed dl == meaning seed dr) ([1, 2, 3] :: [Int]))

-- | A diagram through the library's own operations, so that its nodes are numbered as the other
-- arrows' are: one labelled node from the inputs to the outputs, or two stacked through a random
-- boundary in between.
instance (Ob a, Ob b) => TestableType (Dot a b) where
  gen = GenNonEmpty (boxes @a @b \ @x @y -> node @(UN D x) @(UN D y))

-- | One box, with a random label, from the inputs to the outputs, or two stacked through a random
-- boundary in between, each made by the given function from its label.
boxes
  :: forall {k} (a :: k) (b :: k)
   . (Testable k, Ob a, Ob b)
  => (forall (x :: k) (y :: k). (Ob x, Ob y) => String -> x ~> y)
  -> Gen (a ~> b)
boxes box = do
  stacked <- elem [False, True]
  if stacked
    then do
      Some @m <- genSome @k
      (.) <$> labelled @m @b <*> labelled @a @m
    else labelled @a @b
  where
    labelled :: forall (x :: k) (y :: k). (Ob x, Ob y) => Gen (x ~> y)
    labelled = box @x @y <$> elem ["f", "g", "h"]

-- * What a diagram means

-- | A wire of a diagram: an input, an output, or an edge between two nodes.
data Wire = InWire Int | OutWire Int | EdgeWire Int
  deriving (Eq, Ord, Show)

-- | Which way a wire runs at the node it is attached to.
data End = Into | OutOf
  deriving (Eq, Ord, Show)

-- | The relation a diagram means, every wire carrying one bit: the set of pairs of input and
-- output bits it relates. A 'Spider' means that all its wires agree, a 'Crossing' swaps its two
-- wires, and a 'Box' is a free generator, which the seed gives a pseudo-random meaning that
-- depends on its label and on the wires it has, but not on their order.
meaning
  :: forall (as :: [Symbol]) (bs :: [Symbol]). (IsList as, IsList bs) => Int -> DotData as bs -> Set ([Bool], [Bool])
meaning seed (DotData is os es ns) =
  Set.fromList [(map (bitOf . InWire) inIxs, map (bitOf . OutWire) outIxs) | bitOf <- joined]
  where
    inIxs = [0 .. len @as - 1]
    outIxs = [0 .. len @bs - 1]
    inNames = unVec (names @as)
    outNames = unVec (names @bs)
    -- every attachment of a wire to a node, and every wire going straight through
    attached =
      [(nodeOf p, (Into, portOf p, inNames !! i, InWire i)) | (i, Right p) <- zip [0 ..] (unVec is)]
        ++ [(nodeOf p, (OutOf, portOf p, outNames !! j, OutWire j)) | (j, Right p) <- zip [0 ..] (unVec os)]
        ++ concat
          [ [(nodeOf p1, (OutOf, portOf p1, l, EdgeWire k)), (nodeOf p2, (Into, portOf p2, l, EdgeWire k))]
          | (k, (p1, l, p2)) <- zip [0 ..] es
          ]
    through = [(InWire i, OutWire (unFin j)) | (i, Left j) <- zip [0 ..] (unVec is)]
    factors =
      [nodeFactor seed nd [w | (m, w) <- attached, m == n] | (n, nd) <- zip [0 ..] ns]
        ++ [([x, y], [Map.fromList [(x, v), (y, v)] | v <- [False, True]]) | (x, y) <- through]
    boundary = Set.fromList (map InWire inIxs ++ map OutWire outIxs)
    -- every boundary wire is on a node or goes straight through, so each row has them all
    joined = [(t Map.!) | t <- joinAll boundary factors]

-- | A node as a table over its wires: every assignment of bits to them that it allows.
nodeFactor :: Int -> (NodeKind, String) -> [(End, String, String, Wire)] -> ([Wire], [Map Wire Bool])
nodeFactor seed (kind, opts) ws = (wires, filter holds (assignments wires))
  where
    wires = nubOrd [w | (_, _, _, w) <- ws]
    bits t = [(e, port, l, t Map.! w) | (e, port, l, w) <- ws]
    holds t = case kind of
      Crossing -> crossing (bits t)
      Spider -> allSame [b | (_, _, _, b) <- bits t]
      Box -> even (hashFrom nodeHash (show (List.sort (bits t))))
    crossing bs = lookupBit Into ":nw" bs == lookupBit OutOf ":se" bs && lookupBit Into ":ne" bs == lookupBit OutOf ":sw" bs
    lookupBit e port bs = [b | (e', port', _, b) <- bs, e' == e, port' == port]
    allSame bs = and (zipWith (==) bs (drop 1 bs))
    -- the node's own part of the hash, once rather than for every assignment
    nodeHash = hashFrom 7 (show (seed, opts))
    hashFrom = foldl (\h c -> (h * 31 + fromEnum c) `mod` 1000003)

assignments :: [Wire] -> [Map Wire Bool]
assignments = foldr (\w ts -> [Map.insert w v t | t <- ts, v <- [False, True]]) [Map.empty]

-- | The natural join of the tables, forgetting a wire once no table left mentions it and it is not
-- on the boundary. Tables are taken in order of how many wires they share with the join so far.
joinAll :: Set Wire -> [([Wire], [Map Wire Bool])] -> [Map Wire Bool]
joinAll boundary = go Set.empty [Map.empty]
  where
    go _ ts [] = ts
    go seen ts fs =
      let ((ws, rows), rest) = List.maximumBy (comparing (\(f, _) -> score f)) (picks fs)
          ts' = [Map.union t r | t <- ts, r <- rows, and (Map.intersectionWith (==) t r)]
          live = boundary `Set.union` Set.fromList (concat [ws' | (ws', _) <- rest])
      in go (seen `Set.union` Set.fromList ws) (Set.toList (Set.fromList [Map.restrictKeys t live | t <- ts'])) rest
      where
        score (ws, _) = (length (filter (`Set.member` seen) ws), negate (length ws))
    picks xs = [(x, before ++ after) | (before, x : after) <- zip (List.inits xs) (List.tails xs)]

instance TestableProfunctor Dot