Feedback sought: Optional Braces

I’m sure this is naive, but what if we went with the <no-marker> approach, and then for givens used something like this:

given instance Ord[Int]
   def compare(x: Int, y: Int) =
      if x < y then -1 else if x > y then +1 else 0
given alias Ord[Int] =
  someInstance

My reasoning for this is that when people say “given alias” or “given instance,” I can’t remember which one is which, but if I see keywords like that, they become much more obvious.

I have previously preferred the : approach, but if that’s not feasible, at this point I prefer the <no-marker> approach. Part of Scala’s appeal is its conciseness, and if we use with for every class, it will require 2-3 keywords just to define a class:

// 2 keywords required
class Foo with
   ...

// 3 keywords required, 5 words overall
class Bar with
   ...
end Bar

Again, just spitballing. I comment because I care. :slight_smile:

It’s the other way around, given alias is the one which takes an =, but I get your point and proposed something similar a year or so ago (these syntax discussions have been going in circle for a long time now).

2 Likes

Oops, thanks! I’ll fix that.

I agree. And FWIW, I don’t really associate : with “scope.” When I see it I think, “Pay attention, here’s a group of related things.” That’s why I like it in these contexts. (Really, I think it’s human nature to think this way.)

I’ve created an account to add this having been following optional-braces for what feels like a couple of years at this point!

I’ve been using Scala full-time (>95% of the code I write) for ~5 years. Frankly though that should be irrelevant. Anyone at all involved in software development (be they an engineer, manager, product etc.) should be able to look at this thread and tell us that this feature is not ready for prime time and forcing it out the door will only lead to problems.

Seemingly fundamental design decisions are being discussed and Release Candidates are imminent.

I know it will be extremely painful for everyone that has created material dedicated to this but on a the most basic level pushing this change as mature (and default!) into 3.0 risks the success of Scala 3 as a whole. Edge-cases will be found and it will likely haunt the language until they can be fixed with further breaking changes. It’s a sunk-cost fallacy to prioritise short-term pain (education material) over the long-term success of the language.

In my honest opinion I don’t like the change, equally though I haven’t tried it in anger. It may well turn out I love it. Unfortunately my day-job eats up all work time and I don’t like coding in my spare time. I think that’s another point: this change probably needs to be tried out at scale (i.e. in-industry) before it can be completely given the green light. That just hasn’t happened with dotty/scala 3 because it was so fluid.

At my work most people are just sad at the amount of time and effort this change has taken away from other potential improvements in Scala 3. Just look at this thread, 465 comments in 1 month.

5 Likes

I changed everything to with in our own codebase and started to do the same changes in the community-build. But my experience gave me pause. There were some examples of beautiful code (notably in the intent project) that got much worse with the new scheme. This code used -Yindent-colons, which is not yet standard, but could come after 3.0. An example is:

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

This is legible and clear. No apply operator we invent could even come close. So, I come back to my earlier statement that in theory with and an apply operator are better but in practice : is better.

Here’s are the layout axioms we should follow in Scala:

  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.
  2. If something can follow on the next line, then several such things can also follow, as long as everything is indented.
  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 :.

Note: The “must” in “must be followed” should really mean “must obviously”. It must be immediately clear from the text that something must follow. For instance I would rule out things like

a b c d
  e

since these would rely on counting that d is an operator. On the other hand, in

a +
  b

it is obvious that b continues the previous line since + is an operator, and we do not support postfix as a standard anymore.

With with the third axiom is not needed for classes and friends. But it introduces syntax which looks unnatural for people coming from Scala 2 and it does not solve the problem what to do about function arguments in a satisfactory way.

Nothing at all is also not a better alternative: When reading code, I should know whether a line stands by itself or is continued later. : achieves that in a very natural and low-key way. If it is missing, there’s a problem with code like that:

class C

  /** a 
      very
      long
      comment
   */

 def f = 1

Is f part of C or not? I should not have to count or guess here.

So, taken together, I’d say it’s back to the original design. We had the best solution all along. It was certainly not for lack of trying that the alternatives got discarded. They were considered extremely seriously but in the end : is clearly superior.

The one aspect where we have a clash with : is the given syntax which still uses with. That’s a bit of an inconsistency, but we can live with it, IMO. We went though this many times and found no better way to do it.

So, time to stop turning in circles. After by now lots of experience with : and extensive trials of the alternatives, I conclude that it works overall really well.

3 Likes

For whatever it’s worth, : or with only really helps if f is the first thing in the class. Add something else, and we’re back to ambiguity:

 class C:
  val b = 1

  /** a 
      very
      long
      comment
   */

 def f = 1
5 Likes

<no-marker> really doesn’t work well once you want to put class headers on several lines.

For example:

class Foo
  // These are here meant to be class parameters,
  // but it's not clear if we have no marker before a class begins:
  (x: Int)
  (y: String)
  println(x + y) // and this is a constructor statement

or

class Foo(x: Int)(y: String) extends Bar
  (x)(y)  // meant to be arguments to Bar

To be fair, : barely makes this better:

class Foo
  (x: Int)
  (y: String):
  println(x + y)

So I still prefer with:

class Foo
  (x: Int)
  (y: String)
with
  println(x + y)
7 Likes

That’s why there are end markers. Well formatted code uses an end marker whenever there are blank lines in a scope. This could be enforced by linters if necessary.

1 Like

I hate to say this but at this point it seems pretty clear that all options have serious weaknesses. This is really not ready to freeze. Can we please just mark it experimental and subject to change, and focus on more important things?

10 Likes

Using indentation-sensitive syntax leads to some natural restrictions (or at least conventions) - I’d recommend having a look at PEP 8, which represents a lot of cumulative experience in the python ecosystem about this. Inspired by PEP 8, your examples would look like:

class Foo
      // one more indentation level...
     (x: Int)
     (y: String)
   // ... than for body
   println(x + y)

resp.

class Foo(x: Int)(y: String) extends Bar
      (x)(y)
   // body here

Of course, the “black” style would be to optionally even break the brackets, with yet another additional indentation level for their content…

1 Like

I disagree emphatically. We have a great scheme which has been deployed in practice for 18 months now. We gave it an extremely serious and critical evaluation, including going deep into several alternatives. The original solution turned out to be clearly the best.

This discussion was very useful since it did evolve our understanding, and will guide progress post 3.0. In particular, the three layout axioms crystallized only recently. But one cannot argue that this is rushed. In fact, we have been turning in circles for too long. Not everyone will agree with the outcome, but that was not expected either.

4 Likes

7 days ago you started a post with:

I am getting more doubts that the choice of : over with was the right one

I don’t necessarily think you can say that one option is “clearly the best” given that post and the sheer amount of backwards and forwards in this thread.

I posted right before you did which means my first post is (understandably) going to be overlooked. They key point of that post was that this whole thread should be sounding major alarms that the feature isn’t ready for complete release. I completely agree with @nafg and everyone else stating that this should be put behind an experimental feature flag.

5 Likes

I haven’t found a compelling example where end markers are better than an equivalent set of braces. Honestly, even the example in docs looks orders of magnitude better with braces:

Side by side comparison
End Markers Braces
package p1.p2:

   abstract class C() with

      def this(x: Int) =
         this()
         if x > 0 then
            val a :: b =
               x :: Nil
            end val
            var y =
               x
            end y
            while y > 0 do
               println(y)
               y -= 1
            end while
            try
               x match
                  case 0 => println("0")
                  case _ =>
               end match
            finally
               println("done")
            end try
         end if
      end this

      def f: String
   end C

   object C with
      given C =
         new C:
            def f = "!"
            end f
         end new
      end given
   end C

   extension (x: C)
      def ff: String = x.f ++ x.f
   end extension

end p2
package p1.p2 {

   abstract class C() {

      def this(x: Int) = {
         this()
         if x > 0 then {
            val a :: b = {
               x :: Nil
            }
            var y = {
               x
            }
            while y > 0 do {
               println(y)
               y -= 1
            }
            try {
               x match {
                  case 0 => println("0")
                  case _ =>
               }
            finally
               println("done")
            }
         }
      }

      def f: String
   }

   object C {
      given C =
         new C {
            def f = { "!" 
            }
         }
      end given
   }

   extension (x: C) {
      def ff: String = x.f ++ x.f
   }

}

Outside of the compiler project, how many codebases of reasonable size have actually converted to the Optional Braces style? (I don’t think we should count student work, as it’s neither large enough nor long lived enough to provide useful feedback about readability)

It doesn’t seem like a rigorous test if the primary place it’s been tried out is a singular codebase with a fairly small group of developers, particularly as IIUC the compiler has been effectively rewritten from scratch, so it’s mostly green code.

3 Likes

Several projects in the community build, the two MOOCs, our own doc pages, as well as 3 books that are in preprint or close to it use that syntax as well. All book authors embrace that syntax. Seriously, we have validated it enough. At some point one has to reach a decision. Putting it under experimental would be the single worst thing to do because that way the discussion would go on forever.

1 Like

The problem with all but the community build is that they all tend towards extremely short, self contained, minimally nested chunks of code. They simply aren’t representative of what or how this works at scale.

The community build ones are more interesting, though it would be helpful to know how many, what size projects, how long they’ve been using Optional Braces, and critically - how many have shown up in this thread and what feedback have they given.

That’s not a fair comparison. You will use an end marker only of there are blank lines before. Adding end markers everywhere defeats the purpose obviously.

Putting it under experimental would be the single worst thing to do because that way the discussion would go on forever.

An alternative way of looking at it is that It allows the discussion to continue whilst the major release of a language with already significant changes to syntax and idioms beds into the ecosystem and industry

1 Like

It’s not an unfair comparison either, as I didn’t do any minimization (many of those could be single-lines), and it’s an example from the docs that should be used to sell the idea that end markers are a useful thing.

1 Like

One thing I learned in writing books that are printed is that you really, really want to make sure you’re right, or at least as sure as you can be about what you’re doing. So given that experience of setting something in stone, I really appreciate the time and effort that you all put into making this decision (all of these decisions, really).

4 Likes