| Safe Haskell | None |
|---|---|
| Language | Haskell2010 |
Bluefin.GadtEffect
Synopsis
- data Send (f :: Effect) (e :: Effects)
- send :: forall (e1 :: Effects) (es :: Effects) f r. e1 :> es => Send f e1 -> f (Eff es) r -> Eff es r
- passthrough :: forall f r (e1 :: Effects) (es :: Effects) (e2 :: Effects). (Handle (GadtEffect f r), e1 :> es, e2 :> es) => Send f e1 -> f (Eff e2) r -> Eff es r
- type EffectHandler (f :: (Type -> Type) -> Type -> Type) (es :: Effects) = forall (e :: Effects) r. f (Eff e) r -> Eff (e :& es) r
- interpret :: forall (f :: (Type -> Type) -> Type -> Type) (es :: Effects) r. EffectHandler f es -> (forall (e :: Effects). Send f e -> Eff (e :& es) r) -> Eff es r
- interpose :: forall (e1 :: Effects) (es :: Effects) (f :: Effect) r. e1 :> es => (Send f es -> EffectHandler f es) -> HandleReader (Send f) e1 -> Eff es r -> Eff es r
- type Effect = (Type -> Type) -> Type -> Type
- data GadtEffect (f :: (Type -> Type) -> Type -> Type) a (e :: Effects)
- oneWayCoercibleGadtEffectTrustMe :: forall (e :: Effects) (es :: Effects) f r. e :> es => (forall (e' :: Effects) (es' :: Effects). e' :> es' => f (Eff e') r -> f (Eff es') r) -> OneWayCoercibleD (GadtEffect f r e) (GadtEffect f r es)
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) =>OneWayCoercible(GadtEffectFileSystem r e) (GadtEffect FileSystem r es) whereoneWayCoercibleImpl=oneWayCoercibleGadtEffectTrustMe$ \case ReadFile path -> ReadFile path WriteFile path contents -> WriteFile path contents Trace msg body -> Trace msg (useImpl body) deriving viaOneWayCoercibleHandle(GadtEffectFileSystem r) instanceHandle(GadtEffect FileSystem r)
Then we can define functions that implement the primitive
effectful operations for FileSystem:
readFile :: (e1 :> es) =>SendFileSystem e1 -> FilePath -> Eff es String readFile fc path =sendfc (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
and
makeSem
respectively) but there is no such thing for Bluefin yet,
sorry! Please open an
issue if
that causes difficulties for you.
Finally we can write a handler for the
effect that gives it an interpretation via Send FileSysteminterpret:
import System.IO qualified as IO runFileSystem :: forall es e1 e2 r. (e1 :> es, e2 :> es) =>IOEe1 ->ExceptionIOExceptione2 -> (forall e.SendFileSystem e -> Eff (e :& es) r) -> Eff es r runFileSystem io ex =interpret$ \case ReadFile path -> adapt (IO.readFilepath) WriteFile path contents -> adapt (IO.writeFilepath contents) Trace msg body -> doeffIOio (putStrLn ("Start: " <> msg)) r <-useImplbody 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 =rethrowIOio 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. 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) =>OneWayCoercible(GadtEffectE r e) (GadtEffect E r es) whereoneWayCoercibleImpl=oneWayCoercibleGadtEffectTrustMe$ \case Op1 -> Op1 Op2 -> Op2 Op3 -> Op3 deriving viaOneWayCoercibleHandle(GadtEffect E r) instanceHandle(GadtEffect E r)
and a handler for the effect:Send E
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 ->passthroughfc op
Using interpose is similar:
augmentOp2Interpose :: (e1 :> es, e2 :> es) => IOE e2 ->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 ->passthroughfc 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 -> runHandleReader fc $ \hr -> do
augmentOp2Interpose io hr $ asksHandle hr action
ghci> example -- interpret: op1 augmented op2 op2 op3 -- interpose: op1 augmented op2 op2 op3
Handle
data Send (f :: Effect) (e :: Effects) #
Instances
| e :> es => OneWayCoercible (Send f e :: Type) (Send f es :: Type) | |
Defined in Bluefin.Internal.GadtEffect Methods oneWayCoercibleImpl :: OneWayCoercibleD (Send f e) (Send f es) # | |
| Handle (Send f) | |
Defined in Bluefin.Internal.GadtEffect Methods handleImpl :: HandleD (Send f) # | |
Effectful operations
Arguments
| :: forall f r (e1 :: Effects) (es :: Effects) (e2 :: Effects). (Handle (GadtEffect f r), e1 :> es, e2 :> es) | |
| => Send f e1 | |
| -> f (Eff e2) r | |
| -> Eff es r | ͘ |
Version of send for use when pattern matching in interpose
augmentOp2Interpose :: (e1 :> es, e2 :> es) => IOE e2 ->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 ->passthroughfc op
Interpretation
type EffectHandler (f :: (Type -> Type) -> Type -> Type) (es :: Effects) #
A convenient type synonym. This is like effectful's
EffectHandler.
A similar type also appears in polysemy as the argument to
functions like
intercept.
Arguments
| :: forall (f :: (Type -> Type) -> Type -> Type) (es :: Effects) r. EffectHandler f es | Implementation of effect handler for |
| -> (forall (e :: Effects). Send f e -> Eff (e :& es) r) | Within this block, |
| -> Eff es r |
import System.IO qualified as IO runFileSystem :: forall es e1 e2 r. (e1 :> es, e2 :> es) =>IOEe1 ->ExceptionIOExceptione2 -> (forall e.SendFileSystem e -> Eff (e :& es) r) -> Eff es r runFileSystem io ex =interpret$ \case ReadFile path -> adapt (IO.readFilepath) WriteFile path contents -> adapt (IO.writeFilepath contents) Trace msg body -> doeffIOio (putStrLn ("Start: " <> msg)) r <-useImplbody 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 =rethrowIOio ex (effIO io m)
Arguments
| :: forall (e1 :: Effects) (es :: Effects) (f :: Effect) r. e1 :> es | |
| => (Send f es -> EffectHandler f es) | Reimplementation of effect handler for |
| -> HandleReader (Send f) e1 | Original effect handler |
| -> Eff es r | Within this block, |
| -> Eff es r |
augmentOp2Interpose :: (e1 :> es, e2 :> es) => IOE e2 ->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 ->passthroughfc op
Effect
GadtEffect
oneWayCoercibleGadtEffectTrustMe #
Arguments
| :: forall (e :: Effects) (es :: Effects) f r. e :> es | |
| => (forall (e' :: Effects) (es' :: Effects). e' :> es' => f (Eff e') r -> f (Eff es') r) | |
| -> OneWayCoercibleD (GadtEffect f r e) (GadtEffect f r es) | ͘ |
instance (e :> es) =>OneWayCoercible(GadtEffectFileSystem r e) (GadtEffect FileSystem r es) whereoneWayCoercibleImpl=oneWayCoercibleGadtEffectTrustMe$ \case ReadFile path -> ReadFile path WriteFile path contents -> WriteFile path contents Trace msg body -> Trace msg (useImpl body)