capability-0.4.0.0: examples/Reflection.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Reflection where
import Capability.Error
import Capability.Reader
import Capability.Reflection
import Capability.Sink
import Capability.Source
import Capability.State
import Capability.Writer
import Control.Monad (forM_, replicateM)
import qualified Control.Monad.Except as MTL
import qualified Control.Monad.State as MTL
import Data.DList (DList)
import qualified Data.DList as DList
import Data.Monoid (Product (..))
import Test.Hspec
----------------------------------------------------------------------
-- Instances
newtype Logger s a = Logger (MTL.State (DList String, s) a)
deriving (Functor, Applicative, Monad)
deriving
(HasSink "log" String)
via SinkDList (SinkLog (Rename 1 (Pos 1 () (MonadState (MTL.State (DList String, s))))))
deriving
(HasState "test" s, HasSource "test" s, HasSink "test" s)
via Rename 2 (Pos 2 () (MonadState (MTL.State (DList String, s))))
runLogger :: s -> Logger s a -> (a, s, [String])
runLogger s0 (Logger m) =
case MTL.runState m (mempty, s0) of
(a, (log', s)) -> (a, s, DList.toList log')
----------------------------------------------------------------------
-- Test Cases
spec :: Spec
spec = do
describe "interpret_" $ do
it "evaluates HasSource" $ do
runLogger
[1, 2, 3 :: Int]
( interpret_ @"source"
ReifiedSource
{ _await = do
yield @"log" "await"
state @"test" $ \case
(x : xs) -> (x, xs)
[] -> error "empty stream"
}
(replicateM 3 $ await @"source")
)
`shouldBe` ([1, 2, 3], [], ["await", "await", "await"])
it "evaluates HasSink" $ do
runLogger
[]
( interpret_ @"sink"
ReifiedSink
{ _yield = \x -> do
yield @"log" "yield"
modify @"test" (x :)
}
(forM_ [1, 2, 3 :: Int] $ yield @"sink")
)
`shouldBe` ((), [3, 2, 1], ["yield", "yield", "yield"])
it "evaluates HasReader" $ do
runLogger
(42 :: Int)
( interpret_ @"reader"
ReifiedReader
{ _readerSource =
ReifiedSource
{ _await = do
yield @"log" "ask"
get @"test"
},
_local = \f m -> do
yield @"log" "local"
r <- state @"test" $ \r -> (r, f r)
a <- m
put @"test" r
pure a,
_reader = \f -> do
yield @"log" "reader"
f <$> get @"test"
}
( sequence
[ ask @"reader",
local @"reader" succ $ do
asks @"reader" succ,
reader @"reader" pred
]
)
)
`shouldBe` ([42, 44, 41], 42, ["ask", "local", "ask", "reader"])
it "evaluates HasState" $ do
runLogger
(42 :: Int)
( interpret_ @"state"
ReifiedState
{ _stateSource =
ReifiedSource
{ _await = do
yield @"log" "get"
get @"test"
},
_stateSink =
ReifiedSink
{ _yield = \x -> do
yield @"log" "put"
put @"test" x
},
_state = \f -> do
yield @"log" "state"
state @"test" f
}
( do
old <- get @"state"
put @"state" 0
state @"state" (\new -> (old, succ new))
)
)
`shouldBe` (42, 1, ["get", "put", "state"])
it "evaluates HasWriter" $ do
runLogger
(mempty :: Product Int)
( interpret_ @"writer"
ReifiedWriter
{ _writerSink =
ReifiedSink
{ _yield = \w -> do
yield @"log" "tell"
modify @"test" (<> w)
},
_writer = \(a, w) -> do
yield @"log" "writer"
modify @"test" (<> w)
pure a,
_listen = \m -> do
yield @"log" "listen"
w0 <- state @"test" $ \s -> (s, mempty)
a <- m
w <- state @"test" $ \s -> (s, w0 <> s)
pure (a, w),
_pass = \m -> do
yield @"log" "pass"
w0 <- state @"test" $ \s -> (s, mempty)
(a, f) <- m
modify' @"test" $ \s -> w0 <> f s
pure a
}
( do
tell @"writer" 2
writer @"writer" ((), 3)
((), w) <- listen @"writer" $ do
tell @"writer" 5
pass @"writer" $ do
tell @"writer" 6
pure ((), (+ 1))
pure w
)
)
`shouldBe` (5, 2 * 3 * 5 * 7, ["tell", "writer", "listen", "tell", "pass", "tell"])
it "evaluates HasThrow" $ do
runLogger
()
( MTL.runExceptT $
interpret_ @"throw"
ReifiedThrow
{ _throw = \e -> do
MTL.lift $ yield @"log" "throw"
MTL.throwError e
}
( do
_ <- throw @"throw" "some error"
pure $! tail ""
)
)
`shouldBe` (Left "some error", (), ["throw"])
it "evaluates HasCatch" $ do
runLogger
()
( MTL.runExceptT $
interpret_ @"catch"
ReifiedCatch
{ _catchThrow =
ReifiedThrow
{ _throw = \e -> do
MTL.lift $ yield @"log" "throw"
MTL.throwError e
},
_catch = \m h -> do
MTL.lift $ yield @"log" "catch"
MTL.catchError m h,
_catchJust = \f m h -> do
MTL.lift $ yield @"log" "catchJust"
MTL.catchError m $ \e ->
case f e of
Just b -> h b
Nothing -> MTL.throwError e
}
( do
e1 <-
catch @"catch"
( do
_ <- throw @"catch" "some error"
pure $! tail ""
)
pure
catchJust @"catch"
(const Nothing)
( do
_ <- throw @"catch" ("again " ++ e1)
pure $! tail ""
)
pure
)
)
`shouldBe` (Left "again some error", (), ["catch", "throw", "catchJust", "throw"])
describe "interpret" $ do
it "can be nested" $ do
runLogger
()
( interpret @"source" @'[HasSink "log" String]
ReifiedSource
{ _await = do
yield @"log" "await"
pure ()
}
( interpret @"sink" @'[HasSink "log" String, HasSource "source" ()]
ReifiedSink
{ _yield = \() -> do
yield @"log" "yield"
pure ()
}
( do
yield @"log" "begin"
await @"source"
yield @"sink" ()
yield @"log" "end"
)
)
)
`shouldBe` ((), (), ["begin", "await", "yield", "end"])