There are subtle differences in Haskell’s do
notation and Scala’s for
syntax. Here’s an example of do
notation:
foo = do
x <- Just 3
y <- Just "!"
Just (show x ++ y)
Typically one would write return (show x ++ y)
, but I wrote out Just
, so it’s clear that the last line is a monadic value. On the other hand, Scala would look as follows:
def foo = for {
x <- Some(3)
y <- Some("!")
} yield x.toString + y
Looks similar, but there are some differences.
Monad
type. Instead, the compiler desugars for
comprehensions into map
, withFilter
, flatMap
, and foreach
calls mechanically. SLS 6.19
Option
and List
that the standard library implements map
/flatMap
, the built-in implementations would be prioritized over the typeclasses provided by Cats.
map
etc accepts CanBuildFrom
, which may convert F[A]
into G[B]
. See The Architecture of Scala Collections
CanBuildFrom
may convert some G[A]
into F[B]
.
yield
with a pure value is required, otherwise for
turns into Unit
.
Here are some demonstration of these points:
import collection.immutable.BitSet
val bits = BitSet(1, 2, 3)
// bits: BitSet = BitSet(1, 2, 3)
for {
x <- bits
} yield x.toFloat
// res0: collection.immutable.SortedSet[Float] = TreeSet(1.0F, 2.0F, 3.0F)
for {
i <- List(1, 2, 3)
j <- Some(1)
} yield i + j
// res1: List[Int] = List(2, 3, 4)
for {
i <- Map(1 -> 2)
j <- Some(3)
} yield j
// res2: collection.immutable.Iterable[Int] = List(3)
There are several DSLs around in Scala that transforms imperative-looking code into monadic or applicative function calls using macros:
Covering full array of Scala syntax in the macro is hard work,
but by copy-pasting code from Async and Effectful I put together
a toy macro that supports only simple expressions and val
s.
I’ll omit the details, but the key function is this:
def transform(group: BindGroup, isPure: Boolean): Tree =
group match {
case (binds, tree) =>
binds match {
case Nil =>
if (isPure) q"""$monadInstance.pure($tree)"""
else tree
case (name, unwrappedFrom) :: xs =>
val innerTree = transform((xs, tree), isPure)
val param = ValDef(Modifiers(Flag.PARAM), name, TypeTree(), EmptyTree)
q"""$monadInstance.flatMap($unwrappedFrom) { $param => $innerTree }"""
}
}
Here’s how we can use actM
:
import cats._, cats.syntax.all._
import example.MonadSyntax._
actM[Option, String] {
val x = 3.some.next
val y = "!".some.next
x.toString + y
}
// res3: Option[String] = Some(value = "3!")
fa.next
expands to a Monad[F].flatMap(fa)()
call.
So the above code expands into:
Monad[Option].flatMap[String, String]({
val fa0: Option[Int] = 3.some
Monad[Option].flatMap[Int, String](fa0) { (arg0: Int) => {
val next0: Int = arg0
val x: Int = next0
val fa1: Option[String] = "!".some
Monad[Option].flatMap[String, String](fa1)((arg1: String) => {
val next1: String = arg1
val y: String = next1
Monad[Option].pure[String](x.toString + y)
})
}}
}) { (arg2: String) => Monad[Option].pure[String](arg2) }
// res4: Option[String] = Some(value = "3!")
Let’s see if this can prevent auto conversion from Option
to List
.
{
actM[List, Int] {
val i = List(1, 2, 3).next
val j = 1.some.next
i + j
}
}
// error: Option[String] does not take parameters
// Monad[Option].flatMap[String, String]({
// ^
The error message is a bit rough, but we were able to catch this at compile-time.
This will also work for any monads including Future
.
val x = {
import scala.concurrent.{ExecutionContext, Future}
import ExecutionContext.Implicits.global
actM[Future, Int] {
val i = Future { 1 }.next
val j = Future { 2 }.next
i + j
}
}
// x: concurrent.Future[Int] = Future(Success(3))
x.value
// res6: Option[util.Try[Int]] = None
This macro is incomplete toy code, but it demonstrates potential usefulness for having something like this.