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, -- * Deprecated -- | Do not use. Will be removed in a future version. Compound, runCompound, withCompound, ) where import Bluefin.Internal