Monadic Vedanta
The problem why Monads
in Haskell are so confusing is that we (at
least I) are trying to understand it bottom-up, from what we see in
some crappy code. Not just that, but when we search for an explanation,
shitposts by narcissistic imposers is what Google Search will give you
first.
It might take years (at least for me) to sort everything out, and reduce everything back to What Is.
It took so long because I have to went through piles and piles of bullshit, including the Category Theory, to make sure that it is completely irrelevant in this context.
There are 3 orthogonal notions within this concept in the context of Haskell.
- These things encapsulate computations, similar to what functions do. Just like functions, they are first-class values.
- These things are composeable, just like functions, and form a
Monoid with respect the composition operation.
- These things are a proper Abstract Data Types - particular instances of a
general type-class. This means that a Monad is a generalized universal pattern (verbosely expressed).
Last, but not least, the actual abstraction barrier is enforced by the type-checker (conceptually, Monadic values are tagged with an additional tag and cannot be mixed with ordinary tagged values).
Data-constructors plays the role of type-tags for absract types.
Nesting is the only way to define composition of functions in mathematics and in Haskell, and nesting implies a particular order of evaluation (serialization). This is why their composition is an arrow.
There are common patterns of usage emerged
- Propagation of “nothingness”, as in
Maybe
or aList
. - Threading (as in beads on a rosary) by implicitly (behind the abstraction barrier) passing an additional parameter, and returning an additional value.
Serialization of
IO
(via /abstract threading - passing the World along).In all cases the referential transparency property still holds, so monadic programming (composition) is still pure-functional.
Abstraction barrier
The confusion and frustration come from a lack of clear separation between interfaces and implementation.
A pure function could
- take an extra argument (usually a context)
- return an extra argument (usually a modified state)
Abstractly or conceptually, these extras could be separated and completely hidden by an abstraction barrier.
Composition
These abstractions should be composable, just like ordinary functions \[g \circ f = g(f(x))\]
g . f = \x -> g (f x)
Functor
Just like ordinary functions, these abstractions can be Functoral
instance Functor ((->) r) where fmap = (.)
So a Functor is required and the type-class Monad is a subclass of a Applicative, which is a special kind of Functor.
class Applicative m => Monad m where (>>=) :: forall a b. m a -> (a -> m b) -> m b return :: forall a. a -> m a return = pure
These, however, are Haskellisms - fancies of library writers. Applicative is NOT required at all.
Moreover, implementations of Functor instances break an abstraction (makes it leaky) by “looking at and manipulating the guts”, which is a violation of the abstraction principle.
Formulations
In the context of a List
xs >>= f = concat (map f xs) return x = [x]
were f
must use return
and have the type f : a -> m b
And, of course, List
is a “classic” Functor
instance Functor [a] where fmap = map
For other, arbitrary instances there is join
join :: Monad m => m (m a) -> m a
which is structurally just like concat
for Lists
concat :: Foldable t => t [a] -> [a]
The Proper Philosophy
Life Itself has been evolved upon certain molecular structures, which are made out of Atoms. Life does not know anything about their nature, it is just using them as given.
If it could make any assumptions, that would be, as we assume, that they are indivisible and indestructible, and some particular molecular structures such as aminoacids or RNAs are immutable.
The fact that atoms can actually be broken in certain conditions (created by humans on this planet) is irrelevant, because in the actual environment in which Life has been evolved such conditions do not arise.
What we have here is a very real, not imaginary, abstraction barrier, so real an actual, that literally everything within you and around you is made upon it.
And this is the Reality First Principle for functional programming - atomicity and immutability of base structures, which is only apparent.
The Highest level
At a highest, most abstract level a Monad in Haskell is a generalization of an abstraction barrier similar to that one which holds Life Itself.
Functions on Monadic Values (of a particular type) are as pure as mathematical functions (could be calculated by pure substitution with pen an paper), referential transparency is preserved.
What is going on inside these values is beyond the abstraction barrier and literally cannot be seen by pure Haskell code (the code only declare what to do with these values, including pattern matching on value’s structure).
These are the same notions (of an impenetrable abstraction barrier), and it is not “abstract” or imaginary. Everything is real.
>>=
The (>>=) function (called bind) which does re-binding and sequencing should be considered impure, because it (and only it) has access to the internal state and actual representations.
However, the code that uses (>>=) is as pure as math or logic.
The type system guarantees that no code could “see” or access any value behind the monad interface (abstraction barrier). (>>=) and only (>>=) can access and pattern-match (but not “see”, because Haskell is declarative.).
This is exactly how Haskell code is pure (as math or logic) in the presence of IO, State transformation and side-effects. All these are beyond the abstraction barrier with the Monad type-class establishes, and the type system enforses.
Each instance of a Monad (an actual type) hides all its “stuff” behind a standardized interface, which separates the pure code from impure.
And that is really it. No more, no less.
Passing the Whole World
This metaphor is both philosophically funny and useful. In an abstract theory, a function is indeed pure if it takes a snapshot of the whole universe and returns a value together with the whole universe modified by itself.
In reality this is a form of an explicit serialization (via nesting of
calls and explicit passing of a value - nested lets
, which are
semantically equivalent to nested lambdas) which is
required for sequencing of IO
actions, since Haskell is a
call-by-need language.
Nested function calls and implicit passing of a values inside a Monadic context (behind the abstraction barrier) is the most common idiom.
Nesting of expressions is the natural way to establish an evaluation order in a language with lazy semantics.
Passing of “RealWorld” values ensures “threading”- than no two IO contexts ever overlap.
IO
is a type synonym defined in the following way:
type IO a = RealWorld -> (a, RealWorld)
an ADT
Technically, it is an ADT as defined by Barbara Liskov. There is an interface, which is what the pure code “sees” and uses. There is an actual implementation, based on some particular representation (actual data structure) hidden behind the interface, exactly as intended.
All the most fundamental concepts of programming are there in play.
Actual representation
At the lowest level there is an impure code which manipulates data in memory, like everything else.
The purity ends when the main function of a Haskell program returns a pure expression (to be eventually evaluated by the runtime), which is a type-checked specialized state machine, defined in pure logic.
State Monad
It is just a lambda which returns a pair of values. Lambda, so that they can be
composed sequentially (yes, just nested lambdas) by >>=
.
The two values of a pair are at the different sides of an
abstraction barrier. The State
cannot be seen or accessed outside
of the Monad.
The lambda (which is called a State Transformer) captures the value, and it is lifted into this particular Monadic context “forever”.
The actual State
type and how exactly the values of that type are
actually handled is encapsulated inside a particular instance of a
Monad
type-class and it does not pollute the pure code.
Moreover, the code is still declarative and will be evaluated eventually, so it is literally a pure logical expression which declares what is to be done with Monadic values. Referential transparency still holds.