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.