do vs for 

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.

  • Scala doesn’t have a built-in Monad type. Instead, the compiler desugars for comprehensions into map, withFilter, flatMap, and foreach calls mechanically. SLS 6.19
  • For things like Option and List that the standard library implements map/flatMap, the built-in implementations would be prioritized over the typeclasses provided by Cats.
  • The Scala collection library’s 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)

Implementing actM 

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.