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 =.
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.
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).
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 .
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.
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
}
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, ?]
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.
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.