Recently I stumbled upon some code that shouldn't have worked but did. This is a simplified illustration:
val l=List(1,2,3,4,5)
def isEven(i:Int):Option[Int] = if (i%2==0) Some(i) else None
for {elem <- l
even <- isEven(elem)
} yield even
That will produce List(2,4)
and might be no surprise to Scala developers.
But it shouldn't work given that List is monadic and should be of this form:
class M[A] {
def flatMap[B](f:A => M[B]):M[B]
def map[B](f:A => B):M[B]
}
Following this definition, every element of a chain of monadic operations must belong to the same monad. Map transforms elements within the context of the monad and flatMap
must 'trap' free elements into the monad.
In the example above, we have this desugarized representation:
l.flatMap(elem => isEven(elem))
which is of type: List[Int].flatMap[Int](f:Int => Option[Int])
and does not conform to the monadic definition.
The correct form of isEven
should have been:
def isEven(i:Int):List[Int] = if (i%2==0) List(i) else Nil
Looking into the scala-docs, it turns out that the reason that List
and Option
can be combined in a for comprehension in Scala, is that the flatMap
on List
is defined as flatMap[B](f: (A) ⇒ GenTraversableOnce[B]): List[B]
and that allows for any traversable instance to be 'flatmapped' over. Option
is traversable, Set
, Seq
, and many others are too.
I'm left wondering: What are the consequences of this broader definition of flatMap
.
Are there cases that one should be aware/beware of?