Pre-SIP: improve for-comprehensions functionality

I am open to more work on for expressions. I believe the highest priority issue here is to resolve #2573. Before that’s finished I would be against throwing more variations into the mix, because that will only make the problem harder.

The task of #2573 in a nutshell is: Design and implement a desugaring of for expressions that replaces withFilter by empty and simplifies the encoding of pattern definitions using =.

2 Likes

I would actually prefer if we can fix the current syntax rather than bloating Scala’s syntax even more. This of course is on the proviso that it doesn’t break that much code.

In any case, I would strongly appreciate if we could look into this more. I have a lot of personal pet peeves with the current for comprehension seeing that it is so central to Scala, the biggest ones I experience is withFilter (i.e. not being able to bind variables to a tuple if withFilter is not defined on the type) and not being able to define implicits in for comprehensions. The performance issues are secondary to the usability issues in my opinion, but others my disagree.

1 Like

Do people actually like using for for Monads?

What if we had something like this?

scala> import cats._, cats.data._, cats.implicits._
import cats._
import cats.data._
import cats.implicits._

scala> import example.MonadSyntax._
import example.MonadSyntax._

scala> actM[Option, String] {
         val x = 3.some.next
         val y = "!".some.next
         x.toString + y
       }
res3: Option[String] = Some(3!)
2 Likes

:+1: for comprehensions will be in a much better position for continued evolution after #2573.

1 Like

At a risk of being somewhat annoying I want to continue syntax bikeshedding.

What about:

for {
  b <- a
  c <- b
  yield <- fn(c)
}

as a sugar for:

a.flatMap { b =>
  b.flatMap { c =>
    fn(c)
  }
}

?
Or the IO code:

def loop: IO[Unit] =
  for {
    _     <- IO(println("Doing periodic things"))
    _     <- IO.sleep(1.second)
    yield <- loop
  }

Well I do although people may argue that is a personal preference. In any case I think the more important thing is that its become the standard way to do syntax sugar for Monad’s (for better or for worse).

Neither monadless nor Dsl.scala took off comparably to bm4, so yeah, they probably do

2 Likes

I think this is a really good suggestion @tarsa! It uses an existing keyword and it suggests what we are trying to do quite well.

OTOH it makes deciding which desugaring to apply more complicated as we now have to consider one of the generators rather than just whether the yield keyword is present.

I’m very interested to hear what others think about this syntax.

It seems like the consensus on the whole is that we should tackle #2573 before everything else.

I guess that means I should have another look at the val-desugaring as I had got to the point where I realised I would have to mess about with scala-reflect’s reversal of the desugaring :weary:.

Is that alright with you @oleg-py? Or would you like to take this forward?

FWIW I haven’t done much more than glanced at the Dotty implementation yet.

I would really encourage you to look at Dotty instead, where the parser does not desugar for comprehensions.

1 Like

I personally quite like using for for monads, I use it often for List and Option for example.

As for

scala> actM[Option, String] {
         val x = 3.some.next
         val y = "!".some.next
         x.toString + y
       }
res3: Option[String] = Some(3!)

I… don’t get it. What are the types of x and y what are the signatures of next and + here?

x is Int, y is String, def next: A given Monad[A], and + is string concatenation.

I think of do notation as a procedural syntax of writing Effect monad expressions, and since we already have a procedural syntax it feels more natural to me to use it.

Some of the reasons I like the for-comprehensions are that I have a mental model for how to desugar them, and they’re concise without being cryptic.

That actM block is unpleasantly verbose, compared to it’s for-comprehension equivalent:

for {
  x <- 3.some
  y <- "!".some
} yield x.toString + y

While I can kind of see how your sample actM block would desugar, it doesn’t look any different from the rest of the code, so there’s no feel for what is or is not allowed, and I’m really not sure how something like this would desugar:

actM[Option, String] {
  val x = 3.some.next
  def render(n: Int, delim: String): Option[String] = 
    if (n == 0) none else List.fill(n)(".").mkString(delim).some
  val y = "-".some.next
  val z = render(x, y).next
  z
}
2 Likes

I think I’m missing something fundamental here, but how does next work, and what happens with None? And how would it work for a different monad, say, List, or Function1[Int, ?]

It executes render monadically.

scala> import cats._, cats.data._, cats.implicits._
import cats._
import cats.data._
import cats.implicits._

scala> import example.MonadSyntax._
import example.MonadSyntax._

scala> :paste
// Entering paste mode (ctrl-D to finish)

actM[Option, String] {
  val x = 3.some.next
  def render(n: Int, delim: String): Option[String] =
    if (n == 0) none else List.fill(n)(".").mkString(delim).some
  val y = "-".some.next
  val z = render(x, y).next
  z
}

// Exiting paste mode, now interpreting.

res0: Option[String] = Some(.-.-.)

That’s not really what I was asking, it’s all well and good that it actually does something which looks reasonable in a contrived case, but what’s the recourse when it’s not behaving as expected?

What’s the desugared code look like? What’s in render’s scope?

When it finds a .next imagine a big flatMap wrapping the rest of the code. So adding None in there would make the whole thing None. I wrote some details here herding cats — do vs for.

scala> val list = actM[List, String] {
     |   val x = List(1, 3, 4).next
     |   val y = List("!", "?").next
     |   x.toString + y
     | }
list: List[String] = List(1!, 1?, 3!, 3?, 4!, 4?)


scala> val fun = actM[Int => ?, Int] {
     |   val x = ((_: Int) + 1).next
     |   val y = ((_: Int) * 2).next
     |   x + y
     | }
fun: Int => Int = cats.instances.Function1Instances$$anon$2$$Lambda$2261/1901137402@4ac8df25

scala> fun(5)
res1: Int = 16

However, it probably makes sense more for effects stuff rather than List.

reading the link, I understand that actM isn’t a method, and the val’s inside it and the next call inside it have special semantics that are different from other val’s and method calls, it seems.

I don’t like that. To me, something like

val x: Int = None.next
x + 3

reads like nonesense. x is not a value, next is not a method, and x + 3 is not an expression, they are radically different things, but they look like they are familiar things. I really don’t like that, and this IMO is the complete opposite of what understandable code is about.

4 Likes

FWIW this is very similar to how F# and Ocaml comprehensions work, e.g. @eed3si9n’s initial example expressed as an F# computation expression would look like this

let opt = maybe {
  let! x = Some(3)
  let! y = Some("!")
  let z = x.ToString + y
  return z
}

Here the difference is that a special keyword is used (let!) to bind the generators and the maybe function returns a builder type that implements the interface that is required by the comprehension syntax. return serves the same purpose as Scala’s yield.
I think that is a lot more comprehensible because the keywords let you know that something magical is happening in the same way that for and <- do in Scala, but it still looks a lot closer to the original syntax of the language.

2 Likes

Love this. Or actM could be named monadic and next as ! (very similar to how Idris does it).

This would simplify a lot of things. For example, for (x <- X, y <- Y) yield x + y could be succinctly expressed as monadic {x.! + y.!}.

This would unify a bunch of already-existed related concepts:

val future = async {
  val f1 = async { ...; true }
  val f2 = async { ...; 42 }
  if (await(f1)) await(f2) else 0
}

Unified syntax under monadic:

val future: Future[Int] = monadic {
  val f1 = monadic { ...; true }
  val f2 = monadic { ...; 42 }
  if (f1.!) f2.! else 0
}
@dom def table: Binding[Table] = {
  <table border="1" cellPadding="5">
    <tbody>
      { for (contact <- data) yield <tr><td>{contact.name.bind}</td></tr> }
    </tbody>
  </table>
}

Unified syntax under monadic:

def table: Binding[Table] = monadic {
    <table border="1" cellPadding="5">
    <tbody>
      { for (contact <- data) yield <tr><td>{contact.name.!}</td></tr> }
    </tbody>
  </table>
}

We could also add functorial, applicative etc. blocks, implemented as macros, (or as language features?).