Effects in Haskell
You might have come across the term "effects" in Haskell and found it a bit confusing. You've probably heard of "side effects" - these are operations that interact with the outside world, like making network calls or interacting with the file system. In Haskell, the IO monad is required for these sorts of functions. Without IO, you can't interact with the outside world — as (almost) everything is a pure function in Haskell.
Here's a straightforward representation (that I've read) that is intuitive to me.
effect = side effects ------------ side
Here's some example code showing use without any effects:
-- No side effects here!
-- This function can't interact with the outside world.
firstNameOf :: User -> Text
firstNameOf = ...
lastLoginTimeBy :: User -> IO Time
lastLoginTimeBy user = ...
currentTime :: IO Time
currentTime = ...
getDbReport :: User -> IO DbReport
getDbReport user = do
lastLogin <- lastLoginTimeBy user
currTime <- currentTime
...
runReport :: User -> IO DbReport
runReport user = ...
Now, effects are like IO, but they're tracked in the type system. Think of an effect system as breaking IO into smaller, more defined parts. For instance, we might have effects for database access, logging, or reading the current time. This makes it clear from the function signature what parts of the outside world the function interacts with. We can also swap out how these interactions are handled (via swapping effect interpreters). For example, we could use an in-memory database instead of a real one, change how your program reads time, etc.
With effects, the code might look something like this (syntax will vary depending on the implementation):
someIO :: IO ()
someIO = ...
-- Can only perform operations defined by DbEff
lastLoginTimeBy :: Has DbEff sig m => User -> m Time
lastLoginTimeBy user = ...
-- Can only perform operations defined by TimeEff
currentTime :: Has TimeEff sig m => m Time
currentTime = ...
-- Can perform operations defined by DbEff and TimeEff
getDbReport :: (Has DbEff sig m, Has TimeEff sig m) => User -> m DbReport
getDbReport user = ...
-- Can only call functions with DbEff and TimeEff signatures
-- For example, someIO() cannot be called here.
Any function using getDbReport must also support DbEff and TimeEff (effects are composable). Essentially, we're giving "side effects" a type, which helps with compile-time checks. Personally, I find this more sane compared to monad transformers.
If you're looking for an effect system to use, I recommend fused-effects. It supports algebraic higher-order effects, includes many common effects, and has a non-ugly syntax. It is also used in industry as well.