Simplifying expressions on RHS of assignment sugar

Recently a beginner on reddit asked a question about why `a += if (true) 1 else 2` didn’t work. We know it’s because that desugars to `a = a + if (true) 1 else 2`, and `if` isn’t a simple expression therefore it isn’t a valid RHS for `+`.

But it got me thinking, why don’t we desugar into `x += y` into `x = x.+(y)` instead? This means that `y` no longer needs to be a simple expression.

I think this disproportionately affects new users since they tend to be both using `var` and not knowing how to solve this issue.

So I propose that desugaring `+=` would wrap the RHS in parens if there are no parens already there. I believe this should be backwards compatible with existing code.

7 Likes

`a += b` can also refer to a method `+=` on `a`. It is only replaced by `a = a + b` if no method `+=` exists for `a`. Maybe we don’t want the syntax rules for a method `+=` to be different than for method `+`.

On the other hand, this is allowed:

``````a = if (true) 1 else 2
``````

In this case, `=` can’t be a method. On the other hand, if we had

``````a.x = if (true) 1 else 2
``````

Then this would refer to a method `x_=` if one exists. Looks like you could always argue there is an inconsistency somewhere.

To fix it, you can always use parens:

``````a += (if (true) 1 else 2)
``````

That may look a bit funny if the expression goes multi-line, but in that case, the parens also make it more easy to see what is going on.

You could also use brackets, but then someone might come and benevolently dictate that you should replace your brackets with something strange.

I think this is my biggest problem with it personally. New users don’t know this, and don’t have a clear way to find out.

I’d refine my proposal to say “any method that is sugared to look like an assignment operator should convert the RHS to a simple expression before passing it as a parameter to the method”. `x_=` is a good example showing the compiler can and does do this.

I run into this all the time in sbt builds, where I’m constantly wanting to write `a := if ...` or `a ++= if ...` but can’t without parens or braces. And `:=` and `++=` aren’t syntax, they are just method calls. I doubt this is worth addressing unless the fix works for infix method calls in general.

So for example, could `a + if (true) 1 else 2` be made legal? And if not, why not?

3 Likes

If it was legal to write

``````1 + if(true) 1 else 2
``````

then I guess then it would also be legal to write

``````1 + if(true) 1 else 1 + if(true) 1 else 1 + if(true) 1 else 2
``````
1 Like

Do we have to change the language or just improve (special-case) the error message? If the error message said what to do, that would solve what Nathaniel said that users don’t have a clear way to find out what to do.

2 Likes

But that expression is okay (presuming that it is well defined).

Most languages allow expressions with a mix of operators without requiring brackets even if it would be hard for a human to easily see the precedence. The expectation that the author should put brackets in when it is not obvious.

Forcing brackets around these cases often seems a bit ugly in Scala.

I agree with OP and Seth, it’s very surprising behaviour, and I run into it frequently; it’s annoying having to go back and putting the parentheses around the `if` expression. I don’t know what is technically the best solution, but from user perspective it’s not optimal currently.

1 Like

I think there are two (three?) ideas emerging here

1. the original proposal; only for desugaring things that look like assignment operators, wrap the RHS in parens
2. regarding @SethTisue’s issue; make `if` a simple expression
3. simplest way to address beginner frustration; modify “illegal start of simple expression” messages to say something about wrapping the expression in parens
1 Like

The main problem with bumping the precedence of `if` would be that it would lead to asymmetric situations.

``````x + if a then b else c
``````

would not longer be the same as

``````if a then b else c + x
``````

That’s also surprising, to say the least.

That desugaring happens after parsing but the parser is the one which rejects that expression. It seems that the parser could be changed to simply accept this without changing anything to the desugaring, for example the following already works as I expect:

``````scala> var x = 2
var x: Int = 2

scala> x *= 1 + 1

scala> x
val res1: Int = 4
``````

So `x *= 1 + 1` is (thankfully) equivalent to `x = x * (1 + 1)` and not `x = x * 1 + 1`.

1 Like

I’m not sure I understand.

When I look at those two, I’d expect them to mean

``````x + (if a then b else c)
``````

and

``````if a then b else (c + x)
``````

But in practice this

doesn’t compile, and this

works as I’d expect

1 Like

Well that sounds like it address my original proposal with a bow on it

Given that `+=` doesn’t have the precedence of `+` and does have the precedence of `=`, I think it’s a lot more natural to expect `+=` it to behave like `=` than like `+` with regards to `if`.

This works:

``````var a = List(1, 2) take 1
a ++= List(3, 4, 5) take 2
``````

This doesn’t:

``````var a = if (true) List(1, 2) else List(2, 1)
a ++= if(false) List(3, 4, 5) else List(5, 4, 3)
``````

And you can’t claim that it’s because `x = y` doesn’t return a value because it does! It just returns `()`. As you can verify with

``````val b = a = if (true) List(1) else List(0)
``````

So there is no justification for `if` not working with the `?=` family.

4 Likes

They could not, not sensibly, because the precedence of `+` is higher.

But the precedence of `=` matches the precedence of `+=`.

We don’t have anything to generalize except that `=` is a special snowflake with its own precedence class, and it shouldn’t be. `=` might just be a call to `update`…it might be a method too. So it’s really no different.

This works:

``````scala> val m = collection.mutable.TreeMap.empty[String, String]
m: scala.collection.mutable.TreeMap[String,String] = TreeMap()

scala> m("fish") = if (true) "salmon" else "herring"
``````

But this `=` is just a method call.

5 Likes

Related tweet

Compilers should avoid ever printing “unexpected token”-level vague syntax errors

Alternatives:
- Function “foo” needs a closing bracket
- Return statements can’t be in expression positions
- Did you mean “switch”?

Treat syntax errors like you’re teaching a new developer

— Jamie Kyle (@buildsghost) February 12, 2021
2 Likes