freer-simple
freer-simple copied to clipboard
Documentation about composing IO effects
IMO one common use case for this library is to have an algebra of effects over some IO actions. You never end up with only one effect using IO. For example you may need to wrap file handling in an effect and also spawning external processes. I couldn't find out how to compose the interpretation of these in any of the docs and since this is a new library there are few other resources. In the end I was able to compose interpretM
functions for each however it required quit a few type annotations to get it to work. Something like this:
-- this is in my Main module
interpretIO ::
(LastMember IO effs, Members '[ IO] effs)
=> Eff (Console ': File ': Process ': Error AppError ': effs) a
-> Eff effs (Either AppError a)
interpretIO =
runError . Process.interpretIO . File.interpretIO . Console.interpretIO
runApp :: Spec -> IO ()
runApp spec = do
eRes <- runM . interpretIO . runSpec $ spec
case eRes of
Left (ProcessFailed ec) -> exitWith $ ExitFailure ec
Right res -> pure ()
Where File.interpretIO
is:
interpretIO ::
(LastMember IO effs, Members '[ IO] effs)
=> Eff (File ': effs) a
-> Eff effs a
interpretIO =
interpretM
(\case
WriteFile path text -> Text.writeFile path text
ReadFile path -> tryReadFile path
CreateDirectoryIfMissing parents path ->
Dir.createDirectoryIfMissing parents path
GetHomeDirectory -> Dir.getHomeDirectory
GetCurrentDirectory -> Dir.getCurrentDirectory)
As an example of the type inference problem, I was forced to create interpretIO
in Main since eRes <- runM . runError . Process.interpretIO . File.interpretIO . Console.interpretIO . runSpec $ spec
would not type check, there were issues with constraints.
Of course I may be doing this completely the wrong way but I need some help from the documentation on this :-)
Hi I am starting to dig into this stuff as well. In this example there are 2 effects ensure (which should be rewritten terms of Error but is just being used as a second effect) and file system.
Here is my solution:
{- File System Lang -}
data FileSystem r where
ReadFile :: Path a File -> FileSystem StrictReadResult
WriteFile :: Path a File -> String -> FileSystem ()
readFile :: Member FileSystem effs => Path a File -> Eff effs StrictReadResult
readFile = send . ReadFile
writeFile :: Member FileSystem effs => Path a File -> String -> Eff effs ()
writeFile pth = send . WriteFile pth
{- Ensure Lang -}
data AppEnsure r where
Ensure :: Bool -> String -> AppEnsure ()
FailEn :: String -> AppEnsure ()
ensure :: Member AppEnsure effs => Bool -> String -> Eff effs ()
ensure condition message = send $ Ensure condition message
failEn :: Member AppEnsure effs => String -> Eff effs ()
failEn = send . FailEn
{- File System IO Interpreter -}
fileSystemIOInterpreter :: forall effs a. LastMember IO effs => Eff (FileSystem ': effs) a -> Eff effs a
fileSystemIOInterpreter = interpretM $ \case
ReadFile path -> F.readFileUTF8 path
WriteFile path str -> F.writeFileUTF8 path str
{- Ensure IO Interpreter -}
ensureIOInterpreter :: forall effs a. LastMember IO effs => Eff (AppEnsure ': effs) a -> Eff effs a
ensureIOInterpreter = interpretM $ \case
Ensure condition errMsg -> Monad.unless condition $ Monad.fail $ toList errMsg
FailEn errMsg -> Monad.fail $ toList errMsg
{- Application (Interactor) -}
data ApState = ApState {
filePath :: Path Abs File,
fileText :: StrictReadResult
}
deriving Show
data TestItem = Item {
pre :: String,
post :: String,
path :: Path Abs File
}
data RunConfig = RunConfig {
environment :: String,
depth :: Integer,
path :: Path Abs File
}
interactor :: Members '[AppEnsure, FileSystem] effs => TestItem -> RunConfig -> Eff effs ApState
interactor item runConfig = do
let fullFilePath = path (runConfig :: RunConfig)
writeFile fullFilePath $ pre item <> " ~ " <> post item <> " !!"
-- failEn "random error ~ its a glitch"
txt <- readFile fullFilePath
pure $ ApState fullFilePath txt
{- Application IO Interpreter -}
executeInIO :: forall a. Eff '[FileSystem, AppEnsure, IO] a -> IO a
executeInIO app = runM $ ensureIOInterpreter
$ fileSystemIOInterpreter
app
{- Demo Execution -}
sampleItem = Item {
pre = "I do a test",
post = "the test runs",
path = [absfile|C:\Vids\SystemDesign\VidList.txt|]
}
sampleRunConfig = RunConfig {
environment = "Test",
depth = 44,
path = [absfile|C:\Vids\SystemDesign\VidList.txt|]
}
-- Demos
demoExecuteInIO = executeInIO $ interactor sampleItem sampleRunConfig
@shmish111 I cannot reproduce your problem. The code that you provide is not complete, so it is not runnable, but I tried to reduce what you provided to a minimal test case. However, what I ended up with typechecks. This is my program:
import Control.Monad.Freer
import Control.Monad.Freer.Error
data AppError = ProcessFailed
data Console r
data File r
data Process r
processInterpretIO :: LastMember IO effs => Eff (Process ': effs) ~> Eff effs
processInterpretIO = undefined
fileInterpretIO :: LastMember IO effs => Eff (File ': effs) ~> Eff effs
fileInterpretIO = undefined
consoleInterpretIO :: LastMember IO effs => Eff (Console ': effs) ~> Eff effs
consoleInterpretIO = undefined
data Spec
runSpec :: Members '[Console, File, Process, Error AppError] effs => Spec -> Eff effs ()
runSpec = undefined
runApp :: Spec -> IO ()
runApp spec = do
eRes <- runM . runError . processInterpretIO . fileInterpretIO . consoleInterpretIO . runSpec $ spec
case eRes of
Left ProcessFailed -> pure ()
Right _ -> pure ()
Is something different about your program that makes it fail to typecheck?
@lexi-lambda the issue was that I wasn't including forall a.
or using ~>
which implies it. I think the title of the issue is relevant here, what I needed was documentation around this, i.e. I should have used ~>
and why. I also think I understand why this is but a brief explanation would also be nice.
Could you post the type you tried assigning to Process.interpretIO
and friends? I’d like to better understand your misunderstanding.