bluefin-0.5.0.0: The Bluefin effect system
Safe HaskellNone
LanguageHaskell2010

Bluefin.GadtEffect

Synopsis

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 (GadtEffect FileSystem r e) (GadtEffect FileSystem r es)
  where
  oneWayCoercibleImpl = oneWayCoercibleGadtEffectTrustMe $ \case
    ReadFile path -> ReadFile path
    WriteFile path contents -> WriteFile path contents
    Trace msg body -> Trace msg (useImpl body)

deriving via
  OneWayCoercibleHandle (GadtEffect FileSystem r)
  instance
    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 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 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) =>
  IOE e1 ->
  Exception IOException e2 ->
  (forall e. Send FileSystem e -> Eff (e :& es) r) ->
  Eff es r
runFileSystem io ex = interpret $ \case
  ReadFile path ->
    adapt (IO.readFile path)
  WriteFile path contents ->
    adapt (IO.writeFile path contents)
  Trace msg body -> do
    effIO io (putStrLn ("Start: " <> msg))
    r <- 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 = 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. 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 (GadtEffect E r e) (GadtEffect E r es)
  where
  oneWayCoercibleImpl = oneWayCoercibleGadtEffectTrustMe $ \case
    Op1 -> Op1
    Op2 -> Op2
    Op3 -> Op3

deriving via
  OneWayCoercibleHandle (GadtEffect E r)
  instance
    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 ->
  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 -> 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) #

Bring a Send into scope with interpret.

Instances

Instances details
e :> es => OneWayCoercible (Send f e :: Type) (Send f es :: Type) 
Instance details

Defined in Bluefin.Internal.GadtEffect

Handle (Send f) 
Instance details

Defined in Bluefin.Internal.GadtEffect

Methods

handleImpl :: HandleD (Send f) #

Effectful operations

send #

Arguments

:: forall (e1 :: Effects) (es :: Effects) f r. e1 :> es 
=> Send f e1 
-> f (Eff es) r

Handle this operation using the effect handler currently in scope for the Send f handle.

-> Eff es r 

Send a primitive operation to the handler for interpretation. This is the Bluefin analog of effectful's send and polysemy's send.

passthrough #

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 -> passthrough fc op

Interpretation

type EffectHandler (f :: (Type -> Type) -> Type -> Type) (es :: Effects) #

Arguments

 = forall (e :: Effects) r. f (Eff e) r 
-> Eff (e :& es) r

͘

A convenient type synonym. This is like effectful's EffectHandler. A similar type also appears in polysemy as the argument to functions like intercept.

interpret #

Arguments

:: forall (f :: (Type -> Type) -> Type -> Type) (es :: Effects) r. EffectHandler f es

Implementation of effect handler for Send f

-> (forall (e :: Effects). Send f e -> Eff (e :& es) r)

Within this block, send has the implementation given above.

-> Eff es r 
import System.IO qualified as IO

runFileSystem ::
  forall es e1 e2 r.
  (e1 :> es, e2 :> es) =>
  IOE e1 ->
  Exception IOException e2 ->
  (forall e. Send FileSystem e -> Eff (e :& es) r) ->
  Eff es r
runFileSystem io ex = interpret $ \case
  ReadFile path ->
    adapt (IO.readFile path)
  WriteFile path contents ->
    adapt (IO.writeFile path contents)
  Trace msg body -> do
    effIO io (putStrLn ("Start: " <> msg))
    r <- 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 = rethrowIO io ex (effIO io m)

interpose #

Arguments

:: forall (e1 :: Effects) (es :: Effects) (f :: Effect) r. e1 :> es 
=> (Send f es -> EffectHandler f es)

Reimplementation of effect handler for Send f in terms of the the original effect handler, which is passed as the argument

-> HandleReader (Send f) e1

Original effect handler

-> Eff es r

Within this block, send has the implementation given above.

-> 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 -> passthrough fc op

Effect

type Effect = (Type -> Type) -> Type -> Type #

A convenient type synoynm matching effectful and polysemy's usages provided for people who are migrating from those libraries.

GadtEffect

data GadtEffect (f :: (Type -> Type) -> Type -> Type) a (e :: Effects) #

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 (GadtEffect FileSystem r e) (GadtEffect FileSystem r es)
  where
  oneWayCoercibleImpl = oneWayCoercibleGadtEffectTrustMe $ \case
    ReadFile path -> ReadFile path
    WriteFile path contents -> WriteFile path contents
    Trace msg body -> Trace msg (useImpl body)