bluefin-0.0.7.0: src/Bluefin/Compound.hs
module Bluefin.Compound
( -- * Creating your own effects
-- ** Wrap a single effect
-- | Because in Bluefin everything happens at the value level,
-- creating your own effects is equivalent to creating your own
-- data types. We just use the techniques we know and love from
-- Haskell! For example, if I want to make a "counter" effect
-- that allows me to increment a counter then I can wrap a @State@
-- handle in a newtype:
--
-- @
-- newtype Counter1 e = MkCounter1 (State Int e)
--
-- incCounter1 :: (e :> es) => Counter1 e -> Eff es ()
-- incCounter1 (MkCounter1 st) = modify st (+ 1)
--
-- runCounter1 ::
-- (forall e. Counter1 e -> Eff (e :& es) r) ->
-- Eff es Int
-- runCounter1 k =
-- evalState 0 $ \\st -> do
-- _ <- k (MkCounter1 st)
-- get st
-- @
--
-- Running the handler tells me the number of times I incremented
-- the counter.
--
-- @
-- exampleCounter1 :: Int
-- exampleCounter1 = runPureEff $ runCounter1 $ \\c ->
-- incCounter1 c
-- incCounter1 c
-- incCounter1 c
-- @
--
-- @
-- >>> exampleCounter1
-- 3
-- @
-- ** Wrap multiple effects, first attempt
-- | If we want to wrap multiple effects then we can use the
-- normal approach we use to wrap multiple values into a single
-- value: define a new data type with multiple fields. There's a
-- caveat to this approach, but before we address the caveat let's
-- see the approach in action. Here we define a new handle,
-- @Counter2@, that contains a @State@ and @Exception@ handle
-- within it. That allows us to increment the counter and throw
-- an exception when we hit a limit.
--
-- @
-- data Counter2 e1 e2 = MkCounter2 (State Int e1) (Exception () e2)
--
-- incCounter2 :: (e1 :> es, e2 :> es) => Counter2 e1 e2 -> Eff es ()
-- incCounter2 (MkCounter2 st ex) = do
-- count <- get st
-- when (count >= 10) $
-- throw ex ()
-- put st (count + 1)
--
-- runCounter2 ::
-- (forall e1 e2. Counter2 e1 e2 -> Eff (e2 :& e1 :& es) r) ->
-- Eff es Int
-- runCounter2 k =
-- evalState 0 $ \\st -> do
-- _ \<- try $ \\ex -> do
-- k (MkCounter2 st ex)
-- get st
-- @
--
-- We can see that attempting to increment the counter fovever
-- bails out when we reach the limit.
--
-- @
-- exampleCounter2 :: Int
-- exampleCounter2 = runPureEff $ runCounter2 $ \\c ->
-- forever $
-- incCounter2 c
-- @
--
-- @
-- >>> exampleCounter2
-- 10
-- @
--
-- The flaw of this approach is that you expose one effect
-- parameter for each handle in the data type. That's rather
-- cumbersome! We can do better.
-- ** Wrap multiple effects, a better approach
-- | We can avoid exposing multiple effect parameters and just
-- expose a single one. To make this work we have to define our
-- handler in a slightly different way. Firstly we apply
-- @useImplIn@ to the effectful operation @k@ and secondly we
-- apply @mapHandle@ to each of the handles out of which we create
-- our compound handle. Everything else remains the same.
--
-- @
-- data Counter3 e = MkCounter3 (State Int e) (Exception () e)
--
-- incCounter3 :: (e :> es) => Counter3 e -> Eff es ()
-- incCounter3 (MkCounter3 st ex) = do
-- count <- get st
-- when (count >= 10) $
-- throw ex ()
-- put st (count + 1)
--
-- runCounter3 ::
-- (forall e. Counter3 e -> Eff (e :& es) r) ->
-- Eff es Int
-- runCounter3 k =
-- evalState 0 $ \\st -> do
-- _ \<- try $ \\ex -> do
-- useImplIn k (MkCounter3 (mapHandle st) (mapHandle ex))
-- get st
-- @
--
-- The example works as before:
--
-- @
-- exampleCounter3 :: Int
-- exampleCounter3 = runPureEff $ runCounter3 $ \\c ->
-- forever $
-- incCounter3 c
-- @
--
-- @
-- >>> exampleCounter3
-- 10
-- @
-- ** Wrap multiple effects, don't handle them all
-- | So far our handlers have handled all the effects that are
-- found within our compound effect. We don't have to do that
-- though: we can leave some of the effects unhandled to be
-- handled by a different handler at a higher level. Let's extend
-- our example with a @Stream@ effect. Whenever we ask to
-- increment the counter, and it is currently an even number, then
-- we yield a message about that. Additionally, there's a new
-- operation @getCounter4@ which allows us to yield a message
-- whilst returning the value of the counter.
--
-- @
-- data Counter4 e
-- = MkCounter4 (State Int e) (Exception () e) (Stream String e)
--
-- incCounter4 :: (e :> es) => Counter4 e -> Eff es ()
-- incCounter4 (MkCounter4 st ex y) = do
-- count <- get st
--
-- when (even count) $
-- yield y "Count was even"
--
-- when (count >= 10) $
-- throw ex ()
--
-- put st (count + 1)
--
-- getCounter4 :: (e :> es) => Counter4 e -> String -> Eff es Int
-- getCounter4 (MkCounter4 st _ y) msg = do
-- yield y msg
-- get st
--
-- runCounter4 ::
-- (e1 :> es) =>
-- Stream String e1 ->
-- (forall e. Counter4 e -> Eff (e :& es) r) ->
-- Eff es Int
-- runCounter4 y k =
-- evalState 0 $ \\st -> do
-- _ \<- try $ \\ex -> do
-- useImplIn k (MkCounter4 (mapHandle st) (mapHandle ex) (mapHandle y))
-- get st
-- @
--
-- @
-- exampleCounter4 :: ([String], Int)
-- exampleCounter4 = runPureEff $ yieldToList $ \\y -> do
-- runCounter4 y $ \\c -> do
-- incCounter4 c
-- incCounter4 c
-- n <- getCounter4 c "I'm getting the counter"
-- when (n == 2) $
-- yield y "n was 2, as expected"
-- @
--
-- @
-- >>> exampleCounter4
-- (["Count was even","I'm getting the counter","n was 2, as expected"],2)
-- @
-- ** Dynamic effects
-- | So far we've looked at "concrete" compound effects, that is,
-- new effects implemented in terms of specific other effects. We
-- can also define dynamic effects, whose implementation is left
-- abstract, to be defined in the handler. To do that we create a
-- handle that is a record of functions. To run an effectful
-- operation we call one of the functions from the record. We
-- define the record in the handler. Here @incCounter5Impl@ and
-- @getCounter5Impl@ are exactly the same as @incCounter4@ and
-- @getCounter4@ were, they're just defined in the handler. In
-- order to be used polymorphically, the actually effectful
-- functions we call, @incCounter5@ and @getCounter5@ are derived
-- from the record fields by applying @useImpl@.
--
-- @
-- data Counter5 e = MkCounter5
-- { incCounter5Impl :: Eff e (),
-- getCounter5Impl :: String -> Eff e Int
-- }
--
-- incCounter5 :: (e :> es) => Counter5 e -> Eff es ()
-- incCounter5 e = useImpl (incCounter5Impl e)
--
-- getCounter5 :: (e :> es) => Counter5 e -> String -> Eff es Int
-- getCounter5 e msg = useImpl (getCounter5Impl e msg)
--
-- runCounter5 ::
-- (e1 :> es) =>
-- Stream String e1 ->
-- (forall e. Counter5 e -> Eff (e :& es) r) ->
-- Eff es Int
-- runCounter5 y k =
-- evalState 0 $ \\st -> do
-- _ \<- try $ \\ex -> do
-- useImplIn
-- k
-- ( MkCounter5
-- { incCounter5Impl = do
-- count <- get st
--
-- when (even count) $
-- yield y "Count was even"
--
-- when (count >= 10) $
-- throw ex ()
--
-- put st (count + 1),
-- getCounter5Impl = \\msg -> do
-- yield y msg
-- get st
-- }
-- )
-- get st
-- @
--
-- The result is exactly the same as before
--
-- @
-- exampleCounter5 :: ([String], Int)
-- exampleCounter5 = runPureEff $ yieldToList $ \\y -> do
-- runCounter5 y $ \\c -> do
-- incCounter5 c
-- incCounter5 c
-- n <- getCounter5 c "I'm getting the counter"
-- when (n == 2) $
-- yield y "n was 2, as expected"
-- @
--
-- @
-- >>> exampleCounter5
-- (["Count was even","I'm getting the counter","n was 2, as expected"],2)
-- @
-- ** Combining concrete and dynamic effects
-- | We can also freely combine concrete and dynamic effects. In
-- the following example, the @incCounter6@ effect is left
-- dynamic, and defined in the handler, whilst @getCounter6@ is
-- implemented in terms of concrete @State@ and @Stream@ effects.
--
-- @
-- data Counter6 e = MkCounter6
-- { incCounter6Impl :: Eff e (),
-- counter6State :: State Int e,
-- counter6Stream :: Stream String e
-- }
--
-- incCounter6 :: (e :> es) => Counter6 e -> Eff es ()
-- incCounter6 e = useImpl (incCounter6Impl e)
--
-- getCounter6 :: (e :> es) => Counter6 e -> String -> Eff es Int
-- getCounter6 (MkCounter6 _ st y) msg = do
-- yield y msg
-- get st
--
-- runCounter6 ::
-- (e1 :> es) =>
-- Stream String e1 ->
-- (forall e. Counter6 e -> Eff (e :& es) r) ->
-- Eff es Int
-- runCounter6 y k =
-- evalState 0 $ \\st -> do
-- _ \<- try $ \\ex -> do
-- useImplIn
-- k
-- ( MkCounter6
-- { incCounter6Impl = do
-- count <- get st
--
-- when (even count) $
-- yield y "Count was even"
--
-- when (count >= 10) $
-- throw ex ()
--
-- put st (count + 1),
-- counter6State = mapHandle st,
-- counter6Stream = mapHandle y
-- }
-- )
-- get st
-- @
--
-- Naturally, the result is the same.
--
-- @
-- exampleCounter6 :: ([String], Int)
-- exampleCounter6 = runPureEff $ yieldToList $ \\y -> do
-- runCounter6 y $ \\c -> do
-- incCounter6 c
-- incCounter6 c
-- n <- getCounter6 c "I'm getting the counter"
-- when (n == 2) $
-- yield y "n was 2, as expected"
-- @
--
-- @
-- >>> exampleCounter6
-- (["Count was even","I'm getting the counter","n was 2, as expected"],2)
-- @
-- ** A dynamic file system effect
-- | The @effectful@ library has [an example of a dynamic effect
-- for basic file system
-- access](https://hackage.haskell.org/package/effectful-core-2.2.1.0/docs/Effectful-Dispatch-Dynamic.html#g:2).
-- This is what it looks like in Bluefin. We start by defining a
-- record of effectful operations.
--
-- @
-- data FileSystem es = MkFileSystem
-- { readFileImpl :: FilePath -> Eff es String,
-- writeFileImpl :: FilePath -> String -> Eff es ()
-- }
--
-- readFile :: (e :> es) => FileSystem e -> FilePath -> Eff es String
-- readFile fs filepath = useImpl (readFileImpl fs filepath)
--
-- writeFile :: (e :> es) => FileSystem e -> FilePath -> String -> Eff es ()
-- writeFile fs filepath contents = useImpl (writeFileImpl fs filepath contents)
-- @
--
-- We can make a pure handler that simulates reading and writing
-- to a file system by storing file contents in an association
-- list.
--
-- @
-- runFileSystemPure ::
-- (e1 :> es) =>
-- Exception String e1 ->
-- [(FilePath, String)] ->
-- (forall e2. FileSystem e2 -> Eff (e2 :& es) r) ->
-- Eff es r
-- runFileSystemPure ex fs0 k =
-- evalState fs0 $ \\fs ->
-- useImplIn
-- k
-- MkFileSystem
-- { readFileImpl = \\filepath -> do
-- fs' <- get fs
-- case lookup filepath fs' of
-- Nothing ->
-- throw ex ("File not found: " <> filepath)
-- Just s -> pure s,
-- writeFileImpl = \\filepath contents ->
-- modify fs ((filepath, contents) :)
-- }
-- @
--
-- Or we can make a handler that actually performs IO operations
-- against a real file system.
--
-- @
-- runFileSystemIO ::
-- forall e1 e2 es r.
-- (e1 :> es, e2 :> es) =>
-- Exception String e1 ->
-- IOE e2 ->
-- (forall e. FileSystem e -> Eff (e :& es) r) ->
-- Eff es r
-- runFileSystemIO ex io k =
-- useImplIn
-- k
-- MkFileSystem
-- { readFileImpl =
-- adapt . Prelude.readFile,
-- writeFileImpl =
-- \\filepath -> adapt . Prelude.writeFile filepath
-- }
-- where
-- adapt :: (e1 :> ess, e2 :> ess) => IO a -> Eff ess a
-- adapt m =
-- effIO io (Control.Exception.try @IOException m) >>= \\case
-- Left e -> throw ex (show e)
-- Right r -> pure r
-- @
--
-- We can use the @FileSystem@ effect to define an action which
-- does some file system operations.
--
-- @
-- action :: (e :> es) => FileSystem e -> Eff es String
-- action fs = do
-- file <- readFile fs "\/dev\/null"
-- when (length file == 0) $ do
-- writeFile fs "\/tmp\/bluefin" "Hello!"
-- readFile fs "\/tmp\/doesn't exist"
-- @
--
-- and we can run it purely, against a simulated file system
--
-- @
-- exampleRunFileSystemPure :: Either String String
-- exampleRunFileSystemPure = runPureEff $ try $ \\ex ->
-- runFileSystemPure ex [("\/dev\/null", "")] action
-- @
--
-- @
-- >>> exampleRunFileSystemPure
-- Left "File not found: \/tmp\/doesn't exist"
-- @
--
-- or against the real file system.
--
-- @
-- exampleRunFileSystemIO :: IO (Either String String)
-- exampleRunFileSystemIO = runEff $ \\io -> try $ \\ex ->
-- runFileSystemIO ex io action
-- @
--
-- @
-- >>> exampleRunFileSystemIO
-- Left "\/tmp\/doesn't exist: openFile: does not exist (No such file or directory)"
-- \$ cat \/tmp\/bluefin
-- Hello!
-- @
-- * Functions for making compound effects
Handle (mapHandle),
useImpl,
useImplIn,
useImplWithin,
-- * Deprecated
-- | Do not use. Will be removed in a future version.
Compound,
runCompound,
withCompound,
)
where
import Bluefin.Internal