module Bluefin ( -- * In brief -- | Bluefin is an effect system which allows you to freely mix a -- variety of effects, including -- -- * "Bluefin.EarlyReturn", for early return -- * "Bluefin.Exception", for exceptions -- * "Bluefin.IO", for I/O -- * "Bluefin.State", for mutable state -- * "Bluefin.Stream", for streams -- -- Bluefin effects are accessed through explicitly though -- value-level handles. -- * Introduction -- | Bluefin is a Haskell effect system with a new style of API. -- It is distinct from prior effect systems because effects are -- accessed explicitly through value-level handles which occur as -- arguments to effectful operations. Handles (such as -- 'Bluefin.State.State' handles, which allow access to mutable -- state) are introduced by handlers (such as -- 'Bluefin.State.evalState', which sets the initial state). -- Here's an example where a mutable state effect handle, @sn@, is -- introduced by its handler, 'Bluefin.State.evalState'. -- -- @ -- -- If @n < 10@ then add 10 to it, otherwise -- -- return it unchanged -- example1 :: Int -> Int -- example1 n = 'Bluefin.Eff.runPureEff' $ -- -- Create a new state handle, sn, and -- -- initialize the value of the state to n -- 'Bluefin.State.evalState' n $ \\sn -> do -- n' <- 'Bluefin.State.get' sn -- when (n' < 10) $ -- 'Bluefin.State.modify' sn (+ 10) -- get sn -- @ -- -- @ -- >>> example1 5 -- 15 -- >>> example1 12 -- 12 -- @ -- -- The handle @sn@ is used in much the same way as an -- 'Data.STRef.STRef' or 'Data.IORef.IORef'. -- ** Multiple effects of the same type -- | A benefit of value-level effect handles is that it's simple -- to have multiple effects of the same type in scope at the same -- time. It is simple to disambiguate them, because they are -- distinct values! By contrast, existing effect systems require -- the disambiguation to occur at the type level, which imposes -- challenges. -- -- Here is a Bluefin example with two mutable @Int@ state effects -- in scope. -- -- @ -- -- Compare two values and add 10 -- -- to the smaller -- example2 :: (Int, Int) -> (Int, Int) -- example2 (m, n) = 'Bluefin.Eff.runPureEff' $ -- 'Bluefin.State.evalState' m $ \\sm -> do -- evalState n $ \\sn -> do -- do -- n' <- 'Bluefin.State.get' sn -- m' <- get sm -- -- if n' < m' -- then 'Bluefin.State.modify' sn (+ 10) -- else modify sm (+ 10) -- -- n' <- get sn -- m' <- get sm -- -- pure (n', m') -- @ -- -- @ -- >>> example2 (5, 10) -- (15, 10) -- >>> example2 (30, 3) -- (30, 13) -- @ -- ** Exception handles -- | Bluefin exceptions are accessed through -- 'Bluefin.Exception.Exception' handles. An @Exception@ handle -- is introduced by a handler, such as 'Bluefin.Exception.try', -- and that handler is where the exception, if thrown, will be -- handled. This arrangement differs from normal Haskell -- exceptions in two ways. Firstly, every Bluefin exception will -- be handled – it is not possible to have an unhandled Bluefin -- exception. Secondly, a Bluefin exception can be handled in -- only one place – normal Haskell exceptions can be handled in a -- variety of places, and the closest handler of matching type on -- the stack will be the one that will be chosen upon -- 'Control.Exception.throw'. -- -- @example3@ shows how to use Bluefin to calculate the sum of -- numbers from 1 to @n@, but stop if the sum becomes bigger than -- 20. The exception handle, @ex@, which has type @Exception -- String e@, cannot escape the scope of its handler, @try@. If -- thrown it will be handled at that @try@, and nowhere else. -- -- @ -- example3 :: Int -> Either String Int -- example3 n = 'Bluefin.Eff.runPureEff' $ -- 'Bluefin.Exception.try' $ \\ex -> do -- 'Bluefin.State.evalState' 0 $ \\total -> do -- for_ [1..n] $ \\i -> do -- soFar <- 'Bluefin.State.get' total -- when (soFar > 20) $ do -- 'Bluefin.Exception.throw' ex ("Became too big: " ++ show soFar) -- 'Bluefin.State.put' total (soFar + i) -- -- 'Bluefin.State.get' total -- @ -- -- @ -- >>> example3 4 -- Right 10 -- >>> example3 10 -- Left "Became too big: 21" -- @ -- ** Effect scoping -- | Bluefin's use of the type system is very similar to -- "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 -- | The type signatures of Bluefin functions follow a common -- pattern which looks like -- -- @ -- (e1 :> es, ...) -> \ e1 -> ... -> Eff es r -- @ -- -- Here @\@ could be, for example, @State Int@, -- @Exception String@ or @IOE@. Consider the function below, -- @incrementReadLine@. It reads integers from standard input, -- 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, -- and an IO handle, which allow modification of an @Int@ state, -- throwing a @String@ exception, and performing @IO@ operations -- respectively. They are each tagged with a different effect -- type, @e1@, @e2@ and @e3@ respectively, which are always kept -- polymorphic. -- -- Secondly, let's look at the return value, @Eff es ()@. This -- means the computation is performed in the t'Bluefin.Eff.Eff' -- monad and the resulting value produced is of type @()@. @Eff@ -- is tagged with the effect type @es@, which is also always kept -- polymorphic. -- -- Finally, let's look at the constraints. They are what tie -- together the effect tags of the arguments to the effect tag of -- the result. For every argument effect tag @en@ we have a -- constraint @en :> es@. That tells us the that effect handle -- with tag @en@ is allowed to be used within the effectful -- computation. If we didn't have the @e1 :> es@ constraint, for -- example, that would tell us that the @State Int e1@ isn't -- actually used anywhere in the computation. -- -- GHC and editor tools like HLS do a good job of inferring these -- type signatures. -- -- @ -- incrementReadLine :: -- (e1 :> es, e2 :> es, e3 :> es) => -- State Int e1 -> -- Exception String e2 -> -- IOE e3 -> -- Eff es () -- incrementReadLine state exception io = do -- 'Bluefin.Jump.withJump' $ \\break -> 'Control.Monad.forever' $ do -- line <- 'Bluefin.IO.effIO' io getLine -- i <- case 'Text.Read.readMaybe' line of -- Nothing -> -- 'Bluefin.Exception.throw' exception ("Couldn't read: " ++ line) -- Just i -> -- pure i -- -- when (i == 0) $ -- 'Bluefin.Jump.jumpTo' break -- -- 'Bluefin.State.modify' state (+ i) -- @ -- -- Now let's look at how we can run such a function. Each effect -- 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. The type signatures -- of handlers also follow a common pattern, which looks like -- -- @ -- (forall e. \ e -> Eff (e :& es) a) -> Eff es r -- @ -- -- This means that the effect @e@, corresponding to the handle -- @\ e@, has been handled and removed from the set of -- remaining effects, @es@. (The signatures for -- 'Bluefin.Eff.runEff' and 'Bluefin.Eff.runPureEff' are slightly -- different because they remove @Eff@ itself.) Here, then, is -- how we can run @incrementReadLine@: -- -- @ -- runIncrementReadLine :: IO (Either String Int) -- runIncrementReadLine = 'Bluefin.Eff.runEff' $ \\io -> do -- 'Bluefin.Exception.try' $ \\exception -> do -- ((), r) \<- 'Bluefin.State.runState' 0 $ \\state -> do -- incrementReadLine state exception io -- pure r -- -- >>> runIncrementReadLine -- 1 -- 2 -- 3 -- 0 -- Right 6 -- >>>> runIncrementReadLine -- 1 -- 2 -- 3 -- Hello -- Left "Couldn't read: Hello" -- @ -- * Comparison to other effect systems -- ** Everything except effectful -- | The design of Bluefin is strongly inspired by and based on -- effectful. All the points in [effectful's comparison of itself -- to other effect -- systems](https://github.com/haskell-effectful/effectful?tab=readme-ov-file#motivation) -- apply to Bluefin too. -- ** effectful -- | The major difference between Bluefin and effectful is that in -- Bluefin effects are represented as value-level handles whereas -- in effectful they are represented only at the type level. -- effectful could be described as "a well-typed implementation of -- the @ReaderT@ @IO@ pattern", and Bluefin could be described as -- a well-typed implementation of something even simpler: the -- [Handle -- pattern](https://jaspervdj.be/posts/2018-03-08-handle-pattern.html). -- The aim of the Bluefin style of value-level effect tracking is -- to make it even easier to mix effects, especially effects of -- the same type. Only time will tell which approach is preferable -- in practice. -- Haddock seems to have trouble with italic sections spanning -- lines :( -- | "/Why not just implement Bluefin as an alternative API on/ -- /top of effectful?/" -- -- It would be great to share code between the two projects! But -- I don't know to implement Bluefin's "Bluefin.Compound" effects -- in effectful. -- * Implementation -- | Bluefin has a similar implementation style to effectful. -- t'Bluefin.Eff.Eff' is an opaque wrapper around 'IO', -- t'Bluefin.State.State' is an opaque wrapper around -- 'Data.IORef.IORef', and 'Bluefin.Exception.throw' throws an -- actual @IO@ exception. t'Bluefin.Coroutine.Coroutine' is -- implemented simply as a function. -- -- @ -- newtype t'Bluefin.Eff.Eff' (es :: 'Bluefin.Eff.Effects') a = 'Bluefin.Internal.UnsafeMkEff' (IO a) -- newtype t'Bluefin.State.State' s (st :: Effects) = 'Bluefin.Internal.UnsafeMkState' (IORef s) -- newtype t'Bluefin.Coroutine.Coroutine' a b (s :: Effects) = 'Bluefin.Internal.UnsafeMkCoroutine' (a -> IO b) -- @ -- -- The type parameters of kind t'Bluefin.Eff.Effects' are phantom -- type parameters which track which effects can be used in an -- operation. Bluefin uses them to ensure that effects cannot -- escape the scope of their handler, in the same way that the -- type parameter to the 'Control.Monad.ST.ST' monad ensures that -- mutable state references cannot escape -- 'Control.Monad.ST.runST'. When the type system indicates that -- there are no unhandled effects it is safe to run the underlying -- @IO@ action using 'System.IO.Unsafe.unsafePerformIO', which is -- the approach taken to implement 'Bluefin.Eff.runPureEff'. -- Consequently, it is impossible for a pure value retured from -- `runPureEff` to access any Bluefin internal state or throw a -- Bluefin internal exception. -- * Tips -- | * Use @NoMonoLocalBinds@ and @NoMonomorphismRestriction@ for -- better type inference. (You can always change back to the -- default after adding inferred type signatures.) -- -- * Writing a handler often requires an explicit type signature. -- * Creating your own effects -- | See "Bluefin.Compound". -- * Example -- | -- @ -- countPositivesNegatives :: [Int] -> String -- countPositivesNegatives is = 'Bluefin.Eff.runPureEff' $ -- 'Bluefin.State.evalState' (0 :: Int) $ \\positives -> do -- r \<- 'Bluefin.Exception.try' $ \\ex -> -- evalState (0 :: Int) $ \\negatives -> do -- for_ is $ \\i -> do -- case compare i 0 of -- GT -> 'Bluefin.State.modify' positives (+ 1) -- EQ -> throw ex () -- LT -> modify negatives (+ 1) -- -- p <- 'Bluefin.State.get' positives -- n <- get negatives -- -- pure $ -- "Positives: " -- ++ show p -- ++ ", negatives " -- ++ show n -- -- case r of -- Right r' -> pure r' -- Left () -> do -- p <- get positives -- pure $ -- "We saw a zero, but before that there were " -- ++ show p -- ++ " positives" -- @ ) where