Yield in for comprehension should have higher precedence than method invocation


#1

What is the output of:

for {
    s <- Option("some")
} yield {
  "thing"
}.foreach(println)

It is:

t
h
i
n
g

I guess the reason is that { "thing" }.foreach(println) is evaluated first before it is used as the yielded value.

I propose that the yield “operator” should have higher precedence than method invocation.


#2

If this was true, it would lead to some counter-intuitive behaviour in code like this:

def f(x: Option[List[Int]]): Option[Int] = for {
  lst <- x
} yield lst.sum

I want to interpret it as ... yield (lst.sum) but what you suggest is to interpret it as (... yield lst).sum. I think, your suggestion is generally bad.


#3

The result you’re getting is actually what I would expect. This looks like the foreach is part of the yield to me, and that’s what is happening.

If I’m understanding you correctly, you want the foreach to be based on the result of the entire for comprehension. I would expect that to be written something like:

(for {
    s <- Option("some")
} yield "thing").foreach(println)

#4

Enclosing for comprehesion in parentheses is what I do from time to time and I also agree on the lst.sum example. However, everytime I have to enclose a for comprehension with a yielded value that is enclosed in curly braces, I think that this should not be necessary.

I think that enclosing the yielded value in curly braces could make a difference, but I also understand that this might baffle others.


#5

Well, then you are somewhat understandable since we have another precedence in case of usual method, not a yield keyword:

def f(x: Option[List[Int]]): Int =
  {
    // whatever computation
    x
  } map { lst =>
    // whatever computation
    lst.sum
  }.getOrElse(0)

Here expression after the dot (i.e. getOrElse in this case) after the second curly braces applies not to the expression inside the curly braces but to the outer expression. If yield was instead of map (assuming appropriate beginning of the method), precedence is reverse. Maybe, this is what messes you up.


#6

Thank you for elaborating the situation. This is exactly what feels irregular to me.

Yield should behave like a method with a single by name argument. Another example is:

Try { 1 }.toString

This expression returns a String and not a Try[String].


#7

People also ask for match to behave syntactically like a method invocation.


#8

Of course, that behavior is easily explained by the fact that Scala accepts methods with a single argument in infix notation. To Scala, your code here is seen as the following after being desugared.

def f(x: Option[List[Int]]): Int =
  {
    // whatever computation
    x
  }.map{ lst =>
    // whatever computation
    lst.sum
  }.getOrElse(0)

Now that precedence makes total sense. I don’t see the argument for yield to behave similarly since it isn’t a method. Unlike match, you can only “apply” a yield to a for loop.


#9

In some regard yield accepts a single by name argument.


#10

But if yield were a method you wouldn’t have access to the names defined inside of the for block


#11

It could be syntactically a method.

Just because we’re used to for/yield having its own weird rules doesn’t mean that there isn’t a simpler way to view it.


#12

You could view it as a method call with magical properties that defy normal scoping rules… I’m not sure what that would buy you.


#13

Syntactic regularity and ease of writing in a fluent style.

It’s already profoundly weird. This wouldn’t be any weirder. To be less weird it would have to be something like

for {
  x <- opX
  y <- foo(x)
  yield
  bar(x, y)
}

Those are normal-ish scoping rules. for already breaks them.


#14

Hmm well, method or no method, it would indeed be more regular to interpret for {...} yield {...}.foo as(for {...} yield {...}).foo and for {...} yield x.foo as for {...} yield {x.foo}. I must admit it’s irritated me before.

Edit:
Well actually, a bit to my surprise, yield does work more or less as could be expected: https://scastie.scala-lang.org/BKKtBnmORzeLgqCc9L9Qnw
It’s because yield is always infix that it behaves that way. So if you would be able to call it with a dot like a regular method that would indeed help. But it would look weird at the same time.


#15

Normal for what? Pretty much every modern language that has a for loop allows it to declare one or more variables that appear in the body of the loop. I would say that is normal behavior in the case of a for loop. It is what people expect. Yes, it is irregular in terms of standard method call syntax, but things that are language keywords often are.

Personally, I wish that yield wasn’t required and the compiler could tell if the loop was being used as an expression or a statement and generate the appropriate code based on that. As it is, including yield when you don’t use the result won’t make code incorrect, it just potentially wastes resources.


#16

Normal for Scala.

Most “normal” languages have if/else as a statement, not an expression. But it works better with a FP approach to have it an expression, and also is more regular (in that everything is an expression, even if the expression returns Unit because there’s nothing better it could do).


#17

The comparison to if else is interesting.

scala> val x: Int = if (true) 3 else 4.toString
<console>:11: error: type mismatch;
 found   : String
 required: Int

Like for yield, if you want the method to apply to the whole thing, you have to assign it to a value or wrap it in parens. Changing for yield to not require that would be comfortable for some situations, but inconsistent with other parts of the language. I suspect this is one of those cases where no matter which way the language goes, people will complain either way.


#18

To regularize if/else with method syntax, you would allow this:
if(true){ 3 }.else{ 4 }.toString


#19

I would note that anything descended from C acknowledges the usefulness a conditional expression with the ternary operator. All Scala did was make the simplification that a normal if is an expression so they don’t need a ternary operator.


#20

I would argue that interpreting yield, else, and match as method calls would make the language more regular and improve method chaining abilities.