Search code examples
haskellmonad-transformersfree-monad

Limiting effects, like with `Freer`, using MTL-style


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!


Solution

  • 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