bluefin-0.4.3.0: src/Bluefin/GadtEffect.hs
module Bluefin.GadtEffect
( -- * Introduction
-- | The Haskell effect systems @effectful@ and @polysemy@ allow
-- users to define new effects by defining a GADT (generalized
-- algebraic data type) whose contructors correspond to primitive
-- operations of the effect, and then creating values of the GADT
-- and interpreting them in terms of existing effects. This
-- module provides Bluefin's equivalent. In fact, it @effectful@
-- and @polysemy@ this is essentially the /only/ way you can
-- create new effects. That's not true for Bluefin. Bluefin
-- supports a rich collection of ways to create new effects, most
-- of which are documented at "Bluefin.Compound". This particular
-- module might be helpful for users coming from @effectful@ and
-- @polysemy@, however.
-- * Example filesystem effect
-- | First we define a GADT with a constructor for each primitive
-- operation of the effect we want to define. Here the primitive
-- operations are to read a file, write a file and to wrap an
-- effectful computation in a "trace" block.
--
-- @
-- data FileSystem :: 'Effect' where
-- ReadFile :: FilePath -> FileSystem m String
-- WriteFile :: FilePath -> String -> FileSystem m ()
-- Trace :: String -> m r -> FileSystem m r
-- @
--
-- Then we need to define two instances for @FileSystem@:
--
-- @
-- instance
-- (e :> es) =>
-- t'Bluefin.Compound.OneWayCoercible' ('GadtEffect' FileSystem r e) (GadtEffect FileSystem r es)
-- where
-- 'Bluefin.Compound.oneWayCoercibleImpl' = 'oneWayCoercibleGadtEffectTrustMe' $ \\case
-- ReadFile path -> ReadFile path
-- WriteFile path contents -> WriteFile path contents
-- Trace msg body -> Trace msg (useImpl body)
--
-- deriving via
-- t'Bluefin.Compound.OneWayCoercibleHandle' ('GadtEffect' FileSystem r)
-- instance
-- t'Bluefin.Compound.Handle' (GadtEffect FileSystem r)
-- @
--
-- Then we can define functions that implement the primitive
-- effectful operations for @FileSystem@:
--
-- @
-- readFile ::
-- (e1 :> es) =>
-- 'Send' FileSystem e1 ->
-- FilePath ->
-- Eff es String
-- readFile fc path =
-- 'send' fc (ReadFile path)
--
-- writeFile ::
-- (e1 :> es) =>
-- Send FileSystem e1 ->
-- FilePath ->
-- String ->
-- Eff es ()
-- writeFile fc path content =
-- send fc (WriteFile path content)
--
-- trace ::
-- (e1 :> es) =>
-- Send FileSystem e1 ->
-- String ->
-- Eff es r ->
-- Eff es r
-- trace fc msg body =
-- send fc (Trace msg body)
-- @
--
-- The instances and primitive effectful operations are
-- boilerplate. @effectful@ and @polysemy@ have Template Haskell
-- for generating their boilerplate
-- ([@makeEffect@](https://hackage.haskell.org/package/effectful-th/docs/Effectful-TH.html#v:makeEffect)
-- and
-- [@makeSem@](https://hackage.haskell.org/package/polysemy-1.9.2.0/docs/Polysemy.html#v:makeSem)
-- respectively) but there is no such thing for Bluefin yet,
-- sorry! Please [open an
-- issue](https://github.com/tomjaguarpaw/bluefin/issues/new) if
-- that causes difficulties for you.
--
-- Finally we can write a handler for the @'Send' FileSystem@
-- effect that gives it an interpretation via 'interpret':
--
-- @
-- import System.IO qualified as IO
--
-- runFileSystem ::
-- forall es e1 e2 r.
-- (e1 :> es, e2 :> es) =>
-- t'Bluefin.IO.IOE' e1 ->
-- t'Bluefin.Exception.Exception' t'Control.Exception.IOException' e2 ->
-- (forall e. 'Send' FileSystem e -> Eff (e :& es) r) ->
-- Eff es r
-- runFileSystem io ex = 'interpret' $ \\case
-- ReadFile path ->
-- adapt (IO.'System.IO.readFile' path)
-- WriteFile path contents ->
-- adapt (IO.'System.IO.writeFile' path contents)
-- Trace msg body -> do
-- 'Bluefin.IO.effIO' io (putStrLn ("Start: " <> msg))
-- r <- 'Bluefin.Compound.useImpl' body
-- effIO io (putStrLn ("End: " <> msg))
-- pure r
-- where
-- -- If you don't want to write this signature you can use
-- -- {-# LANGUAGE NoMonoLocalBinds #-}
-- adapt :: (e1 :> es', e2 :> es') => IO r' -> Eff es' r'
-- adapt m = 'Bluefin.IO.rethrowIO' io ex (effIO io m)
-- @
-- * @interpose@ example
-- | If you're familiar with @effectful@'s @interpose@ function
-- you may want to use Bluefin's equivalent. To see how, let's
-- replicate [@effectful@'s interpose
-- example](https://hackage-content.haskell.org/package/effectful-core-2.6.1.0/docs/Effectful-Dispatch-Dynamic.html#v:interpose). First
-- we define a simple effect with three primitive operations:
--
-- @
-- data E :: 'Effect' where
-- Op1 :: E m ()
-- Op2 :: E m ()
-- Op3 :: E m ()
-- @
--
-- Then we define the boilerplate instances
--
-- @
-- instance
-- (e :> es) =>
-- t'Bluefin.Compound.OneWayCoercible' ('GadtEffect' E r e) (GadtEffect E r es)
-- where
-- 'Bluefin.Compound.oneWayCoercibleImpl' = 'oneWayCoercibleGadtEffectTrustMe' $ \\case
-- Op1 -> Op1
-- Op2 -> Op2
-- Op3 -> Op3
--
-- deriving via
-- t'Bluefin.Compound.OneWayCoercibleHandle' (GadtEffect E r)
-- instance
-- t'Bluefin.Compound.Handle' (GadtEffect E r)
-- @
--
-- and a handler for the @'Send' E@ effect:
--
-- @
-- runE ::
-- (e1 :> es) =>
-- IOE e1 ->
-- (forall e. Send E e -> Eff (e :& es) r) ->
-- Eff es r
-- runE io = interpret $ \\case
-- Op1 -> effIO io (putStrLn "op1")
-- Op2 -> effIO io (putStrLn "op2")
-- Op3 -> effIO io (putStrLn "op3")
-- @
--
-- Before using 'interpose', let's look at a use of its simpler
-- cousin, 'interpret':
--
-- @
-- augmentOp2Interpret ::
-- (e1 :> es, e2 :> es) =>
-- IOE e2 ->
-- Send E e1 ->
-- (forall e. Send E e -> Eff (e :& es) r) ->
-- Eff es r
-- augmentOp2Interpret io fc = 'interpret' $ \\case
-- Op2 -> effIO io (putStrLn "augmented op2") >> send fc Op2
-- op -> 'passthrough' fc op
-- @
--
-- Using 'interpose' is similar:
--
-- @
-- augmentOp2Interpose ::
-- (e1 :> es, e2 :> es) =>
-- IOE e2 ->
-- t'Bluefin.HandleReader.HandleReader' (Send E) e1 ->
-- Eff es r ->
-- Eff es r
-- augmentOp2Interpose io = 'interpose' $ \\fc -> \\case
-- Op2 -> effIO io (putStrLn "augmented op2") >> send fc Op2
-- op -> 'passthrough' fc op
-- @
--
-- And now let's see what they each do:
--
-- @
-- example :: IO ()
-- example = runEff $ \\io -> do
-- let action fc = do
-- send fc Op1
-- send fc Op2
-- send fc Op3
--
-- effIO io (putStrLn "-- interpret:")
-- runE io $ \\fc -> do
-- augmentOp2Interpret io fc $ \\fc' -> action fc'
--
-- effIO io (putStrLn "-- interpose:")
-- runE io $ \\fc -> 'Bluefin.HandleReader.runHandleReader' fc $ \\hr -> do
-- augmentOp2Interpose io hr $ 'Bluefin.HandleReader.asksHandle' hr action
-- @
--
-- @
-- ghci> example
-- -- interpret:
-- op1
-- augmented op2
-- op2
-- op3
-- -- interpose:
-- op1
-- augmented op2
-- op2
-- op3
-- @
-- * Handle
Send,
-- * Effectful operations
send,
passthrough,
-- * Interpretation
EffectHandler,
interpret,
interpose,
-- * @Effect@
Effect,
-- * @GadtEffect@
GadtEffect,
oneWayCoercibleGadtEffectTrustMe,
)
where
import Bluefin.Internal.GadtEffect