Feedback sought: Optional Braces

I absolutely love the new syntax and would be incredibly sad if it didn’t make it into Scala 3. Please keep it!

2 Likes

Another remark on stricter enforcement of indentation rules. The current scheme warns if
indentation would be misleading, I.e. for

  class C
    def f() = ???

you get:

-- Warning: test.scala:4:4 -----------------------------------------------------
4 |    def f() = ???
  |    ^
  |    Line is indented too far to the right, or a `{` or `:` is missing

It does not warn otherwise such as in the case

test(...)
  f(...)

because that’s not misleading. test(...) cannot open an indented block. So, I think before we tighten the rules and throw out potentially useful formatting we should let this mature and look at style checkers first. An example of what some people might find useful is the following:

def f(x): Boolean = 
  val p = someFunctionInvolving(x)
     someLongCondition(p)
  || anotherLongCondition(p)
  || yetAnotherCondition(p)

There is an argument that lining up the conditions gives nice esthetics. Maybe we conclude we should support it or maybe we conclude it looks misleading (even though, technically, it isn’t). That’s why I would be reluctant to tighten offside rules at this time.

Also, since we do not distingush between indented and non-indented mode, any offside rule we impose would have to impact code in braces as well.

This formatting looks pretty confusing to me. Even if someFunctionInvolving(x) does not introduce an indentation token, I have to scan for the end of the corresponding line to find that out. I believe not having to scan for the end of lines to determine semantics is precisely the argument given for joining expressions with operators on the start of the line instead of at the end.

So the code above looks almost as hard to read as if I had written this instead:

def f(x): Boolean = 
  val p = someFunctionInvolving(x)
  someLongCondition(p) ||
  anotherLongCondition(p) ||
  yetAnotherCondition(p)

So perhaps we could require parentheses, to clarify things:

def f(x): Boolean = 
  val p = someFunctionInvolving(x)
  (  someLongCondition(p)
  || anotherLongCondition(p)
  || yetAnotherCondition(p)
  )

(I hope this does not parse as an additional argument to someFunctionInvolving.)

4 Likes

What about this one

def f(x): Boolean = 
  val p = someFunctionInvolving(x)

     someLongCondition(p)
  || anotherLongCondition(p)
  || yetAnotherCondition(p)
end f

Confusing or not?

I find it confusing. With significant indentation, I would naturally expect this to be written as:

def f(x): Boolean = 
  val p = someFunctionInvolving(x)

  someLongCondition(p)
  || anotherLongCondition(p)
  || yetAnotherCondition(p)
end f
4 Likes

FYI I created the feature request for Metals about better pasting when using optional braces:

We should be able to support it via range formatting LSP method, since editors each have their own rules when it comes to indentation. Any comments or suggestions will be welcome. We should probably also follow up with an issue for Intellij.

2 Likes

Great, thanks for the fast reaction!

Actually before you added the end marker I didn’t couldn’t see if this is was supposed to be decoded as:

def f(x): Boolean = ???
val p = {someFunctionInvolving(x)

   someLongCondition(p)
|| anotherLongCondition(p)
|| yetAnotherCondition(p)
}

or

def f(x): Boolean = {
  val p = {someFunctionInvolving(x)

     someLongCondition(p)
  || anotherLongCondition(p)
  || yetAnotherCondition(p)
  }
}

or

def f(x): Boolean = {
  val p = someFunctionInvolving(x)

  someLongCondition(p)
  || anotherLongCondition(p)
  || yetAnotherCondition(p)
}

Of course I can decode it correctly after staring at it for a bit, but by that time, my concentration is lost and I’ve lost my flow. Which is ironic because that’s why you don’t like braces when writing code.

The end marker makes things clearer, but without it, the extra indentation before someLongCondition(p) makes it appear to somehow belong to the val p =.

It looks to close to:

def f(x): Boolean = 
  val p =
     someFunctionInvolving(x)
     someLongCondition(p)
  || anotherLongCondition(p)
  || yetAnotherCondition(p)
end f

or even

def f(x): Boolean = 
  val p =
     someLongCondition(p)
  || anotherLongCondition(p)
  || yetAnotherCondition(p)
end f

The lack of some leading grouping token before multiple lines in a val/var/def is the second thing after : that I find hard to read with the indentation syntax.

The thing coming after val, var or def being a single expression, one of which is a { block } was always very appealing and clear to me. I would much prefer to be able to “see” a block expression without having to look to much at context.

The immediate message I get from { is “Of boy, just wait until you see what expressions on the next few lines”. As @curoli already mentioned several times, } is usually actually less important.

Something like this is immediately clear to me:

def f(x): Boolean = 
  let val p = someFunctionInvolving(x)
  in  someLongCondition(p)
   || anotherLongCondition(p)
   || yetAnotherCondition(p)

I also again really want understand the reasoning behind this, @odersky: If I have to write a bunch of end markers in my code to make it readable, then what is the point to all this?

What makes this:

def f(x): Boolean = 
  val p = someFunctionInvolving(x)
  (  someLongCondition(p)
  || anotherLongCondition(p)
  || yetAnotherCondition(p))
end f

any more readable or in any way at all better than this:

def f(x): Boolean = {
  val p = someFunctionInvolving(x)
  (  someLongCondition(p)
  || anotherLongCondition(p)
  || yetAnotherCondition(p))
}

Maybe it would be easier to have a productive discussion about this if there was some common understanding of what the goal is? Is it that “{” and “}” are ugly? Hard to read? Hard to write? Looks old?

3 Likes

I must say that I would find it a bit confusing, specifically the fact that someLongCondition is so much to the right, that the first || starts earlier than someLongCondition.

The following, on the other hand, is much clearer to me:

def f(x): Boolean = 
  val p = someFunctionInvolving(x)
  
  someLongCondition(p)
  || anotherLongCondition(p)
  || yetAnotherCondition(p)
end f

or

def f(x): Boolean = 
  val p = someFunctionInvolving(x)

  someLongCondition(p)
    || anotherLongCondition(p)
    || yetAnotherCondition(p)
end f

In general, I think we can learn a lot about whitespace based syntax from F#:

3 Likes

First, I still don’t like at all that we’re doing this optional braces thing in 3.0. This is a huge change that has had a lot of debate, a lot of furious detractors and proponents, to the point that we’ve discussed indentation syntax more than every other change of Scala 3 combined.

Since it is completely orthogonal to everything else in Scala 3, I have said before and I still think that it should have been either in Scala 2.x, or in 3.y for some y > 0. That way, we could have discussed and tried it in more serene circumstances, lifting an incredible burden from the discussions of all the other changes brought by Scala 3.


Second, a bit about my experience. I’ve had to work with the indentation syntax quite a bit in the past few months, inside the dotty repo, as well as with the other syntax changes to control structures.

Mostly I can deal with writing code while omitting braces. That’s not an issue. I’ve been significantly more hampered and actively slowed down by the if ... then syntax. I keep writing if (cond) as a reflex, and then I have to actually go back and “fix” it to if cond then. That is despite having worked actively in Delphi (Pascal syntax, which uses if ... then) for 10 years before I switched to Scala as my main language.

When reading code, however, I’ve had more trouble. I’ve never worked extensively in any indentation-based language (I did some Python, but not spanning more than a few thousands lines of code), which is maybe why I have trouble interpreting some things. The biggest issue I have is to understand where things start when they are more than 1 level below that start.

For example, I was looking at this line:


and I wanted to understand under what circumstances we can get to that line in the code paths. Because is nested in several layers of if/else’s, it was very difficult to see where it was coming from. I use VS Code, which does highlight indentation levels. But anywhere I put my cursor was not good enough to show me the indentation I wanted to see. There was no way I could make it highlight the start of the various enclosing scopes that were important to me. With braces, I can put my cursor on each brace, one at a time, to immediately see the various scopes. With indentation, not so: I only have access to the innermost and outermost scopes.

Finally, on the technical details.

I absolutely think we should be strict about the indentation start. @eed3si9n’s suggestion seems very important to me.

In addition, I do think it is going to be damaging that we have 4 ways of starting indented blocks, depending on the situations:

class Foo():
  ... 
given Bar with
  ...
extension (xs: List[Int])
  ...
if x < 3 then
  ...

i.e.,

  • a colon
  • with
  • nothing at all
  • some special keywords

and that with those 4 ways of signaling the start of an indented block, we still don’t cover all use cases and we still need braces for higher-order methods!

We can explain the reasons and rationales of those all we want, it’s still a nightmare.

I think we can reduce those 5 (4 + 1) different ways of doing things to exactly 2, using with for all starts of blocks that are not supported by a keyword already:

class Foo() with
  ... 
given Bar with
  ...
extension (xs: List[Int]) with
  ...
if x < 3 then
  ...
xs.map with
  x =>
    ...
// which we should probably also support as
xs.map with x =>
  ...

Since with is itself a keyword, one could also argue that it is only one of the various keywords that introduce an indented block. That changes the story a bit by saying we’re going from 4 (3 + 1) ways of starting blocks to exactly 1.

24 Likes

I finally put my finger on why your code seems confusing, and is a bad idea in terms of formatting.

The reason is that it should always be possible to go from

  val p = foo(bar(x))

to

  val p =
    foo(bar(x))

For instance, as the first step before changing it to

  val p =
    val tmp = bar(x)
    foo(tmp)

But this does not hold with your formatting example!

def f(x): Boolean = 
  val p =
    someFunctionInvolving(x)

     someLongCondition(p) // This is now part of the `val` block above!
  || anotherLongCondition(p) // And this, even more surprisingly,
  || yetAnotherCondition(p)  // is the continuation of the line above!!
end f
3 Likes

Is the “with” in givens an indentation marker? My understanding was that it introduces a sort of refinement, and would appear even if using braces rather than indentation.

Similarly, the then keyword is not an indentation marker, it is part of the if/then/else syntax. If you use parentheses without then you can’t indent a block?

If my understanding is correct that makes only 2 indentation markers. Most things have no indentation marker, you simply start indenting to create a block, except things that don’t support indentation blocks like higher-order functions. The only exception is classes and traits which need a colon to differentiate that it’s not an empty class followed by badly indented sibling code.

We could get rid of that if we made classes require braces, until Scala can error on badly indented code.

(I still say that the whole thing is premature to not at least be marked experimental.)

Is the “with” in givens an indentation marker? My understanding was that it introduces a sort of refinement, and would appear even if using braces rather than indentation.

Correct. with is a required keyword and it allows an indentation block. So with is completely analogous to then, else, try, and the other keywords in that category.

@sjrd with was tried extensively and it just didn’t fly. I agree it looks good as an idea, that’s why I was very keen to try it out, but it fell flat in practice. Too noisy and too much ceremony. The same holds for where.

1 Like

I think with is problematic for the same reasons as :. with already means something, it makes no sense that what comes after with is anything other than a type. Exactly the same issue as with :.

Leaving a side for a moment that it’s weird, confusing and unreadable, reusing with and : for some general block starter makes it really awkward when those clash with new constructs where the syntax is too hard to separate.

Most people, including @odersky think this is looks weird and is unreadable:

given [T](using Ord[T]): Ord[T]:

It doesn’t actually matter if there is some other logical reason the : should be a with under these particular circumstances. The fact that : already dictates more important syntax is bad enough.
I also really don’t see how this is better in any way, and I’d love @oderskys reasoning for why it’s not an issue:

def foo: Foo = new Foo:

Or this:

def foo: Foo:
  type T

I really don’t understand this argument. As I said with is bad for the same reasons as :. It’s confusing.

You keep saying where is somehow inferior and that it has been tried out, but why is this the case?

I understand that it gets old to compare every new Scala feature with Haskell, Haskell is by no means a perfect language, but they use where almost exactly like Scala 3 uses : and I’ve never heard of anyone saying that is somehow noisy.

edit: take this example, many syntax constructs would have to be explained where is not one of them:

class Semigroup a => Monoid a where
        mempty  :: a

        mappend :: a -> a -> a
        mappend = (<>)

        mconcat :: [a] -> a
        mconcat = foldr mappend mempty

instance Semigroup [a] where
        (<>) = (++)
        stimes = stimesList

instance Monoid [a] where
        mempty  = []
        mconcat xss = [x | xs <- xss, x <- xs]

where is immediately clear what it means, doesn’t clash with any existing syntax and it leaves room for using different syntax in refinement types and structural instances.

It would make a lot of sense if this is a structural type:
T where {template body}
and this is a refinement type:
T with {R}

It’s been suggested several times that we make this experimental and put it under a flag. Let me just state flatly: I don’t see a way this will happen. Materials and courses are being written now or have already been written and recorded. We cannot possibly have all the documentation, tutorials, and online courses use indentation syntax and then say, well, it’s actually experimental and you have to turn on a flag to use it. This would be a complete debacle, and I will not let that happen.

Can we go back and re-do all the work? Well, at EPFL alone we have approximately 9 person months invested in the MOOCs. We did this because I am completely convinced that this is a very important step forward for Scala 3. I would have to be even more convinced now that all this was a terrible mistake to go back, throw out all the courses, explain to EPFL central why they have just wasted a large sum of money with the editing, and spend another 9 person months to re-record every lesson with braces. And I just don’t think that will happen either.

So what can happen, and why we are having this discussion, is change some of the details in the proposal. E.g. stricter of offside rules checking, clearer rules for end-marker, some syntax tweaks are all fair game.

6 Likes

You have to take my word for it. I was convinced that it would work, we tried it out, and people did not like it (me neither in the end). What’s different from Haskell I believe is that classes, objects etc are much more common in Scala than in Haskell, and the where is uncommon. Haskell already uses where instead of let. So the where was sticking out too much in the Scala code, whereas it would blend in nicely in the Haskell code.

About with being “an indentation marker” versus "just a regular connector that happens to allow an indent like then": this is a false dichotomy. You can present the syntax in a completely natural way by saying that with is allowed in all those situations, whether you keep writing on the same line or not, and whether you use braces or not. For example, all of the following would be valid as well (they should be):

class Foo() with {
  ... 
}
given Bar with {
  ...
}
extension (xs: List[Int]) with {
  ...
}
xs.map with {
  x =>
    ...
}
xs.map with { x =>
  ...
}
xs.map with (x => ...)

The first one is even already allowed in Scala 2.

Then, optional braces really become optional: I can remove them in the snippets above and I retain a valid program.

There is no indentation marker. It’s all normal syntax, with or without braces.

Then, you simply say that with itself is further optional in some situations. It’s all optional stuff. There’s no marker.


I don’t care about with itself. I care about the fact that we have 5 different ways of introducing blocks. I want to use the same thing everywhere. It could be @ or $ for all I care. I propose with because, of all the things I’ve seen over the past few years, it’s the one that IMO fits all the use cases without being misleading.

14 Likes

with currently means two things:

  • an intersection type, and that one is deprecated in Scala 3 in favor of &
  • as the connector to mixin several traits, and in that use case the additional with that then adds a refinement of a class/trait/object falls perfectly in line:
class Foobar() extends Foo with Bar with
  def myOwnMethod: Int = 42
4 Likes

In my categorization there are two to three different cases that open an indentation level:

  • A keyword, and with is one of those (we need to be careful right now with disambiguations since
    with is still used in too many other contexts, but that will sort itself out eventually.)

  • A :. Right now only allowed for classes and the like, under -Yindent-colons also allowed for
    method arguments

  • Nothing at all in two situations:

    • Old style control code such as
      if (x < 0)
        handleNegative()
      
    • Extension methods

Old-style control code: Will hopefully also be used less over time. In the meantime I believe it is better to allow indentation in order to decouple the question of significant indentation from the question what kind of control syntax is used. But I could be convinced otherwise for this issue.

Extension methods: I believe a : is actually misleading for them, and so are braces. But since it’s a new construct I hope that braces will not be used much for them. The reason a : would be misleading is that
in

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

we really define extension methods at the top-level, not in some nested scope. So I think it’s better to make that clear by omitting the :.

1 Like