Monads for effects are familiar to the Haskell programmer; they were introduced by Wadler's "Monads for functional programming" and are the accepted way to work with side effects in a lazy language.

Note that managing side effects is necessary in a call-by-need language: calling print : String -> () twice with the same value would not execute the print procedure twice; output would not be printed twice.

Wadler's approach was brilliant and timely. Nearly all I/O in a typical Haskell program is done via the monadic interface. This is why monads are famously associated with Haskell: it is hard to imagine doing I/O in a lazy language without reinventing the monad.

Nevertheless, there remain some practical limitations to Haskell's IO type.

Laziness & Streaming FFI

There are several unsatisfying tidbits w.r.t. C FFI:

Philosophical Foundations

There are some philosophical objections to Haskell's IO type: As pointed out by Wadler in Linear types can change the world!, one can duplicate or discard the RealWorld:

kill : RealWorld -> ()

dupl : RealWorld -> (RealWorld, RealWorld)

Moreover, it is not clear that all effecting functions need take place in the IO monad: perhaps one could use a CFFI token for interacting with an appropriate C API (say, for streaming decompression) and another for console output.

Confusion

mtl-Replacements

Some Haskell libraries (and even academic papers) claim to offer "effects" - but are really more like mtl replacements.

None address the C FFI cases above, and in any case GHC still only accepts FFI bindings through IO.

Algebraic Effects

Some languages (ATS, Kitten, Mirth, Idris, PureScript) track effects at the type level, but they do not inform the above problems around laziness. In fact, ATS does not require a token to be passed around for effects and does not use monads at all.