bluefin 0.0.3.0 → 0.0.4.0
raw patch · 4 files changed
+505/−21 lines, 4 filesdep ~bluefin-internalPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: bluefin-internal
API changes (from Hackage documentation)
+ Bluefin.Compound: class () => Handle (h :: Effects -> Type)
+ Bluefin.Compound: mapHandle :: forall (e :: Effects) (es :: Effects). (Handle h, e :> es) => h e -> h es
+ Bluefin.Compound: useImpl :: forall (e :: Effects) (es :: Effects) r. e :> es => Eff e r -> Eff es r
+ Bluefin.Compound: useImplIn :: forall (e :: Effects) (es :: Effects) t r. e :> es => (t -> Eff (es :& e) r) -> t -> Eff es r
Files
- CHANGELOG.md +12/−0
- bluefin.cabal +2/−2
- src/Bluefin.hs +28/−12
- src/Bluefin/Compound.hs +463/−7
CHANGELOG.md view
@@ -1,3 +1,15 @@+## 0.0.4.0++* Add documentation and functions to `Bluefin.Compound`++## 0.0.3.0++* Add `Bluefin.Reader`++## 0.0.2.0++* Add `Bluefin.StateSource`+ ## 0.0.1.0 * Add `Bluefin.Writer`
bluefin.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: bluefin-version: 0.0.3.0+version: 0.0.4.0 license: MIT license-file: LICENSE author: Tom Ellis@@ -34,6 +34,6 @@ Bluefin.Stream, Bluefin.Writer, build-depends:- bluefin-internal >= 0.0.3 && < 0.1+ bluefin-internal >= 0.0.4 && < 0.1 hs-source-dirs: src default-language: Haskell2010
src/Bluefin.hs view
@@ -1,8 +1,8 @@ module Bluefin ( -- * In brief - -- | Bluefin is an effect system which allows you, though- -- value-level handles, to freely mix a variety of effects+ -- | Bluefin is an effect system which allows you to freely mix a+ -- variety of effects, accessed though value-level handles, -- including -- -- * "Bluefin.EarlyReturn", for early return@@ -88,25 +88,26 @@ -- ** Effect scoping -- | Bluefin's use of the type system is very similar to- -- 'Control.Monad.ST': it ensures that a handle can never escape+ -- "Control.Monad.ST": it ensures that a handle can never escape -- the scope of its handler. That is, once the handler has -- finished running there is no way you can use the handle -- anymore. -- ** Type signatures - -- | Bluefin type signatures follow a common pattern which looks- -- like+ -- | The type signatures of Bluefin functions follow a common+ -- pattern which looks like -- -- @ -- (e1 :> es, ...) -> \<Handle\> e1 -> ... -> Eff es r -- @ --- --- -- Consider the example below, @incrementReadLine@, which reads- -- integers from standard input and accumulates them into a state.- -- It returns when it reads the input integer @0@ and it throws an- -- exception if it encounters an input line it cannot parse.+ -- Here @\<Handle\>@ could be, for example, @State Int@,+ -- @Exception String@ or @IOE@. Consider the example below,+ -- @incrementReadLine@, which reads integers from standard input+ -- and accumulates them into a state. It returns when it reads+ -- the input integer @0@ and it throws an exception if it+ -- encounters an input line it cannot parse. -- -- Firstly, let's look at the arguments, which are all handles to -- Bluefin effects. There is a state handle, an exception handle,@@ -144,7 +145,7 @@ -- incrementReadLine state exception io = do -- 'Bluefin.Jump.withJump' $ \\break -> 'Control.Monad.forever' $ do -- line <- 'Bluefin.IO.effIO' io getLine- -- i <- case 'Text.Maybe.readMaybe' line of+ -- i <- case 'Text.Read.readMaybe' line of -- Nothing -> -- 'Bluefin.Exception.throw' exception ("Couldn't read: " ++ line) -- Just i ->@@ -160,9 +161,20 @@ -- must be handled by a corresponding handler, for example -- 'Bluefin.State.runState' for the state effect, -- 'Bluefin.Exception.try' for the exception effect and- -- 'Bluefin.Eff.runEff' for the @IO@ effect.+ -- 'Bluefin.Eff.runEff' for the @IO@ effect. The type signatures+ -- of handlers also follow a common pattern, which looks like -- -- @+ -- (forall e. \<Handle\> e -> Eff (e :& es) a) -> Eff es r+ -- @+ --+ -- This means that the effect @e@, corresponding to the handle+ -- @\<Handle\> e@, has been handled and removed from the set of+ -- remaining effects, @es@. (The signatures for @runEff@ and+ -- @runPureEff@ are slightly different because they remove all+ -- effects.)+ --+ -- @ -- runIncrementReadLine :: IO (Either String Int) -- runIncrementReadLine = 'Bluefin.Eff.runEff' $ \\io -> do -- 'Bluefin.Exception.try' $ \\exception -> do@@ -255,6 +267,10 @@ -- default after adding inferred type signatures.) -- -- * Writing a handler often requires an explicit type signature.++ -- * Creating your own effects++ -- | See "Bluefin.Compound". -- * Example
src/Bluefin/Compound.hs view
@@ -1,14 +1,470 @@ module Bluefin.Compound- ( -- | @Compound@ allows combining two effects into one, for- -- encapsulation. It is not documented yet.+ ( -- * Creating your own effects - -- * Handle- Compound,+ -- ** Wrap a single effect - -- * Handler- runCompound,+ -- | 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+ -- @+ --+ -- @+ -- >>> exampeleCounter1+ -- 3+ -- @ - -- * Effectful operations+ -- ** 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