Feedback sought: Optional Braces

I hope you didn’t perceive this as a sentiment in support of :, because it is not intended to be a such. I still think that we ought to further explore the design space for optional braces, the current implementation – to me – seems lacking.

1 Like

Can we do a poll of how many people prefer to include optional braces as is, to include only the uncontroversial parts for now, namely blocks which don’t need a new keyword or symbol to make them indentation-sensitive, or to leave braces as in Scala 2 until we come to a consensus?

6 Likes

What is the purpose of such a poll when it was already established here that removing the braceless syntax or putting it behind a flag isn’t going to happen?

3 Likes

This is something that has not been brought up before, and I think it is an interesting proposal worth looking into.

Without looking at the grammar – and I’m not particular familiar with Scala 3 grammar at that – I can easily distinguish between two types of curly braces in Scala: delimiting blocks as one type of expression, and delimiting definitions to associate them with objects, traits, classes, packages and whatever else I might have forgotten.

Since expressions already do not require braces, making significant indentation an alternative way of representing blocks has little impact.

The other category (in think of it as “scope of definitions”) is where most of the issues have arisen. Not simply questions of personal preference, but grammar ambiguity. And it’s not sufficient to have an unambiguous grammar from the point of view of the compiler: it has to be unambiguous to programmers and without mental effort.

Personally, I have no preference on this matter – I have used enough languages with vastly different conventions that this feels insignificant to me. However, being able to clearly tell apart one thing from another matters.

So, personally, I support splitting significant space substitution of curly braces along this line, with blocks considered final and scope of definitions left as experimental. I think this is a new proposal with significant merit.

2 Likes

No, I was including it to show the context for “let’s not consider more options” to show that Martin wasn’t rejecting all options (he’d suggested some!), just that these variations were not judged as so clearly superior to be worth attention. (There was time before to discuss them.)

After having mulled over the question for some weeks, I am still very happy with the outcome we have achieved except for one point: I am getting more doubts that the choice of : over with was the right one. So, forgive me to restart this thread with another exceedingly long post.

To summarize my previous evaluation:

Points in favor of : :

  • Shorter, quieter, and often more pleasing typographically (exception: classes with complex parameter or parent lists, where closing with with works better)
  • Learners seem to prefer it
  • Visually less of a departure from current Scala 2 style

Points in favor of with :

  • The language grammar becomes more regular, and will be easier to explain.
  • The visual appearance also becomes more regular, at the price of more verbosity.
  • We are true to the idea of optional braces (since with can also appear in front of { , but we are
    not suggesting that for : ).
  • no “false friends” wrt to Python, where people expect : for indentation everywhere once they have seen them for classes.

The danger I see is that we let ourselves too much be guided by first impressions. That’s basically what polls measure, after all. The other danger is that we look mostly at simple use cases, where : feels most natural. I tried to do a more detailed analysis of various use cases:

    object foo:
      ...

vs

    object foo with
       ...

Clearly, : is nicer. Same for class, enum, trait.

    class Foo(x: Int):
      ...

vs

    class Foo(x: Int) with
       ...

It’s a toss up, IMO. The : feels a bit too much like a type ascription here. On the other hand, the with is still more bulky.

    class Foo(
        x1: T1
        ...
        xN: TN
     ):
       ...

vs

     class Foo(
        x1: T1
        ...
        xN: TN
     ) with
       ...

with wins here. Like @ichoran said, ): is a sad face.

    class Foo extends Bar:
      ...
    class Foo(x: T) extends Bar(x):
      ...

vs

    class Foo extends Bar with
      ...
    class Foo(x: T) extends Bar(x) with
      ...

I think with wins again, in particular if the extended reference is more complicated than just a single identifier.

But how often do they occur in practice? To find out I did a scan of our community build, which consists of a number of standard libraries and their tests (more than 1M lines total).

    (object|class|trait) foo {

(where : clearly wins as a replacement) had 27416 hits.

    (object|class|trait) foo (...) {

    (object|class|trait) foo [...] {

(where I think it’s a toss-up) had 6270 hits.

    (object|class|trait) foo ... extends ... {

(where I think with works better) had 37400 hits

So in practice there are at least as many cases where with would work better than cases where : works better.

This means that preference of : over with might be mostly due to initial familiarity and concentration on simpler use cases. On the other hand, the consistency arguments in favor of with are indisputable. I also think that the “false friends” problem wrt to Python will be an issue. People who see : after an initial class might expect : to start indentation elsewhere, even though that’s not the case.

The fact is that everywhere else the indentation scheme used in Scala 3 is much more like the one in Haskell than the one in Python. So, it might be better to “stick to our guns” and follow the same syntactic tradition everywhere.

It would mean that seen in isolation, class syntax becomes more bulky and at least initially less familiar to most readers. But in the overall scheme an extra keyword for classes does not really matter. And the consistency arguments feel more important. (and, of course, as always, if you don’t like it you can just stick to braces).

23 Likes

It’d be great to have with instead of : for template bodies. I doubt verbosity would be a problem: it’s only a couple keypresses more than : and {}, it’s not used that often, and the increase in clarity is well worth it. To me, at least, : has always looked a useless little symbol someone tacked on to the end, not an indicator of anything.

But even so, is possible to change it now, this late?

I believe it would mean another milestone cycle with a 6 week delay before we can go into RC1. On the other hand, we might need that time anyway to try out some upcoming changes in inlining.

2 Likes

D’oh, I didn’t realize you were Martin Odersky! I see, that’s good news.

2 Likes

I read class Foo extends Bar with as defclass Foo extends Bar with , so seems great.
,

It seems less comfortable for anonymous classes.

I just want to weight in supporting regularity in general.

I recently played with braceless syntax and got tripped up a few times and immediately went back to just using braces. The idea that sometimes we would use :, sometimes with and sometimes nothing at all, IMO, should be a non-starter.

I also worry about ambiguity in teaching if not strictly in parsing that using : would create (is it type ascription or is it a block).

So, I’m very glad to read this latest iteration.

Finally… I think the fact that this is all still massively in flux should encourage us to pump the brakes on the idea that this should also immediately be the preferred syntax. If it is still significantly changing at this date, what assurance do we have that we won’t discover more serious problems, especially as people use it more. I personally think braceless syntax should be considered an experimental feature still subject to change in 3.1 and 3.2.

20 Likes

:point_up: so much this.

Maybe I’ve missed some motivations in the rest of the thread, but while including this syntax in the first 3.0 is a ship that’s sailed, we’re still on time for making it an experimental feature, which would allow gathering more experience about it and refining it further.

The fact it’s changing so often so close to the final release is empirical evidence that it’s not mature enough to the preferred syntax for Scala (yet!)

10 Likes

Since this thread is active again, I’d like to mention that:

has recently been fixed by https://github.com/lampepfl/dotty/pull/10969 (thanks @odersky!), both syntaxes now work.

If I’m not mistaken, that means the only remaining case where braces are currently required is the partial function literal syntax:

List(1, "a").collect({ case x: Int => x })
List(1, "a").collect { case x: Int => x }

val f: PartialFunction[Any, Int] = {
  case x: Int => 1
  case x: String => String
}

I suggest simply dropping the braces:

List(1, "a").collect(case x: Int => x) // now there's only one way to write this
val f: PartialFunction[Any, Int] =
  case x: Int => 1
  case x: String => String

This accomplishes two things:

  • we can declare victory in the War Against Braces without having to enable -Yindent-colons (or one of the alternatives discussed in this thread, which might or might not get in eventually).
  • There’s now exactly one canonical way to call a non-infix method: a.f(x), this is a big win for consistency (and IMO a reason not to proceed with -Yindent-colons or one of its alternatives, but that’s a debate that can be had post-3.0).
11 Likes

I would be very glad to see the : syntax be abandoned in favor of with.

Last time I wrote anything serious in Python was 8 years ago, and I still get tripped up regularly by the : syntax for indentation in my Scala 3 project. I caught myself writing things like this several times when I was distracted:

if x > y:
  blah...

Though it should be noted that I’ve also been using the experimental -Yindent-colons option for : in expression, which may have contributed to my mistakes. I’d also like to see that syntax change for us to experiment with the replacement sooner than later.

I would really like to have an alternative to braces OR parens to delimit blocks in expressions. Both are a nuisance, and replacing one with the other is not a total victory in my book. This especially sticks out like a sore thumb because we can already get rid of braces for control structures.

I’d like to be able to write code like this:

  case Match(scrut, branches) =>
    val s_ty = typeExpr(scrut)
    val (pats, funs, rets) = branches
      .map:
        case AST(Fun(l, r), c) =>
          given Ctx = ctx.nest
          val l_ty = typePattern(l)
          val r_ty = typeExpr(r)
          (l_ty.payload, AST(Fun(l_ty, r_ty), c), r_ty.payload)
      .unzip3
    ...

or like that:

  var changed = true
  while changed do
    changed = false
    varSubst.mapValuesInPlace:
      (k, v) => v.flatMap:
        tv => varSubst.get(tv) match
          case Some(x) => changed = true; x
          case None    => v

Having to break such beautifully-flowing code with parens on empty lines would really be a shame.

3 Likes

I also don’t think a consensus was reached in the number of spaces for indentation. IIUC, the current implementation is 3 spaces, but this is highly irregular although it has other nice properties.

2 Likes

Yes, I agree. We will not be able to mandate that, nor do we want to. Number of spaces is not defined in the language and is orthogonal to everything else. If there is a standard it’s formed organically.

1 Like

True, although official examples, books, style guides and formatters do matter in this regard, so I personally try to be conservative there.

1 Like

Thank you for continuing to iterate on this and seek input.

I’m definitely in favour of ‘with’ over ‘:’. Keeping ‘:’ for type ascriptions makes the language cleaner and more regular.

For consistency, I do think that it would be good for the language to also allow brackets or ‘with’ to be used when collective extension statements are defined. Although perhaps that is already allowed in the latest version?

But for most class/object definitions I still think that the best symbols are probably ‘{’ and ‘}’. I guess the one benefit of ‘with’ is being able to name the end of the class, but a decent editor should be able to provide that information anyway.

For short method bodies and control statements I can easily see the benefit of being able of being able to elide the brackets to make the code shorter and cleaner. But for much bigger/longer constructs the appeal isn’t so obvious to me. Once Scala 3.0 is out (and fastparse is available) I will move my code to Scala 3 and try out the different choices, and pick the syntax that I prefer.

I do strongly think that we should keep this experimental for a release or two - but I don’t think that means that it needs to be under an experimental language flag. This would mean that the updated Scala books might not get written until 3.1, but on the plus side it is possible to get real feedback about how Scala 3 is being received and used by the wider community before the syntax ends up being fixed in stone for the next 10 years.

It would be nice to do this in one release, but I still think that it is more important to be absolutely confident that all of the right choices are being made here.

2 Likes