Make "fewerBraces" available outside snapshot releases

I like the new significant whitespace syntax, however, it feels awful having to reach for curly braces e.g. when using call by name values as the “body” of a function call like munit tests:

test("basic") {
  assert(true)
}

I would love to instead write:

test("basic"):
  assert(true)

As I understand it, this is implemented under the experimental flag

import language.experimental.fewerBraces

however, it seems to be unavailable in the RCs. I suggest making this feature generally available.

14 Likes

Same here. I actually have a small code-base in this style, and the summary removal of -Yident-colons in RC releases is a real pain. Not saying that we should expect -Y flags to get any sort of stable support, but not having this option does make the indentation-based syntax feel incomplete and frustrating.

4 Likes

Experimental features now mean that they are only available in unofficial releases (nightly/snapshot) to discourage their use thinking that they are here to stay when it might not be the case. So you’re essentially requesting to make this feature not experimental anymore.

6 Likes

Yes. Please make it official.

10 Likes

Maybe it’s getting used to, but it’s really unclear to me that assert(true) is a parameter rather than some sort of body when it’s introduced with :. I intuitively expect : to separate a name and a definition.

Please keep that experimental until we have some more experience with that.

5 Likes

How is it different in the curly brace version? It also looks like some sort of body. Intentionally so.

2 Likes

I’ve tried -Yindent-colons in a project and generally liked it. However, there are some problems:

  1. The current implementation has bugs. For example, look at this issue. The following compiles with braces:
@main def runTest(): Unit =
  val arr = Array(1,2,3)
  if
    arr.isEmpty
    || {
      val first = arr(0)
      first != 1
    }
  then println("invalid arr")
  else println("valid arr")

But not with colon:

@main def runTest(): Unit =
  val arr = Array(1,2,3)
  if
    arr.isEmpty
    || :
      val first = arr(0)
      first != 1
  then println("invalid arr")
  else println("valid arr")

  1. Lambda syntax is more confusing. When there is a parameter, no lines are saved vs braces. And in fact an indentation level is lost:
io.map { value =>
  println(Thread.currentThread().getName)
  value
}

vs

io.map:
  value =>
    println(Thread.currentThread().getName)
    value

Of course in this case, it is best to use parens. In Scala 3, you can omit the braces for a multi-line body:

io.map(value =>
  println(Thread.currentThread().getName)
  value
)
2 Likes

The new fewerBraces rules are a bit different from what was implemented under -Yindent-colons. In particular, you can now write map like this:

io.map value =>
  println(Thread.currentThread().getName)
  value

I also just fixed the bug you noticed: Recognize leading infix operator in front of `:` at eol by odersky · Pull Request #12219 · lampepfl/dotty · GitHub

11 Likes

:+1:

Scala has always had great support for “custom control flow” operators, making it possible for library authors to create very clean DSLs such as this testing DSL.

In my view, it would be a big win for Scala developers to enjoy optional braces syntax on custom control flow operators—out of the box and without any special compiler flags.

9 Likes

Looking at this documentation page, is there anything we can do about

xs.map x => x + 1   // error: braces or parentheses are required

being illegal? Because it would mean that adding or removing a few characters can result in a new-line, which in turn leads to adding and removing parentheses. IMO it is exactly the kind of ‘bailing wire’ the new proposal tries to get rid off.

4 Likes

More generally, I don’t quite understand the rationale behind disabling experimental feature flags but for nightly/snapshot versions. I have a DSL project, hence my users basically use the compiler itself, through the scripting engine/REPL. I expect them to use release versions. It basically bars me using experimental features, for instance generic number literals, which is unfortunate for a math DSL, as it is.

The rational is that it is experimental and might go away. Supposedly, the lesson from Scala 2 is that experimental things that aren’t accepted tend to hinder progress because they were too easy to use, but were not accepted in the long run.

One the one hand, I don’t know if that is a good concept, because this also hinders experimentation as well. I think what caused too much trouble is that things stayed experimental too long (e.g., Scala 2 macros). I think a better approach would have been to allow everything under a flag in releases, but limit the experimentation period to up to two minor revisions or 18 months.

On the other hand, the Scala team can be more flexible in the context of nightly/snapshot and allow PRs of experimental stuff that were not yet discussed/accepted by the SIP committee.

5 Likes

Though this approach has it’s own dangers (gestures vaguely in the direction of the optional braces mess)

I don’t understand how it relates to the optional braces which were never experimental in the compiler. An example where this experimental concept would have been good is the typelevel scala fork @milessabin had to maintain. Maintaining a fork is a huge headache, and I would imaging if the experimental nightly concept was available back then, maybe there wouldn’t have been a need for TLS and SIP23 (literal types), AnyKind, and other things would have been just experimental flags. @milessabin, any thoughts?

1 Like

And what about chaining map calls? With parens you can easily

xs.map(x => x + 1).map(x => x + 2)

And when a one-line lambda becomes multi-line (and vice versa), it is easy to refactor with parens, but probably not the braceless version.

xs.map(x => x + 1)

// just add some line breaks
xs.map(x =>
  someThing()
  x + 1
)

// vs deleting parens/braces, adding space, adding linebreaks.
xs.map x =>
  someThing()
  x + 1

The other reason I sometimes use braces is because you can separate statements with ; (semicolon), so instead of

test("basic"):
  assert(a == true)
  assert(b == true)

You could choose to write

test("basic") { assert(a == true); assert(b == true) }

I made a context function-based DOM library and it’s nice to save space with one-liners:

div{color("purple"); t"Hello $user"}

With all that said I really like the existing braceless support and support the goal of making braces even less needed.

2 Likes

It turns out that is it extremely difficult to come up with robust rules for things like

xs.map x => x + 1

and it’s a nightmare to make these backwards compatible. One idea might be to detect the x => sequence and then parse everything to the end of the line. But then what about this code?

xs.map x => {
  ...
}

Or this one?

xs.map x => x match
  case ...
  case ...

So, at present this looks like a lot more risky than the other rules we have and we might never find a good solution. The rest of fewerBraces is solid; that’s why it’s better to decouple the two questions.

5 Likes

I see, thanks for the answer.

In any case, I hope you can figure something out, or that tooling can help avoid doing the adding/removing of parentheses manually. Otherwise I am afraid this is not a clear win over parentheses and might get adopted to various degrees in the wild.

1 Like

I think we still want to keep the convention that parentheses should be used on a single lines. The aim is to make braces that enclose multi-line blocks optional. And the proposal as is achieves that.

4 Likes

But why? This means tedious going back and forth between styles just like in Scala 2 single expression methods:

def doSomething() = 
  gotoBla(foo + bar)

// vs
def doSomething() = {
  val foorBar = foo + bar
  gotoBla(foobar)
}

Now this is neatly solved in Scala 3. However fewerBracess introduces the exact same problem again, but for lambdas.

1 Like

For what it’s worth, I’ve found the ability to eyeball code and tell if a method or lambda is an expression or not at a glance to be a feature, not a bug.