mikey.bike . archive . other writings . lists . about
At work, someone long ago turned on the NoNeedForMonad
wart remover
for our Scala projects. I started bumping up against it recently, had
trouble parsing exactly what the “wart” was, and decided to look into
it.
Imagine this somewhat contrived example. We receive Input
from some
unreliable source; two numbers that may or may not be present.
final case class Input(
: Option[Int],
a: Option[Int],
b)
Let’s say we want to sum the two numbers in Input
— if both are
present, return a Some
, otherwise return None
. Here’s a “naive” way to write this:
object Input {
def sum(input: Input): Option[Int] = {
.a match {
inputcase Some(a) =>
.b match {
inputcase Some(b) => Some(a + b)
case _ => None
}
case _ => None
}
}
}
val input1 = Input(Some(1), Some(2))
val input2 = Input(None, Some(2))
.sum(input1)
Input// #=> Some(3)
.sum(input2)
Input// #=> None
That works, but it’s hard to read with lots of nested match
statements. We can clean this up with a for
comprehension:
object Input {
def sum2(input: Input): Option[Int] = {
for {
<- input.a
a <- input.b
b } yield a + b
}
}
Boom! The NoNeedForMonad
wart is triggered and complains with the
following:
No need for Monad here (Applicative should suffice).
> “If the extra power provided by Monad isn’t needed, it’s usually a good idea to use Applicative instead.”
Typeclassopedia (http://www.haskell.org/haskellwiki/Typeclassopedia)
Apart from a cleaner code, using Applicatives instead of Monads can in general case result in a more parallel code.
For more context, please refer to the aforementioned Typeclassopedia, http://comonad.com/reader/2012/abstracting-with-applicatives/, or http://www.serpentine.com/blog/2008/02/06/the-basics-of-applicative-functors-put-to-practical-work/
Let’s step through this.
The first head-scratcher is that there is no Monad
concept anywhere
in the Scala standard library; nor do we use a library that defines
one, such as scalaz
. Where exactly is the monad?
This is explained by the fact that flatMap
is a monadic bind
operation. If you understand flatMap
, you already know what a monad
is: a monad is something that can flatMap
.
Where are we using flatMap
? That comes from the for
comprehension,
which can be understood as syntatic sugar for using flatMap
and
map
here. The “desugared” version of sum2
would look something
like this:
object Input {
def sum3(input: Input): Option[Int] = {
.a.flatMap(a =>
input.b.map(b =>
input+ b
a )
)
}
}
This desugared code will also trigger NoNeedForMonad
. Really the
error is saying, “No need for flatMap
here.”
The second head-scratcher is that the solution to not needing monads
is Applicative
, which is also not in the standard library!
My best understanding of Applicative
is that it provides a more
powerful map
. Instead of just applying a function that takes one
argument to a context such as Option
(e.g., 1.some.map(_ + 2)
),
you can apply a function to many arguments, all of them in a context
such as Option
.
For instance, the function +
takes two Int
values, and returns an
Int
. Using the method lift2
from scalaz
’s Apply
class (a
superclass of Applicative
), +
can be transformed into a function
that takes two Option[Int]
values and returns an Option[Int]
.
import scalaz.Apply
import scalaz.Scalaz._
object Input {
def sum4(input: Input): Option[Int] = {
val sum = (a: Int, b: Int) => a + b
[Option].lift2(sum)(input.a, input.b)
Apply}
}
Here, Apply[Option].lift2(sum)
lifts the sum
function to accept
and return Option
values; we then simply pass input.a
and
input.b
to that function.
“Applicative should suffice” — if you don’t mind pulling in scalaz
and
are willing to deal with some rather awkward functions for anything
more complex than our example here.
The final head-scratcher is that if you try following any of the links
in the NoNeedForMonad
error text, you are taken to several posts —
not one, not two, but three — all about using the Applicative
typeclass in Haskell.
The comonad link in particular is absolutely full of category theory and GHC language extensions.
For more context, go learn you a Haskell!
You might be wondering, when do you actually need monad flatMap?
We can make small tweak to the sum
function that will no longer
trigger the wart:
object Input {
def sum5(input: Input): Option[Int] = {
for {
<- input.a
a <- input.b.map(_ + a)
bPlusA } yield bPlusA
}
}
Now the value bPlusA
, within the for
expression, depends on the
value of a
; previously, the values a
and b
were separate and did
not reference each other, and were only used together in the yield
.
I think using NoNeedForMonad
makes sense under two conditions: a)
the team is familiar with the concepts of Monad and Applicative,
and b) the project uses scalaz
or some library that provides these
abstractions.
Otherwise, it pushes you to make awkward tweaks to the for
comprehension, such that it is deemed to need flatMap
, or else you
have no abstraction to use and have to fall back to nested match
statements.
This seems like a wart meant for Haskell projects. In Haskell,
Applicative is part of the standard library, and curried functions in
particular make it easy to use. Here’s the same “add two optional
numbers” example in Haskell, using fmap
(<$>
) and apply
(<*>
):
+) <$> Just 1 <*> Just 2
(-- #=> Just 3
That works entirely with functions from Prelude
, no imports or
libraries needed.