Make "fewerBraces" available outside snapshot releases

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.

I started using the fewerBraces syntax and so far it is really nice. The only place where it wasn’t great was with def suite(label: String)(specs: Spec*) (ie varargs). Obviously this didn’t work:

suite("foo"):
  test("asdf"):
    assert(...)
  test("zxcv"):
    assert(...)

Not sure if there is some possible clean way to do that.

2 Likes

This topic seems to be the best one about feedback for fewerBraces, so I’m posting here.

For the sake of evaluating the implicit proposal behind that flag, I have experimented with it in one of my personal projects. As a stylistic exercise, I have tried to remove every single brace in my 8 kLoC codebase. I did manage to do so, although I am clearly not sure whether the price was worth it. Also, as part of my experiment, I made a point of not moving things around in vals, or replacing braces by parens (unless I absolutely had to, see below).

I made a series of changes by category, gathered in a series of commits. The codebase as a whole is private, so I won’t share it. But you will find the diffs of each commit in the following gist:

Without further ado, here is my feedback, as a commentary on each of those commits. I am not discussing the readability of the resulting code in that list. I leave that as a more subjective discussion towards the end of this post.

1. Use fewerBraces in places where it works as intented out-of-the-box

In the first commit, I applied fewerBraces to all the places in my codebase where it was clearly the intended use case of the feature, and where it did not require me to do any other change (including formatting changes). In a sense, this is where things went well.

2. Reformat call chains to admit more uses of fewerBraces down the line

This commit is somewhat unrelated to fewerBraces. It is purely a reformatting of places where my call chain coding style prevented to use fewerBraces. So I reformatted those call chains to make them amenable to the fewerBraces changes down the line.

It’s worth observing that these changes introduced more lines, and more indentation levels. This is a problem I already had when adapting my coding style from the general brace-based syntax of Scala 2 to the indentation-based syntax of Scala 3.

3. Use fewerBraces where it works after the previous reformatting

This is like the commit 1., with new spots enabled by the reformatting of commit 2.

4. Where I had to avoid using an _ as lambda param name

This commit exhibits two instances of a bug involving _. I filed it here:

5. Where I had to use a colon because of a partial function argument

This commit shows a bunch of places where I pass an anonymous partial function as argument. In those cases, I had to use a :, although I think we could get away without that. I filed that as a feature request here:

6. Where the placement of commas goes very wrong

This commit shows several places where leveraging fewerBraces leads to argument-separating commas to be displaced to very misleading places. The typical shape is like this before:

foo(
  xs.map { x =>
    x * x
  },
  otherArgument
)

which becomes

foo(
  xs.map x =>
    x * x, // <-- misleading comma here
  otherArgument
)

This can also happen with trailing commas.

This is not something I have observed with any other language feature before, including the general indentation syntax of Scala 3. I must say that I find it quite concerning.

7. Where I was forced to use new parens because of curried params

This commit shows the only place where I could not avoid introducing new parens to compensate for the braces. This happens because the lambda argument is followed by another parameter list.

This can be quite common because of methods like groupMapReduce, which takes 3 different functions as arguments.

(There exists another way, which involves putting a : alone on a line, but I found that way too horrible to even consider.)

8. Where it goes very wrong because the lambda is an argument of an infix method

Finally, the last commit shows some places where a lambda is an argument to an infix method—a common idiom in Laminar, for example. I have filed another feature request to make this better:

My subjective opinion on the resulting readability

Disclaimer: I was very much against the introduction of the indentation-based syntax in Scala 3. I still think it was a mistake. Not because of the resulting syntax (I use it in that codebase, and it’s fine) but because the change of syntax would inevitably introduce more variability in Scala code styles.

For fewerBraces, I’m not so much against the change. TBH, I think we went so much over the line with the general indentation syntax that a few more changes with fewerBraces won’t make a difference.

Subjectively though, I find the resulting code simply harder to read. While it may be an artifact of my experimental methodology, I strongly feel that fewerBraces is there to get rid of braces for the sake of getting rid of braces—and not to actually improve the developer experience or the readability of the code.

Compare for example

    val perCompanyOrNeutral: Set[AssetType] = Company.AllCompaniesAndNeutral
      .flatMap { company =>
        companyDamAssetNames(company)
          .map { (level, assetName) =>
            AssetType.Image(assetName, AssetPath(s"assets/dam$level-${company.stringID}.png"))
          }.toSet
      }.toSet

with

    val perCompanyOrNeutral: Set[AssetType] = Company.AllCompaniesAndNeutral
      .flatMap company =>
        companyDamAssetNames(company)
          .map (level, assetName) =>
            AssetType.Image(assetName, AssetPath(s"assets/dam$level-${company.stringID}.png"))
          .toSet
      .toSet

The braces in the first snippet give some visual structure, and help associate the various method calls that belong together. In the second snippet, I have to make quite a bit of extra effort to associate the method calls where they belong.

A counter-argument would be that I should put the result of the other flatMap in a separate val first, then call .toSet on that. I don’t accept that argument, though, as it makes the structure worse, and essentially forces me to write even more code, with less structure, to recover the readability loss of fewerBraces.


The misleading commas are also very problematic to me in terms of readability. It becomes quite a lot harder to make a mental structure of the code at a glance when those commas are misplaced.


I also don’t like that this syntax effectively makes multi-function methods like groupMapReduce second-class citizens of the language. There is no good alternative for those. It’s really the language changes telling you that you should never even have been using such methods! And that despite the fact that they are in the standard library.

Summary

As a summary, I would say that I am not at all convinced by this feature. I will repeat my main impression of the feature here: to me, it feels that fewerBraces is there to get rid of braces for the sake of getting rid of braces—and not to actually improve the developer experience or the readability of the code.

13 Likes

What if (and I’m not saying it’s possible) the fewerBraces syntax supported an optional end [applyName] in these situations, and you can configure scalafmt to add those automatically, according to the number of lines in the lambda block?
So the code will look like (if we define number of lines as min 1) :

    val perCompanyOrNeutral: Set[AssetType] = Company.AllCompaniesAndNeutral
      .flatMap company =>
        companyDamAssetNames(company)
          .map (level, assetName) =>
            AssetType.Image(assetName, AssetPath(s"assets/dam$level-${company.stringID}.png"))
          end map
          .toSet
      end flatMap
      .toSet

In my code base I currently use indentation syntax only with the combination of end placed automatically by scalafmt for large blocks for readability sake. Without that option I would never have used it. So maybe we need to have that same possibility for the fewerBraces option.

1 Like

Personally I find

end map
.toSet

extremely jarring aesthetically. It’s easy to read and understand; it just makes me recoil in horror because end map is such a clear stop in the flow, that getting going again smoothly with .toSet just feels wrong.

Not to mention that the normal syntactic rule that .whatever can be rewritten appended to the previous line now fails: end map.toSet even if permitted ought to be forbidden for how incredibly misleading it is. (Because of the usually high precedence of ., it looks like you’re ending map.toSet.)

8 Likes

Yeah, I also doubt I will ever use fewerBraces in chaining.
My personal interest in it is when I just have a def foo(block : => Unit), so to write

foo: 
  bla
  bla 
  bla

is better for me in this particular situation.

1 Like

A good DSL example can be found in Chisel
Instead of this:

switch (myState) {
  is (state1) {
    // some logic here that runs when myState === state1
  }
  is (state2) {
    // some logic here that runs when myState === state2
  }
}

Do this:

switch (myState):
  is (state1):
    // some logic here that runs when myState === state1
  is (state2):
    // some logic here that runs when myState === state2
5 Likes

I can’t help but find the braces version considerably easier to visually parse, even compared to a version with end markers.

    val perCompanyOrNeutral: Set[AssetType] = Company.AllCompaniesAndNeutral
      .flatMap { company =>
        companyDamAssetNames(company)
          .map { (level, assetName) =>
            AssetType.Image(assetName, AssetPath(s"assets/dam$level-${company.stringID}.png"))
          }
          .toSet
      }
      .toSet

Sure, the version without braces is 2 lines shorter, but shoving more content into fewer lines of code isn’t always a benefit. If it’s more expressive or abstracted, it can be a win - but if it’s just shoved together it rapidly turns into a featureless wall of text.

I’ll go further and posit that the indentation syntax as a whole is kind of the same situation, and developer experience or code readability were very much secondary concerns. There are fundamental flaws with it that wider availability of “fewerBraces” isn’t going to paper over.

2 Likes

I actually think the second version is perfectly fine. One can add braces if one wants to but I don’t think I would miss them. I agree the version with end markers would be a non-starter, though.

What I like about the second version is the regularity in the sense that the “.” always align in the same way. Single line chaining:

abc
  .reverse
  .toSet

Multi-line chaining:

abc
  .map x => 
     ...
  .toSet
5 Likes

TBH I think part of the reason this looks bad is the small 2-space indentation. 4-space indents make the grouping and scopes very clear (at the cost of pushing things to the right):

val perCompanyOrNeutral: Set[AssetType] = Company.AllCompaniesAndNeutral
    .flatMap company =>
        companyDamAssetNames(company)
            .map (level, assetName) =>
                AssetType.Image(assetName, AssetPath(s"assets/dam$level-${company.stringID}.png"))
            .toSet
    .toSet

I don’t think this 2 vs 4 space thing is unique to Scala. I’ve worked with Python and Coffeescript and FSharp codebases in the past, and uniformly those that were 2-space indented were hard to parse visually, while the same code after reformatting to four spaces OR adding braces/parens things looked much clearer. And saying it’s “up to the user” is a non-answer: users follow leadership, and if prominent Scala 3 projects such as the compiler the compiler are using two-space indents, everyone else will as well.

For whitespace syntax as a whole, I’m against using it for the same reason as Sebastien (among other reasons). But I have the opposite conclusion: I think we need to go all-in with fewerBraces if we want to make it usable at all. Consider this random piece of Scala 3 code “in the wild”:

In one 20-line snippet, we can find all three demarcation styles: parens, curlies, and end markers, all variously nested and mixed together. This is literally the worst-of-all-worlds, taking the syntax of Java/Python/Ruby and combining them into one big jumble. Sure it could be prettied up some, but the reality is that code in the wild will be a bit messy, and there’s no reason to believe the code I work with day-to-day will look much better than this. The various Scala 3 contributions to my com-lihaoyi projects have a similar hodge-podge of styles.

The “people can use indentation, or braces, or end-markers, whatever they like” approach, while nominally provided flexibility, is a curse that destroys all consistency in a codebase. If we want the benefits of Python-like syntax and simplicity, this isn’t it. While simplifying migration is nice, it cannot come at the expense of migrating to something worse than the status quo.

The way we get out of this mess, assuming we don’t want to abandon indentation syntax entirely, is to go all in. If we want Python-like syntax, we should go for Python-like syntax: only indentation, 4 space indents, no mandatory braces, no other end markers. Braces would be as common as semicolons are today: used once in a blue moon, but actively discouraged and never necessary.

There will be some issues. Some can be solvable:

  • The single-line colon : for calling multi-block functions like groupMapReduce can be given a nicer syntax (I have proposed do in the past, conveniently free since the dropping of do-while loops, but we can come up with some other keyword/marker).
  • The increased indentation could be partially compensated by other techniques, e.g. top-level methods, indent-less match/case statements, there are probably other clever things we could do here

Some may be unsolvable:

  • Passing multi-line lambdas to functions followed by a comma will always look a bit weird.
  • The method-chaining with indent-delimited blocks looks a bit unusual, but with larger indents I think we can get used to (having used that exact syntax in Coffeescript before myself)

But that’s the only way I see that we can do a forward-fix to make Scala’s indentation syntax nice. Otherwise I can’t see myself using or recommending people to use whitespace or end-delimited blocks - the result is just too messy.

If the codebase uses a formatter, I don’t see how that is the case. Across codebases, sure.

Time to rename Scala 3 to Jambalaya.

Since I tend to add end markers, I wondered if that is my end marker, and git blame says yes.

So I can attest that this is not a high quality for a style test, as it shows me learning Scala 3 during the pandemic. The closing brace is “legacy”, as it is not usual to format the entire file when making an edit.

On the Dotty project, I have encountered the pushback that “this edit is not minimal”. So even my sense of a good compromise does not meet everyone’s standard.

I have recently come around to arranging my dots vertically instead of horizontally in “fluent” style. I previously thought of vertical as Java style because it’s the verbosity that requires it. I think I switched because of using less infix (although leading infix improves vertical infix), and now I would be willing to follow anyone’s lead in using indents of 3 or 4. Probably 3 just to remain different from Java. Oh, and 3 because it’s Scala 3.

2 Likes

“Mess” is also a food word for melange or mixture. Or actually, my high school Webster’s says it is just a course (of a meal), with the same root as message. It doesn’t explain why it means opposites: laying out a meal is an arrangement, and getting everyone to table is perhaps as large a task of assembly. The mess is the remains afterward.

One meaning is “enough” food: “a mess of peas” is a measure of what suffices for supper. In one of Joyce’s Dubliners stories, a fellow pops in and orders a plate of peas. That sounded so strange to me, to order peas. It doesn’t say if he ate them with a fork or spoon, or a knife coated in honey. I guess it would say if it were a honey-coated knife.

The dictionary entry directs one to “smite” and then “smegma”. TIL “smegma”. I would be comfortable saying Smegma 3 for Scala 3.

The food word I was thinking of is “hash”. That word means “ax”. I think of a “hash” always as a “re-hash”. “A restatement of something that is already known.” A hash code is such a restatement. As food, I assume a hash is something leftover, chopped up and added to the pan with some primary foodstuff. Fried potato at breakfast is a hash brown.

As a bonus word, the closure in the car port is called a hasp. I don’t know if I knew exactly what a hasp was. But if I were to come up with a hybrid of Haskell and Lisp, it would definitely be Hasp.

Edit: So my point is that I see it as a mess only in the sense of “a mess of peas.”

1 Like

That example needs some context to be understood properly. In dotty we made the decision NOT to reformat globally to indentation in order to keep meaningful git history. But every new or changed piece of code uses indentation style. For people used to the codebase that’s actually an advantage since it gives you a sort of archeological layering of code :wink: . Anyway, I would not treat that as typical.

Generally feedback on indentation was very positive so far, so there’s no way we would go back to braces only.

The issue here is that what you described sounds very typical. Basically all my OSS codr, and all the proprietary code I work on, we have a lot of old code and want to preserve VCS history. I imagine that is the same for all the Scala projects in existence today. What then?

As we’ve seen from @som-snytt, best practices around “minimal diffs” contribute to the mixing, as even working in a file for months you won’t have a consistent syntax. And lampfl/dotty is not special: we have similar “minimal diffs” conventions in all my com-lihaoyi projects, as well as my company’s codebases. Far from being special, what you describe about Dotty sounds like the most typical codebase imaginable.

I don’t mind if we need to put some work to have a smooth migration, but what I’m seeing here is “(a) ruin your git blame, (b) give up your PR best practices, or (c) live with a hodge-podge of syntaxes” as the current migration proposals. I’m sure you can see that is rather offputting and a disincentive to migrate.

What I proposed above is to do aggressive migrations to a single new syntax. That avoids (b) and (c), and we can use Git’s --ignore-rev to mitigate (a). Together with fewerBraces, that will give us files with a nice python-like syntax. And as community leaders, it is on us to come up with recommendations, so people can smoothly migrate without re-inventing half-baked migration strategies themselves.

Anyway, I’m here because we have a few million lines of Scala we may have to migrate at some point, and I want to talk about options and possibilities. I don’t want to re-litigate old decisions. I just hope others are open to engage with me on these topics as well

4 Likes

Yeah this didn’t exist when we made the decision to avoid a bulk rewrite of dotty, but nowadays it is even supported by github (Ignore commits in the blame view (Beta) - The GitHub Blog), so that decision could be revisited.

2 Likes

I was against that for indentation in general, and I am still against that. This essentially proposes trading vertical space (braces) for horizontal space (more indentation). The latter resource is the scarcest, so this is entirely the wrong trade-off!

You can get that regularity with braces as well, if that’s really the important thing:

abc
  .map { x =>
    ...
  }
  .toSet

It does use one more line, though.

While I agree with your point about indentation versus braces, IMO presenting end markers as a third option is misleading. end markers are a part of the indentation-based syntax. They provide a visible ending to larger pieces of indentation-based blocks. They even provide some added value compared to braces, in having a compiler-checked repetition of the name of the thing that is being closed. They are extremely helpful in an indentation-based codebase that exceeds one screen of vertical space.

5 Likes