Feedback sought: Optional Braces

Note as of latest 2.13 and 3 releases, parens are required on single param lambdas – i.e., m { (a: A) => ... }

Overall I think it’s a reasonable syntax. Historically, we can look at F# and Haskell to see that when given a choice, with both brace-delimited and indent-delimited syntaxes are available, people generally end up picking the indent-delimited version. Python’s success also speaks for itself; beginners certainly don’t pick Python because of performance, ease of installation, packaging, IDE support, or simplicity of the language’s runtime semantics!

I’m generally in favor of this indent-delimited Scala, as long as:

  1. The exact syntax has time to be bikeshedded over and argued about, so we don’t get stuck long term with a sub-optimal or warty design simply for lack of discussion

  2. We find a solution to avoiding braces when calling higher-order functions with multi-line lambdas. Higher-order functions are common enough that we cannot claim to have brace-free Scala unless they’re supported, and it’s generally accepted that indent-delimited blocks and multi-line lambdas are a challenging pair of requirements to satisfy https://stackoverflow.com/questions/1233448/no-multiline-lambda-in-python-why-not

  3. We have a best-effort backwards-compatibility plan and migration plan. As Scala 3 is a big release, I expect upgrading to be painful, but hopefully indent-delimited blocks do not make it more so

I’ve a ton of experience using indent-delimited syntax: I’ve used Python regularly for my entire career, I spent years working in Coffeescript, and have done some work in F#. But as much as I like indentation-syntax, there is definitely ways you can do indent-delimited syntax badly:

  1. Coffeescript ends up making too many delimiters optional (parens and commas, in addition to braces and curlies) resulting in code that is challenging to skim.

  2. Coffeescript also commonly used two space indents, which visually make distinguishing blocks much harder than code using four space indents, more so because of the lack of explicit delimiters

  3. F# has a pretty unusual/irregular syntax overall, so despite being indent-delimited I find it much less pleasant to read than curly-heavy C# or Scala

Thus it’s not sufficient to do indentation syntax in Scala, but we also have to do it right.

Looking at the linked ExtractSemanticDB.scala file, I have the following concrete feedback:

  1. I still find the two-space indentation makes it very hard to distinguish blocks when skimming. Normally I don’t discuss formatting concerns, but given that this whole effort is about improving Scala’s surface syntax, I think this is important. After spending years reading 2-space-indented Coffeescript, I don’t think this response to 2-space indents is going to change for me

  2. The code snippet has a distinct lack of multi-line lambdas being passed to higher-order functions. I understand that those would be the “worst case” scenario for the whitespace rules as currently specced and implemented

  3. The (inconsistently applied?) indentation of case blocks within a match statement seems extraneous. With curlies it looks a bit more reasonable, but without curlies it seems a lot more awkward to have two levels of indentation where an if/else if/else chain would only have one

Given that we’ll be stuck with this syntax for the long haul we need to make sure it’s the best syntax we can come up with. While the current set of rules that are specced out is very reasonable, I do think that arguing out these last few concerns (and any other concrete concerns that people bring up) is definitely worth doing before the spec and implementation are set in stone for the next decade.

19 Likes

I think it’s normal for developers who are already familiar with Scala 2 to be skeptical of optional braces, in much the same way that developers like myself were skeptical of optional semicolons.

But as Kotlin and Java move closer to Scala 2.x, I think optional braces really helps differentiate Scala 3 (like a fine coat of paint on a new automobile), and, moreover, represents a doubling-down on “Scala being Scala”. Optional braces is more of what we came to Scala for: concision and beauty, without duplication and boilerplate.

I myself am teaching Scala 3 without braces, and I think the braceless syntax will eventually win over the majority of Scala 2.x developers. This despite the fact that braceless syntax has an obvious drawback (copy/pasting code doesn’t work without adjusting indentation). Yet I personally find this drawback is compensated by the benefit (layout reflects semantics, without duplication!), and the more minimal look and feel really plays to the historical strengths of the Scala language.

As Python has demonstrated, whitespace significance is, at the very least, not an impediment to adoption, even among beginning programmers, despite the very real drawback it has with copy/paste.

So for all this reason, I’m in favor of optional braces, and encourage developers to give the syntax a spin, because, as I personally discovered long ago with optional semicolons, sometimes that’s all it takes to acquire a taste for a new way of writing code.

14 Likes

I mostly don’t (yet) care for the new syntax, but I am delighted to have it available to me for those cases where it makes code cleaner, visually more pleasant, and/or easier to understand.

Having these kinds of syntactic options available is one of the main reasons why I find Scala code appealing to read and write. Other examples include

  • Functions need not have braces when they are a simple expression
  • for's map/flatMap block can be in parens instead of braces
  • Method names can be symbolic or alphabetic
  • Methods can be called infix as well as dotted (Um…?)
  • Function return types are optional
  • Semicolons are optional
  • = is optional in purely side-effecting functions instead of requiring a return of Unit (Uh…)
  • return is optional
  • Results of simple expressions can be used without being named (e.g. 2 * 3 + 5 is okay, don’t need { val a = 2 * 3; a + 5 })

I am delighted to add optional braces to this list (and saddened to lose the ones we’re losing).

I do not like alternative syntax when the two options clash or where the alternative admits dangerous behavior (e.g. def foo(x: X) = println(x) which suggests that foo might return something worthwhile).

But for optional braces it’s almost entirely upside with very little chance for confusion once one has a modest amount of familiarity with the syntax. I can envision using it quite a bit in contexts where the block is very short, and the brace-to-content ratio is therefore currently very poor.

4 Likes

Thank you for the acknowledgement of the process issues this feature has experienced, for a while it felt a lot like gaslighting, and was a deeply disorienting and unsettling experience, and it’s helpful to clear the air.

My experience with this feature has been that, the more I used it, the less I liked it. This, admittedly, may be partially due to my background, as I’ve worked in Python and Ruby prior to Scala, and this feature runs headlong into some of the least pleasant parts of the syntax of both those languages.

I don’t consider this a complete feature. This may be due to coming in before the rebranding to “optional braces”, as when I was experimenting with this it was treating it as “significant whitespace” and the concerns I encountered while evaluating it under those criteria have stuck, mostly because I haven’t really seen anything that would resolve them.

When I was experimenting with this, it hit the sweet spot of covering enough cases to look good in most of the sample code, while repeatedly running into situations where the whitespace wasn’t sufficient. I found this really detrimental to the legibility of the code. The problem is that, at least for me, it’s much harder to see what isn’t there, so blocks delimited only by indentation tended to bleed together. This was worsened when some of the blocks had braces and some didn’t, because when looking for boundaries the braces made the whitespace differences even harder to spot.

Refactoring was unpleasant, as was having to manage indentation. These are familiar problems for me from working in Python, and tooling hasn’t really improved much over “select the region and hit tab until it lines up”. Compared to “paste and hit Ctrl-Opt-L” (note: that’s my scalafmt hotkey), moving code around was much slower. Given the popularity of Python, if tooling improvements haven’t materialized by now, Scala adopting significant whitespace is unlikely to change that. Bottom line is that this moves indentation and formatting from “stuff I don’t have to think about” to “stuff I have to actively manage”, which is not generally the direction of progress.

Similar to Ruby, even when it became possible to skip the braces for function calls with the appropriate compiler flag, the code is deeply ugly when multiple parameter blocks or even method chaining come into play. Both tend to come up in my code fairly often, which is a problem for me, due to the issues intermixing the styles I mentioned previously. As an aside, I don’t have the same issues with the cases where curly braces are optional now, because they only apply when the block would contain a single expression, and those tend to be visually distinct.

Given this irregularities, I don’t see how adding this is consistent with changes like removing infix flexibility. It adds way more variability and irregularity than infix syntax, and BDFL or not, I can’t shake the impression that this is getting in because it’s new and shiny, rather than on its own merits.

Call it out as an experiment and put it behind a feature or language import, if it’s really as compelling as you think, it’ll get adopted. If not, that sets up an easy way for people to try it out, and time to figure out how to fix the places it currently underperforms. I’d be behind that 100%, because the places where this really shines for me is code that doesn’t have the characteristics mentioned above: embedded DSLs, and it would be handy to be able to opt-in on a file-by-file basis (block by block would be better, but that’s probably an unreasonable ask).

3 Likes

As long as the lambda-containing thing is last, all you need is to double-down on infix syntax rather than abandon it:

xs map s =>
  val n = s.length
  if n < 5 then s * n
  else s.toUpperCase

and

xs foldLeft(0) (acc, x) =>
    acc + x

The problem comes when you have multiple parameter blocks or a lambda that is part of a parameter block, because you’re mixing styles: delimited parens, but non-delimited blocks. That’s already true even with stuff like multi-line if/else

def foo(p: Int => Boolean, q: Int => Boolean) = ???

foo(
  i =>
    if (i < 189571) bippy(i/2)
    else whatchamacallit(i, i + 1) < 893475,
 j =>
    if (lovely(j)) checkIfFlower(j)
    else false
)

This is about the best one can do and there’s still an awkwardness in finding that comma that separates the two.

Anyway, I just don’t buy that multi-line lambdas are hard at all. => can just be an end-of-line block opener. The problem is that the other syntax doesn’t support them. (You end up surrounding things with parens instead of braces, and the parens were already optional with braces, so you accomplish precisely nothing.)

P.S. Want chaining? No worries, just triple-down on infix syntax:

xs
  map s =>
    val x = foo(s)
    bar(x, s, quux(x))
  filter y =>
    val z = baz(y)
    isScrupulated(z, bippy(y, z))

Maybe we don’t want to write things that way because braces or parens make things more clear, but hey, that’s why braces are optional. Sometimes they make things more clear.

2 Likes

Note–the parsing rules for this are a bit hairy for both machines and people. You have to consider that it might be xs.foldLeft(0)(acc, x) in addition to allowing for infix syntax of a method plus all-but-the-last parameter block. It might be better to introduce a new argument-supplying operator for clarity. Something like

xs foldLeft(0) @ (acc, x) =>
  multiple
  lines

Or

xs foldLeft(0) $ (acc, x) =>
  multiple
  lines
2 Likes

Scala being “differentiated” or “Scala being something” shouldn’t be what the decision is based on. It should be what the developers favors and what boosts productivity and maintainability, etc.

While it doesn’t stop newcomers to learn Python, it does stop many people from even looking at at all. The huge benefit of using Python is due to its AI and scientific libraries and easy (almost naive) syntax, and that made people to make compromise on the indentation syntax and by no means it means all Python developers like it.

10 Likes

Or:

xs foldLeft(0) { (acc, x) =>
multiple
lines

Making } optional is an extremely bad idea. It would be confusing for all users and all tooling while bringing no benefit at all.

8 Likes

Thanks for the feedback!

  • Two space vs more: Three spaces looks nice, four is arguably already too much. But it will be very hard to get the community to shift. I believe the syntax will work for two spaces even if might not be optimal.
  • Multi-line lambdas: Will be worked out next, I hope. It could come in 3.1 or 3.2.
  • Match layout: Note that you can write
     s match
     case A => 
       ...
     case B => 
       ...
     case C => 
       ...
     somethingElse
    
    The syntax allows it. Most people are used to the double indentation however.
2 Likes

Regarding the multiline lambdas. It is already possible to write them without braces or awkward :

xs.map(
  s =>
    val n = s.length
    if n < 5 then s * n
    else s.toUpperCase
)

For some reason xs.map( s => on one line and a single indentation level doesn’t work yet, but I don’t see a fundamental reason why it shouldn’t.

4 Likes

No thank you. I like my delimiters matched.

What is the point of this?! Just

xs.map{ s =>
  val n = s.length
  if n < 5 then s * n
  else s.toUpperCase
}

The only benefit is if you don’t need that closing whatever. That the whatever happens to be a paren instead of a brace is immaterial.

3 Likes

The point was that you can just pass a multiline lambda argument between parens like you pass all other kinds of arguments. So braces are not obligated for multiline lambdas. I personally don’t see the harm of writing it like you just did, but most of the complaints are about this very subject.

I feel far less religiously for this than other people. As I’ve already said in the other thread, optional braces makes sense for a lot of reason, one of which is productivity.

I’ve been embracing the brace-free syntax for about a year now (though I haven’t been coding Scala 3 constantly during that time). While some tooling has sometimes been awkward with the new syntax, I don’t think that’s an issue in the long term.

For exclusively writing code, the new syntax doesn’t bother me at all.

My concern is mostly that I find the current syntax hard to read. The : sort of floats around on the screen for me. I imagine this would suck really bad for someone with dyslexia. I’m also really wondering what the new syntax would mean for blind programmers.

I don’t think it’s serious to close the door on optional braces, but I really don’t see why this is something which must be done now. Shipping Scala 3 with optional braces on by default means it can never be rolled back or changed if it’s discovered to be sub optimal for any reason. The significance of this change should not be underestimated!

This is why I think the best solution by far would be to make it an experimental feature in 3.0 and make it official when it’s matured more.

I tried to expand on this reasoning in the angry sister of this thread:

Other reservations about the syntax: (click to expand)

This seems like a half measure to me. Other languages that’s made this change prints warnings when tab characters are found, for good reason.

end markers solve part of the issue but they are not perfect:

3 Likes

I would’ve preferred the syntax to be more similar to that of F# or Haskell. This is why I promoted where in the other thread. This is also a good argument in favor of waiting with this change. Doing so would leave the door open for alternative syntax. Deciding now makes it very hard to consider alternatives. With all respect for that @odersky and others put a lot of time and energy into this, no harm can come from letting people play around with this more before setting it in stone.

1 Like

What I’d like is for end to be loosely checked, maybe I have an

if something:
  //...
end if

Seeing end if is of close to none help, what would be better is end if my condition, eg everything after a space (or delimiter) is ignored, so we can have end C class and end C object

I think it very depends on the way that someone write code. I will not mention dsl or something similar. In general when I am working with sql I often need to use copy\paste\editor’s macros and python syntax makes work more difficult. When I switch to a new\old framework I prefer to copy snippets from stackoverflow and refactor it.
I wonder how often does a usual developer write code from scratch and how often does a usual developer copy\paste\refactor code from google or a similar project. May be it is one of the reason why someone does not like python while others do like. I am sure in one thing this question open the holywar which will never end. At least I do not see the end in python.

For what it’s worth, I end up writing from scratch quite a lot - which generally means multiple revisions and more moving things around as things solidify than when I start refactoring from existing code (which is generally more stable), so the ability to quickly move a block of code from point A to point B is central to my workflow.

3 Likes

I don’t support the prefernce towards indention based code blocks.

  1. What about the tool/formatting support. How is a tool like Intelij to determine what is intended to be. a block and which ones aren’t.
  2. “Being Python friendly”- Why is this a goal for Scala? Python and Scala have a lot of differences that would turn off an experienced Python coder. They also have different uses. Python has a lot of problems that come with the whitespace intentions (Ever mix up tabs vs spaces, or not having enough/too-few spaces… you’re in trouble)
  3. (More of 2.1) How valid are Python developers who make contributions to Scala if they have to have this kind of crutch? (They’re also likely to use python isms where it wouldn’t be appropate in scala)
  4. Copying and pasting blocks is an issue as that the taget has a different scope
  5. This seems only friendly towards teaching new members to the language rather than experienced users who dig through quite a bit of code.
  6. For the suggestion of having start and ending tags. This is a heck of a lot more verbose than single characters to denote the start and end of the blocks. if/fi & while done is quite a pain, also it adds more noise to the code rather than stating intention.
  7. I was never a fan of the if statement for ternary in scala. However would this indention force those statements to be 3 lines instead of 1?
  8. This encourages mixed bracket and indention code bases. That’s no good for maintainability. If your dependences use bracketless and your code uses brackets, that’s a lot of context switching.
  9. Showing code on html based pages. If you’re not using pre the intendentation may not show up. (Multiple scapes are condensed) With brackets tools are able to fix the formatting, with spaces the copy and pasted bit is not able to be reconstructed.
  10. What is the preferred amount of spaces per indentation? Some people say 3, some say 4, some say 5 (tabs), etc. Having a mix there is inconsistent and prevents sharing of code. Now you have to address the person’s code that is sharing it with you to fix that syntax error. (It moves this from a style issue to a syntax error)

Another goal I wonder about this: Is this trying to make code longer? There’s a lot of harm when you get 10+ lines of code in a function. For languages that can do a lot in a single statement this is just asking for buggy code.

Where I support bracketless usage: single statements. That’s common in C++ & Java

4 Likes