Feedback sought: Optional Braces

While I personally like 3 spaces, I doubt other people would. Most IDEs and editors may allow you to configure 3 spaces, but it may still be headache to switch to 3 spaces for many. I don’t think Scalafmt even allows you to do this (not sure about this, though). Some may even think of it as the worst of both worlds. Scala has a convention of 2 spaces (or sometimes 4), and I really doubt changing the convention is not going to be a very popular decision.

1 Like

I don’t think Scalafmt even allows you to do this

that should be a solvable issue

Scala has a convention of 2 spaces

It also has mandatory braces for the time being :slight_smile:

I really doubt changing the convention is not (sic!) going to be a very popular decision.

I wouldn’t be surprised if the amount of backlash wouldn’t surpass that of this thread :slight_smile:

Sure, change is controversial, change affecting literally every single line of your code even more so. But Optional braces is a significant enough change to ask if our current ways of doing things meshes well with it. I looked at some code posted here and felt that visually better separated indentation would improve its readability. On the other hand though, with 4 spaces horizontal whitespace accumulates too quickly.

Might be worth checking what the sentiment is, and if the feedback is more negative than worth dealing with, then just leave it (There’s enough pushback to some changes already that needs being taken into account before 3.0 final, I’m sure reconciliating all the opinions already takes a ton of energy). Idk though… so far I have a like and your reply saying that you would personally like it.

2 Likes

What about using Tab, and let the IDE translate it to whatever number of spaces you see fit ? Indentation would have to be Tab, that would address the stray whitespace problem.

Let me summarize what I took back from feedback on details of the proposal

  • : at end of line is contentious. It was argued that it does not work well after ) because it’s too close to a return type. There’s also the “false friends” issue with : in Python. There’s partial overlap in that : is used in both cases to start the definitions of a class. But Python uses : everywhere whereas Scala 3 otherwise uses keywords to turn indentation on. Furthermore, if we allow : for function arguments (i.e. turn -Yindent-colons on), then we have a situation where Scala allows : but Python doesn’t.

  • It’s confusing to have so many keywords that start an indentation region. I’d like to turn this around, actually. In fact, any keyword that must be followed by an expression or template can start an indentation region. There’s no need to keep tab of which keywords do this - if you could have written ... KEYWORD {...} before, you can write KEYWORD, a new line and indented code now. This principle was not quite true before - two keywords were missing – but it will be true when https://github.com/lampepfl/dotty/pull/10861 is merged.

  • Discussions on indentation width. It was argued that code is more legible with higher indentation widths. This would not concern the language definition itself, which allows any number of spaces or tabs, but additional tooling like formatters.

  • We might want to be stricter with enforcing layout. Right now, the scheme is intentionally very liberal in order to accommodate as much existing code as we can without needing a mode switch. Over time we might want to tighten that.

8 Likes

I think this is a good summary. Any thoughts on how to deal with point 1? I know students like the colon, however quite a few experienced Scala developers in this thread clearly do not.

On another note, I think - based on this thread - there seems to be a small majority in favor of optional braces. However I don’t think there is any consensus on how to deal with higher-order functions. IMO every alternative proposed looks worse than just using braces/parentheses. Maybe we are trying to throw out the baby with the bath water here?

I don’t think that’s always true even with this PR, for example (and as discussed before in this thread):

List(1, 2).map(x =>
  val y = x + 1
  y
)

Does not compile (error expression expected but val found after the =>), but adding braces to match indentation does compile:

List(1, 2).map(x => {
  val y = x + 1
  y
})
2 Likes

Let’s analyze where we would need to replace : (including code permitted under -Yindent-colons):

  1. In a class, object, trait, or enum, to start its definitions.
  2. To demarcate a function argument which is not a closure.
  3. To demarcate a function argument which is a closure

I believe for (3), “nothing at all will” work, i.e.

xs.map x =>
  x * x

is fine. That leaves (1) and (2).

What to do for classes

We might give with another try. We use it already for givens, so in that sense it would be good for regularity to do the same for classes, traits, objects, and enums. I tried to re-evaluate my experiences from the course. I changed all the slides back to with and had a close look at them. The pushback against with might have been due to lack of familiarity. We were all used to writing braces, so having something heavy like with there looked unfamiliar. : was less jarring since it was less visible. But I think that’s something that might change once one gets more used to it.

What to do for function arguments

I think we could try do for function arguments that are not closures

E.g.

logged("file") do
  some
  code 
  block

or

inContext(new ForkJoinContext) do
  some code
  that takes
  execution context parameters

or

Signal do
  some computation
  that should be reified as a signal

I was against do so far since it feels imperative. And I still think it would not work well for closues. E.g.

xs.exists do x =>
  0 < x && x < 10

feels off for me. But if we disregard closure arguments since we have a better solution for them already,
what remains? When would we write {...} around a multi-line function argument that is not a closure? I believe in most cases it would be an argument evaluated by-name. If this was a complex argument that would be evaluated by value then it’s arguably better to just pull out the argument into a val. One exception to this is if we want to depend on evaluation order. I.e. in a curried function application, the first argument has some side effect that the second argument relies on. Or vice versa, the first argument reads some state that the second argument modifies. In all of these cases argument evaluation order matters, so we want to see the argument as an expression to be evaluated rather than as something equivalent to a simple value. Something lile evaluate or eval would make that clear without the imperative connotation, but do is probably close enough and feels not as clunky.

Are two keywords too complicated?

Arguably not. with and do play different roles. with introduces a list of definitions whereas do introduces an expression. Both of them are already used in these respective roles.

Possible Downsides

  • Too much “in your face”, “feels to heavy”. : is admittedly more quiet from a typographic persepective.
  • do could still feel wrong in some cases.
  • have to do some major changes to course materials (that affects mostly me and the people who work with me on this)

Possible Upsides

  • The language would feel more uniform.
  • We are 100% true to the idea of optional braces. Both with and do can be followed by either braces or an indented block.
  • less confusion with Python syntax.
  • no confusion with type ascriptions.

So, maybe this makes @lihaoyi and @sjrd each half happy since they were the ones proposing do and with here :grin:. What do others think?

4 Likes

Sure. But that’s orthogonal. We interpret neither newlines nor indentation insides (...). The reason for that is to offer an escape hatch for people who want to use some layout that is not supported by the rules. E.g. in Scala 2, one use of parens would be to avoid semicolon inference in situations like this

(  someCondition
|| someOtherCondition
|| someFinalCondition
)

This one is no longer an issue in Scala 3, but there might be other situations where one might want to turn off semicolon or brace inference.

Then I don’t understand why this works:

List(1, 2).map(
  x =>
    val y = x + 1
    y
)
2 Likes

Changing class A: to class A with would change my reaction from “this is great!” to “this is awful”.

A colon here makes sense semantically. “This is class A: …”, or “here we define class A as follows: …”, and so on. with seems like we already have class A and then define something that is “class A, with additionally…”.

And it certainly shouldn’t be necessesary to use two keywords to define a class. Would be better to just go back to braces, then.

On the other hand. If we have:

 trait A:
  def foo: String

it doesn’t make quite as much semantical sense to reuse the colon to instantiate it anonymously:

val a = new A:
  def foo = "foo"

Here, with might be better:

val a = new A with
  def foo = "foo"

as it could be seen as “mixing in” the implementation.

Regarding do, IMO it’s the worst of all possible choices. It would be enormously confusing in the context of thinking about side-effects. Please avoid at all cost. :slight_smile:

I think this makes sense, yes. I think it addresses my objective/technical concerns.

I would add with on extensions as well, but I will take your new proposal without that amendment over the status quo.

2 Likes

I do understand your other arguments, but this one is actually not quite right. I get the class already by writing

class A

This introduces a new empty class by itself, not a label for what follows. So, writing

class A with 
  ...

introduces a class A with some additional definitions.

Yes! Let’s go with with.

And I guess we can do do.

Though this means the following should also be accepted, for consistency, which still looks weird:

xs.exists do
  x =>
    0 < x && x < 10

But thankfully that’s not a common existing indentation pattern anyway.

I do wonder if it will always be clear when used in multiple braceless block arguments, though:

foo do
  blah
  blah
do
  bleh
  bleh

// i.e.:

foo {
  blah
  blah
} {
  bleh
  bleh
}

In the braceless version above, it kind of looks the second do introduces an independent block.

1 Like

I see what you mean, and I have no doubt that you are technically correct, of course. But for me as a language user, it still reads like something like class _ extends A with, even if that is not what it means. I guess what I’m trying to say is that it’s counter-intuitive. And I think the syntax should make sense intuitively rather than just being technically correct.

@kavedaa - I think people have different senses of what is intuitive?

To me, : is dreadfully unintuitive, as it introduces a type, while with is quite intuitive because you already can do with Foo which gives you everything from Foo. So with { ... } must give you everything in { ... }. And if braces are optional, well there you go. It all makes sense. Just takes what you already know and extends it into something that you might have guessed already.

I do agree that do does not read well. (But I don’t think : reads any better.)

3 Likes

I was initially opposed to do, but I think it strikes the right balance between “noticeable enough” and “not too long.” About its imperative nature - Haskell uses do, and it doesn’t look unnatural (at least not to me).

with works well enough for me. It is indeed a bit heavy, but it seems consistent and makes intuitive sense to me.

I do think this looks quite good. However I am not sure how this will work with multiple arguments or multiple argument lists. E.g.

// do would be wrong here since it is just an expression
myCollection.foldLeft(initialValue) { elem => 
...
}

// Some result like data type that doesn't allow pattern matching
result.fold(
  error => doSomethingWithError(error),
  success => doSomethingWithSuccess(success)
)

// Or on a single line when its short
Option(1).fold("", value => value.toString)

In my experience these patterns are quite common, so I think they should be supported in the new scheme as well.

If with is chosen, what is the plan regarding this case:

Regarding do, it doesn’t look right in the anonymous class case:

trait KeyListener with
  def keyPressed: Unit

def addKeyListener(listener: KeyListener) = ()

addKeyListener do
  new KeyListener with
    def keyPressed = ()
1 Like

On second thought, I am having more reservations against do. do for me implies that something will be executed now, it’s an imperative after all. But there’s also the case where we just want to build
up a command, i.e. delay code. Signal construction falls under that, so Signal do seems off. Or, think of constructing a task graph

task do
  val x = ...
  val y = ...
  f(x, y)

I think that syntax sends exactly the wrong message. We do not “do” the statements that follow. They are treated as data here, to make up the task being defined.

An alternative might again be “nothing at all”. More precisely, one could allow optional braces after any operator. Here’s an example that comes from the compiler code:

          isMatchingApply(tp1)
          || defn.isCompiletimeAppliedType(tycon2.symbol) 
             && compareCompiletimeAppliedType(tp2, tp1, fromBelow = true)
          || {
             tycon2.info match
               case info2: TypeBounds => ...
               case info2: ClassInfo => ...
               case _ => fourthTry
            }

What is a good alternative to the pair of braces? Both do and : look strange after ||. But I don’t think we need anything here! The following code looks fine:

          isMatchingApply(tp1)
          || defn.isCompiletimeAppliedType(tycon2.symbol) 
             && compareCompiletimeAppliedType(tp2, tp1, fromBelow = true)
          ||
             tycon2.info match
               case info2: TypeBounds => ...
               case info2: ClassInfo => ...
               case _ => fourthTry

So, the rule would simply be that braces can also be omitted after any operator that appears at the end of a line and that is followed by indented code.

To address the case of general function arguments we still need an “apply” operator, which can be defined in a library. F# and some other languages use pipe operators <| and |> for this. I believe that’s workable in Scala as well. Alternatively, @ was proposed here and $ is used in Haskell, but I find these more cryptic. So we could define task as follows:

task <|
  val x = ...
  val y = ...
  f(x, y)

An advantage is that this one can be naturally chained, like any other operator.

someFunction 
<|
   firstComputation
<|
   secondComputation

This would look even nicer if we could create a ligature that merges the <| to look more like a left triangle. Here’s another example, this time from the Dotty parser:

    def constrExpr(): Tree =
      if in.isNestedStart then
        atSpan(in.offset) <|
          inBracesOrIndented <|
            val stats = selfInvocation() ::
              if isStatSep then { in.nextToken(); blockStatSeq() }
              else Nil
            Block(stats, unitLiteral)
      else
        Block(selfInvocation() :: Nil, unitLiteral)

Here’s the same code with braces and parens:

    def constrExpr(): Tree =
      if in.isNestedStart then
        atSpan(in.offset) {
          inBracesOrIndented {
            val stats = selfInvocation() :: (
              if isStatSep then { in.nextToken(); blockStatSeq() }
              else Nil
            )
            Block(stats, unitLiteral)
          }
        }
      else
        Block(selfInvocation() :: Nil, unitLiteral)

I think it’s worth investigating this further. To be sure, none of this is for 3.0. The only thing we need to decide now is whether to use : or with for classes and similar. But it’s good to know also for this decision that we might not end up using : for function arguments, after all

7 Likes