Scala has the trait Iterable[A]
that defines
def flatMap[B](f: (A) ⇒ GenTraversableOnce[B]): Iterable[B]
That certainly looks like the bind function on a monad, and the documentation hints that it is a monad, but there are two objections, one minor and one major:
GenTraversableOnce
. I think this is just a convenience that can be overlooked when judging monad-ness.Do these problems break the monad-ness of the collection?
bind (or flatMap ) and unit (the constructor) are all it takes to be considered a monad.
Monads are simply a way to wrapping things and provide methods to do operations on the wrapped stuff without unwrapping it. For example, you can create a type to wrap another one, in Haskell: data Wrapped a = Wrap a. To wrap stuff we define return :: a -> Wrapped a return x = Wrap x.
What the hell are monads you ask? The formal wikipedia definition says: “In functional programming, a monad is an abstraction that allows structuring programs generically. Supporting languages may use monads to abstract away boilerplate code needed by the program logic.
Monad is not a class or a trait; monad is a concept. Every “wrapper” that provides us with our two beloved operations, unit, and flatMap, is essentially a monad (well, it's not really enough to just provide methods with those names, they, of course, have to follow certain laws, but we'll get to that).
The "major" concern is easier to answer: no, it doesn't, because that's not what it means. A monad is not required to have any particular "value" or none, only to compose with functions in particular ways.
For the "minor" one, you're right to be concerned about the types. Properly, a monad is a monoid (with some additional constraints), meaning it's a set with certain operations. The elements of this set are, as far as I can tell, things of type A => M[B]
(in scalaz this type is called Kleisli
); flatMap
is the |+|
operation of the monoid.
Does the set of all possible A => Iterable[B]
in Scala form a monoid with respect to this operation (and a suitable choice of identity)? No, very much not, because there are plenty of possible A => Iterable[B]
that violate the monad laws. For a trivial example, {a: A => throw new RuntimeException()}
. A more serious example is that e.g. if a Set
is present in a flatMap
chain, this can break associativity: suppose we have:
f: String => Iterable[String] = {s => List(s)}
g: String => Iterable[String] = {s => Set(s)}
h: String => Iterable[String] = {s => List("hi", "hi")}
Then
((f |+| g) |+| h).apply("hi") = List("hi") flatMap h = List("hi", "hi")
but
(f |+| (g |+| h)).apply("hi") = List("hi") flatMap {s => Set("hi")} = List("hi")
which is upsetting, because the whole point of a monoid is that we can write f |+| g |+| h
and not worry about which way we evaluate it. Going back to monads, the point is that we should be able to write
for {
a <- f("hi")
b <- g(a)
c <- h(b)
} yield c
and not worry about which order the flatMap
s are composed in. But for the f
, g
and h
from above, which answer do you expect the above code to give? (I know the answer, but it's quite surprising). With a true monad, the question wouldn't come up except as a scala compiler implementation detail, because the answer would be the same either way.
On the other hand, does a particular subset of possible A => M[B]
, e.g. "the set of all A => List[B]
implemented under the scalazzi safe subset of scala", form a monad with respect to that definition of flatMap
? Yes (at least for the commonly accepted definition of when two scala functions are equal). And there are several subsets for which this applies. But I think it's not entirely true to say that scala Iterable
s in general form a monad under flatMap
.
The answer to your headline question is maybe. A collection with flatMap
is not sufficient to be a monad, but it might be a monad if it satisfies some further conditions.
Your "minor" issue certainly breaks the monadicity (the proper word for "monad-ness") of Iterable
. This is because many subtypes of Iterable
and GenTraversableOnce
are not monads. Therefore, Iterable
is not a monad.
Your "major" issue is not a problem at all. For example, the function argument to the List
monad's flatMap
receives the elements of the List
one at a time. Each element of the list generates a whole list of results, and those lists are all concatenated together at the end.
Fortunately, judging whether something is a monad is really easy! We just have to know the precise definition of monad.
F[_]
that takes one type argument. For example, F
could be List
, Function0
, Option
, etc.A
and produces a value of type F[A]
.A => F[B]
, and a function of type B => F[C]
and produces a composite function of type A => F[C]
.(There are other ways of stating this, but I find this formulation straightforward to explain)
Consider these for Iterable
. It definitely takes one type argument. It has a unit of sorts in the function Iterable(_)
. And while its flatMap
operation doesn't strictly conform, we could certainly write:
def unit[A](a: A): Iterable[A] = Iterable(a)
def compose[A,B,C](f: A => Iterable[B],
g: B => Iterable[C]): A => Iterable[C] =
a => f(a).flatMap(g)
But this does not make it a monad, since a monad additionally has to satisfy certain laws:
compose(compose(f, g), h)
= compose(f, compose(g, h))
compose(unit, f)
= f
= compose(f, unit)
An easy way to break these laws, as lmm has already pointed out, is to mix Set
and List
as the Iterable
in these expressions.
While a type construction with just flatMap
(and not unit
), is not a monad, it may form what's called a Kleisli semigroupoid. The requirements are the same as for monad, except without the unit
operation and without the identity law.
(A note on terminology: A monad forms a Kleisli category, and a semigroupoid is a category without identities.)
Scala's for-comprehensions technically have even fewer requirements than semigroupoids (just map
and flatMap
operations obeying no laws). But using them with things that are not at least semigroupoids has very strange and surprising effects. For example, it means that you can't inline definitions in a for-comprehension. If you had
val p = for {
x <- foo
y <- bar
} yield x + y
And the definition of foo
were
val foo = for {
a <- baz
b <- qux
} yield a * b
Unless the associativity law holds, we cannot rely on being able to rewrite this as:
val p = for {
a <- baz
b <- qux
y <- bar
} yield a * b + y
Not being able to do this kind of substitution is extremely counterintuitive. So most of the time when we work with for-comprehensions we assume that we're working in a monad (likely even if we're not aware of this), or at least a Kleisli semigroupoid.
But note that this kind of substitution does not work in general for Iterable
:
scala> val bar: Iterable[Int] = List(1,2,3)
bar: Iterable[Int] = List(1, 2, 3)
scala> val baz: Iterable[Int] = Set(1,2,3)
baz: Iterable[Int] = Set(1, 2, 3)
scala> val qux: Iterable[Int] = List(1,1)
qux: Iterable[Int] = List(1, 1)
scala> val foo = for {
| x <- bar
| y <- baz
| } yield x * y
foo: Iterable[Int] = List(1, 2, 3, 2, 4, 6, 3, 6, 9)
scala> for {
| x <- foo
| y <- qux
| } yield x + y
res0: Iterable[Int] = List(2, 2, 3, 3, 4, 4, 3, 3, 5, 5, 7, 7, 4, 4, 7, 7, 10, 10)
scala> for {
| x <- bar
| y <- baz
| z <- qux
| } yield x * y + z
res1: Iterable[Int] = List(2, 3, 4, 3, 5, 7, 4, 7, 10)
For more on monads in Scala, including what it all means and why we should care, I encourage you to have a look at chapter 11 of my book.
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