Feedback sought: Optional Braces

We might need an M4 for some late changes to inlining. Essentially, inlining non-transparent macros will be done after typer. This has several potential benefits, but we might need another cycle to make sure that all code that uses the new meta programming can migrate smoothly, We will know for sure whether we need another cycle next Monday.

2 Likes

Could you provide a more motivating example for the change with classes?

To me, all of these look pretty much the same:

lock.synchronized:
   handler.handle(sbtEvent)
   sbtEvent.log(loggers, event.duration.toMillis)

lock.synchronized
   handler.handle(sbtEvent)
   sbtEvent.log(loggers, event.duration.toMillis)

lock.synchronized...
   handler.handle(sbtEvent)
   sbtEvent.log(loggers, event.duration.toMillis)

But it’s not the case where with would be used anyway. So where’s an example of that making code so much less beautiful? That’s really the issue, isn’t it? Regularity vs. practicality?

I just wrote code that contains

trait Status {
  def mark: String
}
case object Leader   extends Status { def mark = "*" }
case object Officer  extends Status { def mark = "!" }
case object Ordinary extends Status { def mark = " " }
case object Emeritus extends Status { def mark = "-" }

I can change it to various different schemes and none look particularly more beautiful to me:

trait Status with
  def mark: String
case object Leader extends Status with
  def mark = "*"
case object Officer extends Status with
  def mark = "!"
case object Ordinary extends Status with
  def mark = " "
case object Emeritus extends Status with
  def mark = "-"
trait Status:
  def mark: String
case object Leader extends Status:
  def mark = "*"
case object Officer extends Status:
  def mark = "!"
case object Ordinary extends Status:
  def mark = " "
case object Emeritus extends Status:
  def mark = "-"
trait Status
  def mark: String
case object Leader extends Status
  def mark = "*"
case object Officer extends Status
  def mark = "!"
case object Ordinary extends Status
  def mark = " "
case object Emeritus extends Status
  def mark = "-"
trait Status...
  def mark: String
case object Leader extends Status...
  def mark = "*"
case object Officer extends Status...
  def mark = "!"
case object Ordinary extends Status...
  def mark = " "
case object Emeritus extends Status...
  def mark = "-"

Other than the weird

trait Status:
  def mark: String

they all look pretty comparable. So I’m willing to believe that : makes a difference, but a better motivating example would help.

1 Like

I think : is clearly better. We should not do indentation here without an intervening token, for reasons given, and ... is neither as legible nor as clear as :. Besides, let’s look at the three layout axioms in detail:

  1. If some code must be followed by an expression or definition, that expression or definition can be on the next line; no ; is inserted.

    This is self evident and for the most part holds also in Scala 2, I believe

  2. If something can follow on the next line, then several such things can also follow, as long as everything is indented.

    This is the essence of optional braces. It is also self-evident.

  3. Otherwise, if some code may be followed by an expression or definition, we can give it
    on the next line, as long as the previous line ends with a : .

    That’s the only non-obvious rule. We could avoid it by tweaking the syntax to require with after classes and introducing an apply operator such as .... Grammatically speaking, that would be better, since we need one less layout rule. But visually we replace one natural punctuation character by two different conventions, neither of which is very familiar.

So that’s the rationale in detail. It is admittedly post-hoc. I cannot argue by deduction from first principles here. So I try to rationalize my impressions from working with the different alternatives in the code.

2 Likes

This is a special case of one-liners with a recurring pattern that is nice to read and artful to the eye :slight_smile: . I think you should stick with braces in this case! Braces will not go away and I think we will develop conventions for mixing braceful and braceless stuff because of nice one-liners that happen to fit the column width.

Here is a nice mix:

trait Status:
  def mark: String

case object Leader   extends Status { def mark = "*" }
case object Officer  extends Status { def mark = "!" }
case object Ordinary extends Status { def mark = " " }
case object Emeritus extends Status { def mark = "-" }
3 Likes

It’s a little sad but we can live with one inconsistency. What about extension blocks though?

  • If we require :, the scheme is always use :, except for givens
  • If we require with, the scheme is use with for given and extensions, : elsewhere
  • If we have nothing (which I think is the status quo), the scheme is use with for given, nothing for extension, and : elsewhere

Each option gets more inconsistent as we go down the list. I believe regularity and predictability are very important so obvious my vote goes to the first option.

5 Likes

+1 for : on extension blocks. givens are unique enough that their exception will not bother much.

1 Like

Extension blocks have to be as they are according to the axioms:

  • If some code must be followed by an expression or definition, that expression or definition can be on the next line; no ; is inserted.

Clearly, that’s the case. An extension clause must be followed by at least one extension method. Besides it would be very weird to allow

extension (x: T) def f = ...

but forbid

extension (x: T)
   def f = ...
  • If something can follow on the next line, then several such things can also follow, as long as everything is indented.

I rest my case :wink:

I think there’s a potential ambiguity there. So according to the new axioms, these are all equivalent:

extension (x: T) def f = 1 + 1

extension (x: T)
  def f = 1 + 1

extension (x: T)
  def f = 1 +
    1

That makes sense from the point of view that they’re all a single definition. But extension blocks can be defined with multiple methods too. So this is legal (and desirable):

extension (x: T)
  def f = 1
  def g = 2

but it doesn’t follow the axiom because the following aren’t legal equivalences:

extension (x: T) def f = 1 def g = 2

extension (x: T) def f = 1; def g = 2

That’s why I can’t help but view this as a new scope.

extension (x: T):
  def f = 1
  def g = 2

which reads to me as

extension (x: T) {
  def f = 1
  def g = 2
}

So I guess if we allow : for extension blocks then it would work like this (and adhere to aforementioned axiom)

// No :, single method = ok
extension (x: T)
  def f = 1

// With :, single method = ok
extension (x: T):
  def f = 1

// No :, multi method = error
extension (x: T)
  def f = 1
  def g = 2

// With :, multi method = ok
extension (x: T):
  def f = 1
  def g = 2
5 Likes

Okay, fair enough. My impressions are different, and I can explain why, but my impressions are different from others’ often enough so that I can’t really claim to have any particular wisdom about what works better in general, just offer my point of view. For instance,

trait Status:
  def mark: String

is unpleasant to me because it has two different uses of : right on top of each other–one with a “consists of” meaning, and the other with an “is-a” meaning. It’s like writing x.map(_ andThen foo _), except you have to do it all over the place instead of coming up with it as a weird confusing example. To me it feels like writing def foo(x = Int = 7) = 2*x + 1. Syntactically it’s unambiguous for function arguments to use = for both type and value assignment, but the context switch is jarring to me.

But if other people feel that it’s totally cool, and that somehow

trait Status with
  def mark: String

is clearly worse (even though to me the parallel with incorporating methods etc from another trait with with is very clear so it feels sensible), well, maybe I’m just different.

After all, people somehow came up with English quotation rules that were like, “Let’s put in commas that are totally redundant with quote marks, and capitalize the first word of the quotation, and put the ending comma inside quotes even though that comma isn’t being quoted but rather is part of the punctuation of the outer sentence,” instead of being principled. Compared to that, using colon is dramatically more rational.

I do wish, however, that I understood what it felt like to like :, rather than to constantly find it slightly jarring. (The synchronized: thing feels okay. But synchronized... does too. So…still puzzled.)

6 Likes

Could you share some more examples that doesn’t use -Yindent-colons?

I was under the impression that -Yindent-colons was a separate issue. I’m very critical of it, I’m not sure one more way to pass arguments to methods/functions will make the language simpler, more powerful, more approachable or better in any way. Having : mean “apply the following indented block of code to this method” is not obvious or intuitive to me in any way, and it doesn’t align with any other language either.

I’ve shared why I think using : in template definitions hurts readability, I think using it in expressions is even worse for readability.

I again, feel like that this thread would have been much more efficient if we actually had some common problem definition, because in the case of -Yindent-colons, I don’t understand at all what problem it solves. I don’t think it should influence the rest, which is more clear.

I really want to address this in particular. People coming from Scala 2 will have to learn the new syntax. I doubt with will be that much less natural than :. Function arguments to me is a completely different issue, and has absolutely nothing to do with template definitions.

At some point one has to take a step back and consider what one would do if this was a new language. The only reason we are coupling these very different things together is that Scala 2 uses braces for them. That’s where the similarities end. I really think a different solution should be used for block arguments regardless of what the syntax is for template definitions.

If it is the case that proposed alternatives like do have some unacceptable weaknesses then I really don’t see a compelling reason why this is so bad:

lock.synchronized {
   handler.handle(sbtEvent)
   sbtEvent.log(loggers, event.duration.toMillis)
}

I still feel like a big problem in this discussion has been that people don’t seem to have a common view of what the problem is in the first place. This makes it very difficult to have a productive discussion. I understand the bailing wire argument, but when it comes to examples like, by-name block arguments, I really don’t see the issue.

(@odersky, please bring back with, you gave me a taste of consistency and now I must have it :cry:)

7 Likes

While I am definitely not an expert, it seems the “colons everywhere they need to be and with where they must” seems to be a “reasonable” resolution at least in the legal sense. In judicial cases, there are ex post (make judgments based on events that have happened) vs ex ante considerations (make judgments based on predictions about future events and how actors would respond to it) conflicting in judgments; maybe this resolution balances them. This thread would surely be interest to social computing archaeologists in the future (or the present, maybe there is a paper or at least blog post in this). A nice essay on ex post vs ex ante: http://www.thelegalanalyst.com/ExAnte.pdf

Meanwhile in the Python world where there is supposedly only just 1 way of doing things, there is a code formatter with 20k likes. Would such a thing be possible or useful in a post Scala 3 world?
Talk: https://www.youtube.com/watch?v=esZLCuWs_2Y

1 Like

The first of these is illegal and the second parses as

extension (x: T) def f = 1
def g = 2

So I don’t see what this is supposed to show except that semicolons outside braces may be surprising since they terminate everything, not just the immediate statement before. That’s not linked to extension methods. The following is probably equally surprising:

if cond then a else b; c

parses as

if cond then a else b
c
1 Like

On a slight tangent: this actually caused a real bug in my code.

Happy to use the newly-introduced quiet control syntax, I had basically written:

while i < len do something(); i += 1

thinking it was just putting on one line the supposedly-equivalent:

while i < len do
  something()
  i += 1

But of course, my code lead to an infinite loop.

2 Likes

I agree that : for expressions under -Yindent-colons can lead to neat-looking code, for example with the locally helper:

locally:
  given Ctx = ...
  ...

However, it can also look very strange and ugly.

For instance, I have code that looks like this:

head :: :
  val tmp = blah()
  tmp.fold:
    0
  :
    x => x * 2

With braces, for reference:

head :: {
  val tmp = blah()
  tmp.fold{
    0
  }{
    x => x * 2
  }
}

So I’m not completely sure we have found the optimum just yet. It may be that this is the least bad alternative, but the code above makes me think there might be other approaches that would help make some code look less alien.

4 Likes

That will not be needed anymore. The :: means that the expression must continue so you can indent without :. In fact by these new axioms, : would not be allowed at this point.

I am not sure about the fold. My tendency would be to disallow the last : there as well. But we can sort that out post 3.0. EDIT: I think we should disallow : on its own line. : serves as an intro only if following something on the same line. Everything else looks weird. Tant pis for fold, I believe functions with several braces are an anti-pattern anyway.

1 Like

I find it a shame that with lost, especially since you came up with this conclusion just a week ago.

To make things even worse, it seems the scheme optimizes ‘cute’ code in self contained examples, instead of ‘real world’ classes that have lots of parameters spanned over multiple lines.

That said I accept whatever comes out of this. However I am quite afraid that all the options will lead to many different styles in the wild. Some might like the quiet syntax, but not the braceless syntax, others use both, and some may even adopt the yIndent-Colons while probably a large group will just stick with Scala2 syntax. Scala already doesn’t have a great reputation in this regard, and this will even make it worse. I really hope we can somehow actually push Scala to be more opinionated in the future. Because I don’t think Scala3 does this enough (even though I am very much looking forward to it with regard to everything else).

It has become painfully apparent that a large group will continue to disagree on these stylistic choices. So I do think Scala should move towards one way of doing these things. (Choice is good, unnecessary choice IMO is not).

5 Likes

Am I the only one reading “I don’t like currying” here?

2 Likes

What’s the proposed alternative?

This does not really look better:

  opt.fold(
    val tmp = bar()
    tmp + 1
  ,
    x => x * 2
  )

Besides, curried parameters are etched into the standard library and libraries provided by the community. Whether or not we consider them anti-patterns now, their syntax ought to be well-supported, as they are used all over the place.

I would suggest going with:

  opt.fold:
    val tmp = bar()
    tmp + 1
  .apply:
    x => x * 2

which already works in Scala 3, apparently.

1 Like

If methods that take multiple functions are an anti-pattern, then Spark is bad.

What I fear the most about optional braces is community won’t have an agreement on some option as it happens in the thread.

That will cause inconsistency in different codebases and might lead to forbidding optional braces syntax in some companies guidelines due to inconsistency, as inconsistency in approach creates mess in code.

Speaking from our company point of view, if braces ship with 3.0 we considering to allow optional braces only for simple unambiguous parts of code and disallow else (keep braces)

7 Likes