I think I have a basic grasp of Monads and monadic operations but am still a bit stuck on understanding how the magic features of a monadic type get added to the underlying type (hope this makes sense).
For example, I was reading about how a List[T]
is a monad. But if I flatMap
and map
over some Lists sequentially in a for
comprehension then isn't it really flatMap
and map
that are providing the monadic magic?
If I create a List<String>
then how is the monadic magic added? Or is a List<T>
always a monad in Scala because it just happens to be one of those containers that the language already provides in-built monadic support for?
Lists are a fundamental part of Haskell, and we've used them extensively before getting to this chapter. The novel insight is that the list type is a monad too! As monads, lists are used to model nondeterministic computations which may return an arbitrary number of results.
What is a Monad? A monad is an algebraic structure in category theory, and in Haskell it is used to describe computations as sequences of steps, and to handle side effects such as state and IO. Monads are abstract, and they have many useful concrete instances. Monads provide a way to structure a program.
The simplest monad is the Identity monad, which just annotates plain values and functions to satisfy the monad laws: newtype Id T = T unit(x) = x (x >>= f) = f(x) Identity does actually have valid uses though, such as providing a base case for recursive monad transformers.
One thing I noticed was that Tuple does not have a Monad instance. Which already extremely heavily restricts what we can make the Monad instance be.
You're exactly right that flatMap
and map
are providing the "monadic magic." There is fortunately or unfortunately (depending on how much bad code you've seen) no magic in programming. No amount of abstraction will save you (or someone else) from ultimately writing the code that does the thing you want. Abstraction "just" lets you re-use previously written code and clarify your thoughts around a problem. A monad then is just a concept, an idea, an abstraction, etc.
In the case of Scala this is very literally what the compiler does to a for
comprehension, which becomes a series of flatMap
, map
, withFilter
, and filter
statements.
A monad (in Scala) can be thought of as just a label for the phenomenon where you happen to have a type constructor T[_]
and two functions1
def f0[A](x: T[A], f: X => T[A]): T[A]
def f1[A](x: A): T[A]
By convention, when they see this phenomenon, the Scala community calls f0
flatMap
and usually make it a method so that the x
is always the parent class instead of a separate argument. There is also a convention to call f1
point
or pure
(see scalaz
or cats
). f1
is also usually a method so that it doesn't end up explicitly taking an argument and just uses its parent class as the x
.
Whenever anyone says "such-and-such" is a monad, there is always an implied f0
and f1
which the speaker expects the listener to infer. Strictly speaking "List
is a monad" is a mild abuse of terminology. It's short-hand for List
along with the functions (xs: List[A], f: A => List[A]) => xs.map(f).flatten
(which forms f0
) and (x: A) => List(x)
(which forms f1
) form a monad. Or slightly less obtusely, List
along with the standard flatMap
on lists and the List.apply
constructor form a monad.
Therefore there was never any magic. As part of classifying something as a Monad
you had to have provided a notion of flatMap
and pure
.
There are many ways you could turn this abstraction of a monad into code. The naive way (i.e. Scala with no third-party libraries) is to just agree on a common name for f0
and f1
(e.g. flatMap
) and just name your methods that have the appropriate type signature those names. That is essentially what scalac
expects you to do for for
comprehensions. You could go one step further and try to formalize things with a trait
or an abstract class
. Maybe call it Monad
to be cute and have something like the following:
trait Monad[A] {
def flatMap(f: A => Monad[A]): Monad[A]
def pure(x: A): Monad[A]
}
Then you might call anything that extends this Monad
an implementation of the monad idea (you might imagine something such as class List[A] extends Monad[A]
).
For a variety of practical reasons this turns out to be less than satisfactory and so you end up with the usual solution that looks something like (hand-waving away a lot of other complexity)
trait Monad[F[_]] {
def flatMap[A](f: A => F[A]): F[A]
def pure[A](x: A): F[A]
}
that gets implemented by implicit
s.
Footnotes:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With