Scala 3 significant indentation

I think the name “optional braces” is a bit misleading, because it might be read as making all braces optional. But actually, the proposal only makes some braces optional, while other braces are still required.

I’d be willing to endorse the rubric “some optional braces”, if we also advertised “some optional semi-colons”, because not all semis are optional. Depending, as some wise person must have said, on context.

2 Likes

Oh ok thanks, I guess I missed that. In that case what’s the long-term plan or motivation for this experiment? Is it that if i works out well and is favoured for many years, that braces might go away in 4.0 or something? Is the long-term vision whitespace default, braces optional like it will in 3.0+ and then one day the Scala stops calling it experimental? Is it literally just an experiment to see where it leads, no long-term vision in mind? Maybe it’s just me but I’d appreciate a bit more context to understand this better.

4 Likes

Yes, maybe “Sometimes optional semicolons” is more accurate.

On the other hand, it is different in practical terms. Your example with the +17 is a bit strained, because the +17 serves no purpose.

In my code, I’m not sure if I have even a single semicolon, but there are plenty of places that cannot be written without braces, for example I have methods that look like this:

def m(f: A => B)(g: C => D): R = ...

and then are typically called like this:

val result = m { a =>
...
}{ c =>
...
}

and as far as I know, the above call cannot be written without braces.

I guess they can do the recursive Fibonacci numbers in CS 101 without braces, but I don’t think that should be the standard.

3 Likes

Braces would never go away, just like they never went away in Haskell and just like semicolons never went away in Scala. If the syntax is released as part of Scala 3 it won’t be experimental, it will be the default recommended way to do things.

1 Like

As the original proponent of a Scala whitespace-delimited syntax (https://github.com/lihaoyi/Scalite, circa 2014), I agree with @curoli that the standard that needs to be reached is “zero required braces”.

To me, “sometimes optional braces” to me is significantly worse than “never optional braces”. Imagine if Scala code had a mix of required and non-required semi-colons, or if Python had a mix of indentation syntax and require curly braces. Either would be awful.

I don’t even think it would be that hard to get to “zero required braces”. For example, my original Scalite prototype handles this as such:

val result = m do a =>
...
do c =>
...
}

Where the do keyword is a stand-in to open a block in all scenarios, which is closed on a decrease in indentation. You can look at the linked Scalite repo above to see many examples of it working.

Hang-ups over an arbitrary specific english meaning of do aside (that anyway doesn’t really apply to programming languages, e.g. haskell or coffeescript), having a specific, unambiguous indentation-block-start delimiter has a very large number of advantages. As a bonus do is currently an almost-unused keyword, and any existing usages of do{...}while(...) can be mechanically substituted with while({...; ...})(); (it looks even prettier with indentation syntax, as Scalite shows).

do could also be in addition to :, rather than replacing it. If we’re ok with having multiple keywords open indentation-based blocks in difference scenarios, adding another one to elegantly handle the multi-line lambda scenario doesn’t seem like a big deal.

4 Likes

With -Yindent-colons you can write this as

val result = m:
  a =>
    ...
:
  c =>
    ...

As long as there is an option to turn off this feature and maintain good ol’ bruce syntax, I don’t see a problem here. Well, as long as there’s a no-brainer way/tool to convert from one syntax to another. Copy-pasting code is still very useful.

Speaking of indentation, scalafmt and codebase migrations, it’s just so happen that at my current place of employment, we wish to move to scalafamt. Alas, most of our code has a tab size of 4 spaces, and scalafmt doesn’t support anything other than 2 spaces. This is intentional “as it is recommended in official Scala guide”.

I would hate it if we end up with a significant-indentation oriented tooling and ecosystem without enough support to maintain the current syntax.

3 Likes

Just my two cents. I tried optional braces syntax, and to my suprise, I really like it. I think it’s due to the combination of:

  • higher “signal-to-noise” ratio
  • saves a line with single }, allowing for more compact vertical formatting
  • { and } are a bit clunky to type (on a Norwegian keyboard)

Isn’t this exactly the same for optional semicolons? (There are places where you need them, unless you change the layout of the code.) Yet, we still call them optional semicolons.

2 Likes

As long as there is an option to turn off this feature and maintain good ol’ bruce syntax, I don’t see a problem here.

I do : introducing new people to scala.

lets start with the basics: scope, so a scope is defined by indentation, unless the project you are working on has disabled it so make sure to check the build config. Oh and of course as you navigate through the source code of libraries which the project use you might encounter brace using libraries in particular in scala 2 which doesn’t support significant whitespace notation. Also the current convention is to use braces here and here and here but not use braces here and here and oh yeah there is this : variant you may encounter once in a while but that’s not really used much outside of this specific part of the library ecosystem so you will be just fine.

And I can imagine project switching from one model to the other as a maintainer change leaving half the code using braces and the other half using significant whitespace.

Isn’t this exactly the same for optional semicolons?

Semicolon usage in existing codebases is almost zero except for extreme conciseness and I have yet to encounter someone actively defending using semicolons everywhere.

My personnal preference is for braces but I woud still take brace-less significant whitespace over a configurable mix of both without thinking twice.

3 Likes

Great, there is a possibility to write such code without braces. But at what cost?

  • Scala 3 now has at least three different syntaxes to be picked from via compiler options? That’s absolutely horrible.

  • It also horrifies me that all those type ascriptions where the type goes to a new line look like blocks now.

  • And note that the braceless version of my example actually has more lines and a line with a lonely colon. I thought the argument against brace syntax was that we want to save lines by avoiding lines with lonely close-braces?

So, we are looking for the best block start marker. Should it be colon? Or do? Come on, there is one block start marker that every programmer immediately understands, and it is free and unambiguous: open-brace. {.

That close-braces by convention take up a whole line on their own bothers me, too. But that could be solved by simply changing the convention.

7 Likes

Don’t forget <code> :smile:

Sounds like optional closing braces solves all problems.

If in all cases where changing the layout would mean you can remove the braces, calling them optional would be less controversial, I’m sure.

1 Like

Indeed.

But, the biggest mistake in my opinion was inherited by Scala and that was putting the opening brace at the end of the previous line. In my view this was a syntactic abomination. If the opening brace was at the beginning of the first line of the block and the closing brace at the end of the last line of the block then the braces wouldn’t add to file length.

I suspect the costs of optional braces will prove high, as the costs of option semi colons were not inconsiderable, leaving us at least for a while with the ugly compromise of hanging operators. Maybe I’m at fault but I’m still not clear what the specification for the compiler continuing a statement / expression vs starting a new one is.

I feel part of the problem in this debate (or lack of debate) is not acknowledging the importance of aesthetic. its totally reasonable that we as programmers care about the syntactic aesthetic of our code. That we don’t need to justify our aesthetic preferences, purely by reference to performative claims of comprehensibility, space, error rates, etc. Virtually all of us here give a large part of out lives to coding. so its not unreasonable that we should care about the presentation of our creations.

I can understand that some people really want to code using (much greater) significant white space and are willing to pay a significant cost to enable that, but I feel that can lead them to underplay those costs. I am willing to take some things on trust. Notably the Scala dot calculus. I’m willing to trust that this allows testing of type system soundness. When it comes to syntax, I see no evidence for trust in the soundness of the syntax overall.

2 Likes

I suspect very few Scala programmers would be willing to give up optional semicolons though.

I also don’t really understand the sudden obsession with file length (as in line count) in this thread. Sorry but you’ll never convince me that less lines is always better. Definitely not if your motivating example is

def foo =
  {...
  ...
  ...}

By the way, every Scala program can be written without a single newline…

6 Likes

Most of these points should have been raised, discussed, and addressed in an SIP before significant effort was put into implementing it. They were not because there was no SIP, which is the core problem. It’s been almost a year since these concerns were last raised, so what @odersky is working on now isn’t really relevant. We appreciate the work he’s doing, we’re annoyed at how this has been handled, both these things are true.

Now we’re in a situation where, not only has significant work been put into a feature that can be charitably called “incomplete”, but it’s having cascading impacts on other features, like “asless givens” - a term that I originally misread, causing quite a chuckle, so my thanks for that :smile:.

I don’t particularly like significant whitespace, however I would still be very uncomfortable if something I liked was handled the way this has been.

2 Likes

I prefer this:

def foo = {
// empty line
  ...
  ...
}

And that’s why I like what @RichType said:

its totally reasonable that we as programmers care about the syntactic aesthetic of our code. That we don’t need to justify our aesthetic preferences, purely by reference to performative claims of comprehensibility, space, error rates, etc.

With sufficient enough tooling, most styles could be enforced (in a given codebase). I’m not sure what’s the effort estimation for this kind of tooling though.

3 Likes

Hello strawman :smile:, literally nobody is talking about anything anywhere close to that.

I can’t speak for everyone else, but at least for me the size of the file is much less important than the size of the block, which is significantly less important than how easy it is to visually parse one block from the other.

These two may be identical for the compiler, but the one without the braces is much harder for me to read, and I suspect that’s going to be the case for a significant portion of the community:

def foo[F[_,_], G[_], A, B, C, D](fab: F[A,B], fbc: F[B, C])
  : G[
       Validated[NonEmptyChain[ErrorADT]],
       D
  ] =
    def aOrBinF
     : G[
          Validated[NonEmptyChain[ErrorADT]],
          D
        ] =
    fab.fold:
      a =>
       ...
    :
      b => 
       ...

    def bOrCinF
     : G[
          Validated[NonEmptyChain[ErrorADT]],
          D
        ] =
    fbc.fold:
      b =>
       ...
    :
      c => 
       ...

    for 
      dFromAB <- aOrBinF
      dFromBC <- bOrCinF
    yield dFromAB.combine(dFromBC)

def foo[F[_,_], G[_], A, B, C, D](fab: F[A,B], fbc: F[B, C])
  : G[
       Validated[NonEmptyChain[ErrorADT]],
       D
  ] = {
    def aOrBinF
     : G[
          Validated[NonEmptyChain[ErrorADT]],
          D
        ] =
    fab.fold { a =>
       ...
    }{ b => 
      ...
    }

    def bOrCinF
     : G[
          Validated[NonEmptyChain[ErrorADT]],
          D
        ] =
    fbc.fold { b =>
       ...
    }{ c => 
      ...
    }

    for {
      dFromAB <- aOrBinF
      dFromBC <- bOrCinF
    } yield dFromAB.combine(dFromBC)
}
3 Likes