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 vals.
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.