Motivation: To be able to control effects in MTL like we can in Free
/Freer
-style.
The example might be slightly contrived—imagine a program with some basic operations (GHC 8.2 using freer-simple
),
#!/usr/bin/env stack
-- stack --resolver lts-10.2 --install-ghc runghc --package freer-simple
{-# LANGUAGE GADTs #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
import Control.Monad.Freer
data Effect next where
ReadFilename :: String -> Effect String
WriteOutput :: Show a => a -> Effect ()
Computation :: Int -> Int -> Effect Int
readFilename :: Member Effect effs => String -> Eff effs String
readFilename = send . ReadFilename
writeOutput :: (Member Effect effs, Show a) => a -> Eff effs ()
writeOutput = send . WriteOutput
computation :: Member Effect effs => Int -> Int -> Eff effs Int
computation i1 i2 = send $ Computation i1 i2
With this, we can model a program that does some simple operations, reading a file and outputting it, doing a computation and outputting it,
program :: Eff '[Effect, IO] ()
program = do
contents <- readFilename "test.txt"
writeOutput contents
result <- computation 12 22
writeOutput result
We can then implement our interpreters, which will be were we decide how to run the code. Our first interpreter will be placed client-side, and will run IO
instructions locally, and it will send off the pure instructions to a server, for calculation,
runClientEffect :: Eff '[Effect, IO] a -> IO a
runClientEffect = runM . interpretM (\case
ReadFilename filename -> readFile filename
WriteOutput s -> print s
Computation i1 i2 -> do
print "Imagine the networking happening here"
pure $ i1 + i2)
The server side we can skip for now.
What this hopefully demonstrates is an interface that relies on some aspects being pure, and thereby able to be sent to a server, while others are impure, and are run locally.
What I struggle with, is how to do this in an MTL-style, namely how to limit the amount of IO
that can be done in the monad operations.
Let me know if the question is too vague!
That's quite straightforward.
import Control.Monad.IO.Class
Instead of an Effect
datatype representing the syntax of our DSL, we define an EffectMonad
typeclass that abstracts the monad itself.
class Monad m => EffectMonad m where
readFilename :: String -> m String
writeOutput :: Show a => a -> m ()
computation :: Int -> Int -> m Int
The program is the same (up to the type signature).
program :: EffectMonad m => m ()
program = do
contents <- readFilename "test.txt"
writeOutput contents
result <- computation 12 22
writeOutput result
And an interpreter is given as an instance (if you are layering effects with transformers, that's where we run into the O(n*m)
instance problem).
instance EffectMonad IO where
readFilename filename = readFile filename
writeOutput s = print s
computation i1 i2 = do
print "Imagine the networking happening here"
pure $ i1 + i2
Then running the program is just instantiating it with the right type.
main :: IO ()
main = program