I am playing around with freer-simple and trying to work out how to combine effects.
I have an algebra to represent a simple file system and user invoked failure as follows:
data FileSystem r where
ReadFile :: Path a File -> FileSystem String
WriteFile :: Path a File -> String -> FileSystem ()
readFile :: Member FileSystem effs => Path a File -> Eff effs String
readFile = send . ReadFile
writeFile :: Member FileSystem effs => Path a File -> String -> Eff effs ()
writeFile pth = send . WriteFile pth
data AppError r where
Ensure :: Bool -> String -> AppError ()
Fail :: String -> AppError ()
ensure :: Member AppError effs => Bool -> String -> Eff effs ()
ensure condition message = send $ Ensure condition message
fail :: Member AppError effs => String -> Eff effs ()
fail = send . Fail
And an "application" in a function called interactor as follows:
data TestItem = Item {
pre :: String,
post :: String,
path :: Path Abs File
}
data RunConfig = RunConfig {
environment :: String,
depth :: Integer,
path :: Path Abs File
}
type FileSys r = (Member FileSystem r)
type AppFailure r = (Member AppError r)
interactor :: TestItem -> RunConfig -> (AppFailure r, FileSys r) => Eff r ApState
interactor item runConfig = do
let fullFilePath = path (runConfig :: RunConfig)
writeFile fullFilePath $ pre item <> post item
fail "random error ~ its a glitch"
txt <- readFile [absfile|C:\Vids\SystemDesign\Wrong.txt|]
pure $ ApState fullFilePath txt
At this stage I am only interested in dumb "documentation" interpreters that log the steps, I don't even care what failing would do in terms of control flow:
fileSystemDocInterpreter :: FileSystem ~> Eff '[Writer [String], effs]
fileSystemDocInterpreter =
let
mockContents = "Mock File Contents"
in
\case
ReadFile path -> tell ["readFile: " <> show path] $> mockContents
WriteFile path str -> tell ["write file: " <>
show path <>
"\nContents:\n" <>
str]
errorDocInterpreter :: AppError ~> Eff '[Writer [String]]
errorDocInterpreter = \case
Ensure condition errMsg -> tell [condition ? "Ensure Check Passed" $
"Ensure Check Failed ~ " <> errMsg]
Fail errMsg -> tell ["Failure ~ " <> errMsg]
The combined interpreter is as follows:
type FileSys r = (Member FileSystem r)
type AppFailure r = (Member AppError r)
executeDocumented :: forall a. Eff '[FileSystem, AppError] a -> ((a, [String]), [String])
executeDocumented app = run $ runWriter
$ reinterpret errorDocInterpreter
$ runWriter
$ reinterpret fileSystemDocInterpreter app
When I run this with sample configs I get something like the following:
((ApState {
filePath = "C:\\Vids\\SystemDesign\\VidList.txt",
fileText = "Mock File Contents"
},
["write file: \"C:\\\\Vids\\\\SystemDesign\\\\VidList.txt\
"\nContents: I do a test the test runs",
"readFile: \"C:\\\\Vids\\\\SystemDesign\\\\Wrong.txt\""]
),
["Failure ~ random error ~ its a glitch"]
)
I have a couple of questions about the interpreters above:
The order this to compile I had to make the types as follows:
fileSystemDocInterpreter :: FileSystem ~> Eff '[Writer [String], effs]
errorDocInterpreter :: AppError ~> Eff '[Writer [String]]
and call errorDocInterpreter
after fileSystemDocInterpreter
because the
fileSystemDocInterpreter
has trailing effs and errorDocInterpreter
doesn't.
Is there a way to change the type signatures or invoke these so it wouldn't matter which was needed first by the parent interpreter?
Both fileSystemDocInterpreter and errorDocInterpreter use the Writer [String] effect. Is there a way to combine these so runWriter is only called once so the failure and file system messages appear in one log?
The documentation for the Eff
type states that
Normally, a concrete list of effects is not used to parameterize Eff. Instead, the Member or Members constraints are used to express constraints on the list of effects without coupling a computation to a concrete list of effects.
Therefore, in order to maximize flexibility we could change the signatures of fileSystemDocInterpreter
and errorDocInterpreter
to:
fileSystemDocInterpreter :: Member (Writer [String]) effs => FileSystem ~> Eff effs
errorDocInterpreter :: Member (Writer [String]) effs => AppError ~> Eff effs
We don't really care where the Writer [String]
is on the type-level list, of if there are any more effects on the list. We just need Writer [String]
to be there. This change takes care of (1).
As for (2), we could define executeDocumented
as follows:
executeDocumented :: forall a. Eff '[FileSystem, AppError, Writer [String]] a
-> (a, [String])
executeDocumented app = run $ runWriter
$ interpret errorDocInterpreter
$ interpret fileSystemDocInterpreter
$ app
Here we are making use in the interpreter of the flexibility we gained when defining the computations. We put a Writer [String]
at the end of the list, and the two interpret
s send FileSystem
and AppError
s effects to the writer. No need to have separate Writer [String]
layers! (That said, if at other occasion we have two effects of the same type at the front of the list, we can use subsume
to remove the duplication.)