packages feed

proarrow-0.4.0.0: test/Examples/Squares.hs

-- | Update the value field of every named entry, using square optics. 'field' is a lens,
-- 'each' is a list traversal, and 'fused' combines their witnesses into one absorption.
module Examples.Squares where

import Data.Kind (Type)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Falsify (testProperty)
import Prelude

import Proarrow.Category.Instance.Unit (Unit (..))
import Proarrow.Core ((:~>), type (+->))
import Proarrow.Functor qualified as F
import Proarrow.Limit.BinaryProduct (Product)
import Proarrow.Object (pattern Objs)
import Proarrow.Optic (Prostrong)
import Proarrow.Optic.Traversal (TravFl)
import Proarrow.Path (Path (..))
import Proarrow.Profunctor.Corepresentable (Corep (..))
import Proarrow.Profunctor.Instance.Composition ((:.:) (..))
import Proarrow.Profunctor.Instance.Constant (Constant)
import Proarrow.Profunctor.Instance.Identity (Id (..))
import Proarrow.Profunctor.Instance.Star (Star, unStar, pattern Star)
import Proarrow.Profunctor.Representable (Rep (..), RepCostar (..), Representable (..))
import Proarrow.Squares
import Proarrow.Testing (expect, genNamed)
import Props.Hask ()

-- * Data and effects

data Entry a = Entry {entryName :: String, entryValue :: a}
  deriving (Eq, Show)

-- | The list records the old ages, in the order their effects execute.
data Logged a = Logged {visits :: [Int], result :: a}
  deriving (Eq, Show, Functor)

instance Applicative Logged where
  pure = Logged []
  Logged xs f <*> Logged ys x = Logged (xs ++ ys) (f x)

type Effect :: Type +-> Type
type Effect = Star (F.Prelude Logged)

type Point :: Type -> () +-> Type
type Point a = Rep (Constant a)

type Cell :: (Type +-> Type) -> Type -> Type -> Type
type Cell r a b = InputSquare r (Point a) (Point b)

-- | Introduce a primitive effect as a square; 'runCell' is the execution boundary.
primitive :: (a -> Logged b) -> Cell Effect a b
primitive f = mkSquare (\(Id Unit) -> Star (F.Prelude . f))

runCell :: Cell Effect a b -> a -> Logged b
runCell sq = F.unPrelude . unStar (runSquare sq (Id Unit))

-- * The value-field lens

type FieldP :: Type +-> Type
type FieldP = Rep (Product String)
type FieldQ :: Type +-> Type
type FieldQ = Corep (Product String)

fieldUnpackNat :: Point (Entry a) :~> FieldP :.: Point a
fieldUnpackNat (Rep f) = Rep (\x -> let Entry name a = f x in (name, a)) :.: Rep id

fieldRebuildNat :: Point b :~> FieldQ :.: Point (Entry b)
fieldRebuildNat (Rep g) = Corep id :.: Rep (\(name, b) -> Entry name (g b))

fieldUnpack :: Cell FieldP (Entry a) a
fieldUnpack = Sq fieldUnpackNat

fieldRebuild :: Cell FieldQ b (Entry b)
fieldRebuild = Sq fieldRebuildNat

-- | Focus an entry's value while carrying its name as the residual. In the diagram,
-- @s@/@t@ are the entry shapes, @a@/@b@ the value shapes, @p = FieldP@, and
-- @q = FieldQ@. The legs introduce residual effects, which are absorbed below the input.
-- Wire names stay on borders; each contiguous run of @\@@ is one transformation.
--
-- > +-----+-----+-----+
-- > |     |     |     |
-- > s>-@->a>-@->b>-@->t
-- > |  |  |  |  |  |  |
-- > +--p--+--r--+--q--+
-- > |   \    |    /   |
-- > |    @@@@@@@@@    |
-- > |        |        |
-- > +--------r--------+
field :: forall r a b. (Prostrong TravFl r) => Cell r a b -> Cell r (Entry a) (Entry b)
field input =
  fieldUnpack ||| input ||| fieldRebuild
    === proSquare @TravFl @r @FieldP @FieldQ

-- * Traverse a list of entries

type ListP :: Type +-> Type
type ListP = Star []
type ListQ :: Type +-> Type
type ListQ = RepCostar ListP

listUnpackNat :: Point [a] :~> ListP :.: Point a
listUnpackNat f@Objs = tabulate (index f)

listRebuildNat :: Point b :~> ListQ :.: Point [b]
listRebuildNat (Rep g) = RepCostar id :.: Rep (map g)

listUnpack :: Cell ListP [a] a
listUnpack = Sq listUnpackNat

listRebuild :: Cell ListQ b [b]
listRebuild = Sq listRebuildNat

-- | Visit every element, in list order. Its diagram is the same as 'field', with
-- @p = ListP@ and @q = ListQ@. Feeding the output of 'field' into 'each'
-- makes the complete entry update the input square of the list traversal.
each :: forall r a b. (Prostrong TravFl r) => Cell r a b -> Cell r [a] [b]
each input =
  listUnpack ||| input ||| listRebuild
    === proSquare @TravFl @r @ListP @ListQ

-- | A lens inside a traversal, composed by feeding one square program into the other.
entries :: (Prostrong TravFl r) => Cell r a b -> Cell r [Entry a] [Entry b]
entries input = each (field input)

-- * Fuse the two absorptions

type BothP :: Type +-> Type
type BothP = ListP :.: FieldP
type BothQ :: Type +-> Type
type BothQ = FieldQ :.: ListQ

-- | Compose the unpacking squares horizontally, then combine their residual effects.
unpackBoth :: Cell BothP [Entry a] a
unpackBoth =
  listUnpack ||| fieldUnpack
    === vCombineAll @(FieldP ::: ListP ::: Nil)

-- | Rebuilding composes in the other order: the field square precedes the list square.
rebuildBoth :: Cell BothQ b [Entry b]
rebuildBoth =
  fieldRebuild ||| listRebuild
    === vCombineAll @(ListQ ::: FieldQ ::: Nil)

-- | The same update with one absorption for the composed witness pair:
-- @p = ListP :.: FieldP@ and @q = FieldQ :.: ListQ@. Their effects remain vertical
-- or diagonal, including when the witnesses happen to be representable/corepresentable.
-- 'unpackBoth' and 'rebuildBoth' compose the surrounding legs with squares as well.
-- Agreement with 'entries' is the composition law for 'Prostrong'.
--
-- > +--p--r--q--+
-- > |   \ | /   |
-- > |    @@@    |
-- > |     |     |
-- > +-----r-----+
fused :: forall r a b. (Prostrong TravFl r) => Cell r a b -> Cell r [Entry a] [Entry b]
fused input =
  unpackBoth ||| input ||| rebuildBoth
    === proSquare @TravFl @r @BothP @BothQ

-- * Execute the square program

increment :: Int -> Logged Int
increment age = Logged [age] (age + 1)

-- | Increment both ages, preserving the names and recording the visits as @[36,85]@.
demo :: Logged [Entry Int]
demo = runCell (entries (primitive increment)) [Entry "Ada" 36, Entry "Grace" 85]

test :: TestTree
test =
  testGroup
    "Square programs"
    [ testProperty "field lens preserves the name and visits its value once" $
        expect "field" (Logged [36] (Entry "Ada" 37)) (runCell (field (primitive increment)) (Entry "Ada" 36))
    , testProperty "lens and traversal update each field in list order" $ do
        flags <- genNamed @[Bool] "ages"
        let rows = sampleRows flags
            Logged seen updated = runCell (entries (primitive increment)) rows
        expect "effect order" (map entryValue rows) seen
        expect "names and values" (map (\(Entry name age) -> Entry name (age + 1)) rows) updated
    , testProperty "composed witnesses agree with sequential square optics" $ do
        flags <- genNamed @[Bool] "ages"
        let rows = sampleRows flags
            input = primitive increment
        expect "composition" (runCell (entries input) rows) (runCell (fused input) rows)
    , testProperty "composition also preserves type-changing updates" $ do
        flags <- genNamed @[Bool] "ages"
        let rows = sampleRows flags
            input = primitive (\age -> Logged [age] (show age))
            expected = Logged (map entryValue rows) (map (\(Entry name age) -> Entry name (show age)) rows)
        expect "sequential" expected (runCell (entries input) rows)
        expect "composed" expected (runCell (fused input) rows)
    , testProperty "empty traversal runs no effects" $
        expect "empty" (Logged [] []) (runCell (fused (primitive increment)) [])
    ]

sampleRows :: [Bool] -> [Entry Int]
sampleRows = zipWith (\n b -> Entry (show n) (if b then 7 else 0)) [0 :: Int ..]