But you can have generic effects. Your arguments and return type can specify "any effect", indicating your function can use a type with any effect safely, or can be used in any effect context safely.
Passing an async value to a function doesn't mean that function must now also be an async function. It can be a "for all effects, do the thing" function. The code duplication problem is gone.
Someone writes a post lamenting red and blue functions, and everyone eats it up.
Substitute colour for something meaningful and the idea becomes idiotic.
"Top level function declares that it is non-blocking, but when I try to call a small blocking function from it, I have to change the declaration to blocking???"
Yes, yes you do.
Total functions can't call non-total functions.
Deterministic functions can't call nondeterministic functions.
Non-IO functions can't call IO functions.
How do you handle logging then? If f() calls g(), how can I add logging to g() without having to change or recompile f() (and everything in the call stack above it)? ‘You can’t’ is not an acceptable answer.
type Log = Yield String
-- workWithLogging cannot do arbitrary IO!
-- All it can do is yield log messages, which
-- must be processed elsewhere.
workWithLogging ::
(e1 :> es) =>
Log e1 ->
Int ->
Int ->
Eff es Int
workWithLogging l x y = do
yield l ("x was " <> show x)
yield l ("y was " <> show y)
let result = x + y
yield l ("result was " <> show result)
pure result
-- ghci> example
-- x was 5
-- y was 7
-- result was 12
-- 12
example :: IO Int
example = runEff $ \io -> do
-- forEach determines how each log message
-- should be handled.
forEach
(\l-> workWithLogging l 5 7)
(\logMsg -> effIO io (putStrLn logMsg))"You can't" should be the ONLY acceptable answer.