Monads in Haskell can be thought of as composable computation descriptions. The essence of monad is thus separation of composition timeline from the composed computation's execution timeline, as well as the ability of computation to implicitly carry extra data, as pertaining to the computation itself, in addition to its one (hence the name) output, that it will produce when run (or queried, or called upon). This lends monads to supplementing pure calculations with features like I/O, common environment, updatable state, etc.
Each monad, or computation type, provides means, subject to Monad Laws, to
- (a) create a description of a computation that will produce (a.k.a. "return") a given Haskell value, and
- (b) combine (a.k.a. "bind") a computation description with a reaction to it, – a pure Haskell function that is set to receive a computation-produced value (when and if that happens) and return another computation description, using or dependent on that value if need be, – creating a description of a combined computation that will feed the original computation's output through the reaction while automatically taking care of the particulars of the computational process itself.
Reactions are thus computation description constructors. A monad might also define additional primitives to provide access to and/or enable manipulation of data it implicitly carries, specific to its nature; cause some specific side-effects; etc..
Sometimes the specific monadic type also provides the ability to somehow (c) run a computation description, getting its result back into Haskell if computations described by the monad are pure, but this is expressly not a part of the Monad interface. Officially, you can't get the
a out of
M a directly, only arrange for it to be "fed" into the next computation's constructor, the "reaction", indirectly. In case of an
IO monad value, a computation it describes runs implicitly as a part of the chain of I/O computation descriptions composed together into the value
main (of type
IO ()) in a given Haskell program, by convention.
join is another function expressing the essence of monad; where
m >>= k = k =<< m = join (k <$> m) = join (fmap k m); it is prefered in mathematics, over the bind; both express the same concept).
Thus in Haskell, though it is a purely-functional language, side effects that will be performed by a computation can be dealt with and combined purely at the monad's composition time. Monads thus resemble programs in a particular EDSL (embedded domain-specific language, "embedded" because the values denoting these computations are legal Haskell values, not some extraneous annotations).
While programs may describe impure effects and actions outside Haskell, they can still be combined and processed ("assembled") purely, inside Haskell, creating a pure Haskell value - a computation action description that describes an impure calculation. That is how Monads in Haskell help keep the pure and the impure apart.
The computation doesn't have to be impure and can be pure itself as well. Then monads serve to provide the benefits of separation of concerns, and automatic creation of a computational "pipeline".