Feedback sought: Optional Braces

I agree in thinking we should value consistency between class, object, given, and extension blocks. Extension blocks don’t define the same kind of scope as the others, but they are still scopey (in the broader intuitive sense of what a “scope” is):

  • Declarations inside an extension (x: T) block are put into the surrounding scope. This makes it not look like a new scope.
  • The x: T in such a declaration is accessible only inside the block, but not outside. This makes it look like a scope (in the same way as inline functions and method declarations scope their parameters).

It seems the status quo is that these blocks are scopey enough to need indentation (after all, something is needed to specify the scope of the x: T), but not scopey enough to need extra markers, and indeed un-scopey enough to necessarily be differentiated from the other more true scopes.

I’m not sure if I see the need for that. It seems to introduce more complexity than the alternative of just expanding the intuitive notion of a scope.

(I feel like I read people suggesting this a long time ago, but maybe this problem would be avoided if it were disallowed to call extension methods directly by name. I personally find this idea appealing, though I’m sure it has downsides (but it’s off-topic and I don’t remember the other discussions on this). Anyway then it would behave a lot like an object or class declaration, in that you’d need to reach into the (apparent) scope of an instance before you can access a method declared in the block. If this were true, I think there’d be a stronger case for regarding collective extensions as creating something which can be intuitively regarded as a “new scope.”)

Regardless, I’m pretty on board with the idea of having no markers at all (which has the benefit of being uniform) — it seems to have the most ergonomic benefits (esp. what was just pointed out by @bjornregnell). Unless people have tried this in practice and found that the “stray space” issue is frequent or causes insidious problems. I’m not sure I buy that it would be an issue unless people have had it happening in practice; I’ve rarely had these kinds of issues in languages where indentation is significant (and I’ve almost certainly spent more time wrangling braces and parens than indentation levels… though I do more Scala than Python). In such a language you don’t just leave stray spaces hanging around (and neither would you get away with it for long), precisely because they are significant.

7 Likes

So, we have three options that are “alive” in this engaging discussion:

  • with, which is regular with classes, objects and givens, but not with extension. And it is clunky so many don’t like it.
  • :, which is irregular with some types of givens, and some don’t like it.
  • <no-marker>, which is regular with classes, objects, [some] givens, extensions. There is no risk of forgetting to write a marker, and moving around code is really ergonomic. Many think it looks the nicest and reads the best. Downside: stray spaces makes indentation significant

@odersky I think we maybe have thrown out the baby with the bath tub water, so to speak, when the <no-marker> option was ruled out early on because of the “stray space problem”. So perhaps we should think like this: The <no-marker> option is most ergonomic and most regular, so let’s choose that and do what we can to help to detect stray spaces, e.g. by migration warnings, IDE-support etc. What do you think?

*Edit: I updated the list above to “[some] givens” based on example in reply by @smarter below.

6 Likes

Setting asides other issues for now, I don’t think <no-marker> is great for givens considering that we have both given aliases and instances:

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

Right now it’s very easy to visually distinguish between these two kinds of givens, but if we drop the with, then the difference becomes a single character which is both hard to see and hard to explain (and what if someone moves the = on the line below, how do we interpret that?).

Here’s a concrete example of what could happen:

  trait Foo[A]:
    def foo: Int = 0

  def fooInt = new Foo[Int] { def foo: Int = 1 }

  given a: Foo[Int] =
    fooInt

  given b: Foo[Int]
    fooInt

The second one is likely to be a typo for the first one, but since they only differ by a single character and both compile fine, it’s an easy mistake to make and one that could be very hard to find.

3 Likes

Thanks for the example, @smarter ! I haven’t thought about that… :innocent: But perhaps there is another solution to the “distinguish given aliases and instances problem” than going for with everywhere. I’ll think a bit more about that to try to figure out what could be done under the assumption that <no-marker> is the best solution for all other cases.

It just struck me that anonymous classes might also be a downside for the <no-marker> option as, in for example:

  new ClassName 
    def addedMethod: Int = 42

it might be too silent, in a similar way as with givens.

I see that the current docs for 3.0.0-RC1-bin-20210123-b731573-NIGHTLY still have colon for anonymous classes and packages; is that just because of ongoing updates, or is the colon intended to be left as is in those cases?

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 MarkersBraces
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