Flat Map
The flatMap view of the world
In Scala they think intuitively in terms of flaMap
and map
(this is what for-syntax)
desurars into, instead in terms of arrows and composition.
In other words, they just emphasize the Functor aspect over the arrow aspect.
They thus define things operationally, in terms of how it is done (nested calls
to fmap
), instead of what it is for - composition of transformations (arrows).
the flatMap
is just a wrong concept. It messes up the fundamental concepts and
induces inappropriate, or plain wrong intuitions.
The essence of what we traditionally call map
is to access and/or modify a value
within (“inside of”) a given context (an Abstract Data Type).
There is a generalized abstraction which mathematicians call Functor.
Traditionally, in CS we use a type-class (a set of required signatures) with fmap
, as
an operational definition of what a Functor is - to be an
instance of a Functor an ADT should have fmap
implemented.
flatMap
comes from the other “realm” and is just flatten
(of a nested List -
a list of lists - this is important) after being map
-ped over the inner list.
flatten :: [[a]] -> [a]
concat
is just a better generalization of this
concat :: Foldable t => t [a] -> [a]
and, of course, fmap
is
class Functor f where fmap :: (a -> b) -> f a -> f b ...
But this is not the whole story, not even the most essential part.
The essence is composition of functions (transformations) and the only way to implement composition of pure functions (mathematical or in the context of a programming language expressions) is with nesting. In math we use explicit parenthesis, in CS we use nested lanbda expressions.
This is the fundamental principle - a composition always involve nesting, implicit or explicit. This is as fundamental as biological cells (and partitions within the cells), which “implement” processes.
This is why the do-notation in Haskell desugars to nested lambda expressions,
and the for-syntax in Scala desugars to something similar (but conceptually wrong) - nested
flatMap
s .
Yes, nesting of partially applies fmap
s establishes an explicit order of
transformations (abstracted out by parameterization), but this is not the
essense. The essense is that these whole things can be composed.
The symbol >>=
captures the essence of the whole operation - nesting of lambdas.
The essence has is captured (and shown) in the type - it is a transition from m
a
to m b
, or just m a -> m b
, using a -> m b
, which can be partially applied.
If we hypothetically change the order of arguments of (>>=)
, then
(>>=) :: (a -> m b) -> m a -> m b
looks exactly like fmap
, and a -> m b
is a Kleisli arrow - an one-way lifting transformation.
This, by the way, is exactly why a Monad is an instance of a Functor - it “has” this aspect of forming a context, which means an implicit abstraction barrier - a conceptual membrane.
An abstraction barrier, in turn implies the way to cross it (a pump), and this is what “lifting” and Kleisli arrows (yet another abstract notion of crossing an abstraction barrier) for.
We used to think of a map
in terms of a Lists, but it may be just a single
element “container”, or either empty or a single value (a sum-type), which is how a List can
be viewed (conceptually it has the aspect of being either empty or not).
Whether the type is a sum-type or not has been abstracted away from the notion of mapping over it (and of composing such mappings at a higher level).
When we partially apply fmap
or our hypothetical reordered (>>=)
, what we get is
a function from something a
to the same something of b
, which is just an arrow - a single transformation (a step).
3 different fundamental, but orthogonal notions are packed tightly here (and
could be clearly separated out by partially applying the fmap
(Functor) aspect).
- mapping (reaching to a value “inside”)
- an arrow (a transformation
a -> b
) - nesting at the level of lambdas
- an implicit order (caused by nesting)
- composition of arrows (by nesting)
Abstractly and conceptually, this is not unlike to the emergent properties, which a folded protein - a whole 3D structure (an enzyme) has.
So, being an instance of a Functor here is rather an implementation detail. The essense is an arrow (a transformation) which, can be composed (nested).
The functor aspect here is for reaching behind abstraction barriers, which is a “dirty hack” or “cheating”.
The “proper way” is to declaratively say what is eventyally to be done (by the dirty runtime) behind that abstraction barrier (within a given context).
And we just use fmap
s to do the “dirty” jobs. Again, in the Scala tradion they
emphasize the means (the Functor property) over the ends (composition of
“arrows”) - whole transformations, resulted from partially application, which
abstract out the actual “transformer”.
As one may notice, all the fundamental notions (abstraction by parameterization, abstraction by specification (type-classes), ADTs as actual implementations) are being used it his “locus” which is at the essense of functional programming.
This, of course, is not an arbitrary coincidence.