This answer is a bit of an over-simplification, but if we define side effects as computations being affected by previous computations, it's easy to see that the Functor
typeclass is insufficient for side effects simply because there is no way to chain multiple computations.
class Functor f where
fmap :: (a -> b) -> f a -> f b
The only thing a functor can do is to alter the end result of a computation via some pure function a -> b
.
However, an applicative functor adds two new functions, pure
and <*>
.
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
The <*>
is the crucial difference here, since it allows us to chain two computations:
f (a -> b)
(a computation which produces a function) and f a
a computation that
provides the parameter to which the function is applied. Using pure
and <*>
it's
possible to define e.g.
(*>) :: f a -> f b -> f b
Which simply chains two computations, discarding the end result from the first one (but possibly applying "side effects").
So in short, it's the ability to chain computations which is the minimum requirement for effects such as mutable state in computations.