Since the past couple of months, I’ve been playing around with effectful. What I like in particular, is this video, which advocates for creating some hierarchy of effects. I find it very satisfying to build a bunch of effects, and then express my program in terms of these effects. I also like that effects are easy to refactor and restructure.
What I’m not too comfortable about, though, is this:
main :: IO ()
main = do
exitCode <-
runEff $
runCliIO $
runFilesIO $
runInterruptible $
Env.runEnvironment $
runLoggingIO
run
exitWith exitCode
Another example, source:
{-# LANGUAGE PartialTypeSignatures #-}
{-# OPTIONS_GHC -Wno-partial-type-signatures #-}
runEffects :: Eff _listOfAllEffects a -> IO a
runEffects =
runEff
. Logger.logStdout
. Config.runGetConfig
. Environment.runEnvironment
. Concurrent.runConcurrent
. Error.runFailOnError @CommandError
. Error.runFailOnError @Secrets.SecretError
. Command.runCommand
. Sqlite.runSqliteIO
. Secrets.runSecrets
. runMountDrive
. RSync.runRSync
. MostRecentBackup.runMostRecentBackupStateSqlite
. ExternalDiskBackup.runExternalDiskBackup
. Time.runCurrentTime
. PeriodicBackup.runPeriodicBackup
The more effects a program has, the bigger this Eff [biglist] a -> IO a
becomes. This seems to be because you have to run the effects of the entire hierarchy, and not just the ones at the top.
From an architectural point of view, what would be the best strategy?
- Hide effects by using the
reinterpret
function, with the downside that mocking becomes harder - Accept the big list of effects in a single big handler, and deal with it by e.g. using the type hole in the example above, or writing a type alias for the effects list.
- Avoid having/using many effects in the first place.
- Split up the
Eff someList a -> IO a
by making/combining interpreters that interpret multiple effects at once - Stop using effects in favour of X (not really what I’m aiming to discuss here)
In defense of the big effects list, I do like that it provides a nice summary of how my program interacts with the world. From the second snippet above, one can tell immediately that my program does stuff with SQLite, backups, rsync, running commands, etc. That’s a nice summary, and a simple “go to definition” will lead to the answer of “how”.