Feedback sought: Optional Braces

I was originally more opinionated but I’m now of the opinion that whether we choose : or with wont really matter. They both work for slightly different reasons and after a while, we’ll all just get used to it and not even really see it anymore.

I think the bigger problem is with consistency from a users point of view. Whatever style we choose for class and object should also be applied to given and extension blocks. I know from the compiler author’s point of view there’s some objection in that the semantics are slightly different, I think @odersky doesn’t consider an extension block as being “a new scope” where as he does with class. From at user’s point of view (at least from my perspective), they’re all the same. To me a indented block is a new scope and if it’s technically not, I don’t really care (nor do I think users in general are going to care either). I think both Scala beginners and Scala veterans would find it easier and more regular to just remember to use one style (: or with) to mark a line as having an indented body. Trying to remember that extension blocks need to be declared differently than class blocks because of some implementation detail that isn’t important to me is a situation I hope we can avoid.

Of course there’s always the danger that when one tries to speak for many based on personal intuition there’s a chance you’re completely misrepresenting the many. :slight_smile: Please chime in if you disagree or maybe like if you agree. I’ve come to believe that this decision is going to be more impactful than : vs with.

10 Likes

Why did the coder use optional braces? …

This space intentionally left blank

A: To keep their pants up!

The thread is so long, I don’t remember if someone already told that old joke.

In fact, the thread is so long, somewhere near the beginning of it is where someone invented the original joke, although I first heard it as why the fireman wore red suspenders. That was a while ago, hence fireman and not firefighter. It’s funnier with purple suspenders.

I hope dcsobral got a couple of gumdrops back when he put in his two cents. I still match braces with % in vi.

However, it’s also true that I misconstrued some sample code from a ticket, where the indentation made a difference, and someone had to tell me to shift it left to reveal the bug.

I can already hear myself saying in desperation: Siri, where is the end of this block?

And she will reply: “At Sunnyvale and Fremont! Ha, ha. I can tell you many jokes, just ask me.”

I agree with japgolly, opinionated Scala has cured me of holding opinions.

One idea that hasn’t been floated is to code in github-flavored markdown. Then however it renders is the significant indentation you get, and therefore the inferred block delimiters.

The beauty of that solution is that your code samples are guaranteed to render correctly in the docs.

Since my Parisian friend appreciated this pun, I’ll repeat it here:

This space intentionally Left Bank

That must be an old one, but google doesn’t find the t-shirt for me.

2 Likes

I think one problem is with backwards compatibility. Say your old code has a stray whitespace, like so:

class A
 object B

It is going to be interpreted differently by Dotty than former Scala versions.

2 Likes

Perhaps this could be helped by a migration warning? Stray spaces in accidental indentation is after all detectable by a compiler and Scala 2.13.5 could issue warnings.

Also, it would be interesting to see in the community build, how many actual cases of stray spaces there are.

And if we change the rule to: “at least two spaces or at least one tab” then the problem might be even less in practice.

7 Likes

I think Scala 3 complains about bad indentation. So I am okay with the following:

Scala 2.13:
object C extends A with B
object D // extra indentation ignored, D is sibling of C

object C extends A with B {
object D // braces, indentation not required so D is member of C
}

Scala 3.0:
object C extends A, B
object D // extra indentation is an error, should only be used for scoping, but can’t silently have different meaning than Scala 2

object C extends A, B with
object D // indentation plus with keyword to disambiguate, so D is member of C

Scala 3.1:
object C extends A, B
object D // indented, unambiguously a member of C, so no need for with

1 Like

I totally agree on this point. Defining a set of extension methods for a type feels exactly like defining a set of methods for a class, object, or trait, and it feels like they should use the same syntax.

4 Likes

Hmm. I wrote it in Gmail. I guess it stripped indentation, even though I made the code in a fixed width font. To be safe I wrote what the indentation was in the comments.

(Of course this is a big disadvantage of indentation sensitivity.)

Should I resend it from the app?

This ambiguity has caused a lot of unnecessary pain in languages like python, and I’d suggest reconsidering that tabs should be forbidden in “optional braces” mode from the get-go. I’d be interested to hear who thinks that permitting tabs is a necessity for them - every decent editor allows setting the tab key to a given value of spaces.

1 Like

The editors can take of it, including converting tabs to spaces. I don’t think this should be mandated by the compiler.

1 Like

@h-vetinari - The trick that Scala uses to avoid problems is that the leading whitespace must be the same for all lines. It’s a syntax error if it isn’t. So you can use whatever crazy scheme you like, as long as it’s consistent.

You can’t indent to one tab from space-space-space-space. But you can indent from tab to tab-space-space, or from space-space-tab-space-space to space-space-tab-space-space-tab. Whatever makes you happy.

This makes it very hard to get things unexpectedly wrong. Scala might not support your favorite elision of spaces to tabs, but you won’t get “unnecessary pain”. Only necessary pain.

2 Likes

What I really like with Scala 3 indentation-sensitive syntax (and I like it better and better the more I work with it), is that I can move around members like building blocks, e.g. I can move methods between classes. It feels like a productivity booster not having to fiddle with braces then.

But the <indent>-marker hinders me :frowning: . For example, if I want to move def f from A to B I need to also change marker, which is tedious and easy to forget:

class A(const: Int) with
  def f(x: Int): Int = x + const 

class B(const: Int)

When moving I have to fiddle with width to get it right:

class A(const: Int) 

class B(const: Int) with
  def f(x: Int): Int = x + const

But with no token it gets super-easy; just move around with copy-paste or alt-up/down-arrows or whatever shortcut your editor offer. And no need to adjust it to the case when going from no method to having a method.

class A(const: Int) 
  def f(x: Int): Int = x + const 

class B(const: Int)

With no marker, moving around is much simpler, making the benefits of optional braces really pay off:

class A(const: Int) 

class B(const: Int) 
  def f(x: Int): Int = x + const 

16 Likes

This will almost always be unintentional, and frustrating to debug. I don’t see the value of that footgun-freedom - just because the compiler can, doesn’t mean the compiler should.

I’d love to see a compiler error message for this one.

1 Like

The value of the freedom is that the compiler doesn’t judge you unless it has to. If you muck something up, it will yell at you. Otherwise it works.

You’ll just get a The start of this line does not match any of the previous indentation widths. message (or similar…depends where things change), plus an explanation of how the indentations differ.

If it is required that not only the width must match, but the exact sequence of spaces and tabs, then that error message is incorrect.

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?