Scala 3 significant indentation

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

I’m not sure how you could feasibly have orchestrated a SIP process around Dotty, given the need to iterate quickly and decisively. We’re talking about a completely greenfield compiler project that’s been underway for almost 8 years (if you really go back to it). The foundational semantics of the language and type system itself have been shifted to a completely different calculus. SIPs are and always were effectively out of the question except as a post-facto process.

My expectation would be that the SIP process will be observed for most changes within the mainline language path once Scala 3 final is released, while some more niche areas (e.g. match types) will likely undergo more rapid and less waterfall-oriented iteration.

Coming back to the significant indentation point… A SIP wouldn’t have changed anything. Significant indentation is always controversial, and a SIP would just be a way for the two sides to lob grenades at each other without coming to any non-dictatorial resolution. Not to say that SIPs are bad or that they should be bypassed for controversial things, but they aren’t a panacea and wouldn’t have changed anything about this particular language feature in the end.

4 Likes

I’m not sure I agree with that assessment. Yes, the foundations may have been rebuilt, but to extend that metaphor, the house on top looks very similar. We trust it’s sturdier, and there’s been work to fix a bunch of pain points. And there have been SIP (or SIP equivalent) threads on many of the changes that affect the facets of the language that developers interact with, just not this one.

That’s kind of a depressing take, to be honest. I’m not saying it’s necessarily wrong, only that I hope that this is not the case. I wouldn’t be agitating for it if I didn’t think there was the possibility of coming to some resolution everyone would be happier with than the current situation :man_shrugging:

If nothing else, this thread has brought up a bunch of things that, if this is something I’m going to have to live with, I would rather have fixed than not. I’m confident that, at minimum, the number of things that fit into that category would be reduced if this had gone through some form of community review.

3 Likes

Adding significant whitespace as an option goes against several stated goals of Dotty. http://dotty.epfl.ch/docs/reference/overview.html The ones I believe it breaks are

“Make Scala easier and safer to use.” Adding another syntax makes it more difficult to learn, read, and write Scala.

“Further improve the consistency and expressiveness of Scala’s language constructs.” Two different syntaxes are inherently inconsistent.

“Corresponding to these goals, the language changes fall into seven categories: …”
“(2) simplifications” having two different syntaxes to choose from is less simple.
“(3) restrictions” having two syntaxes is less restrictive.
“(4) dropped constructs to make the language smaller and more regular” two syntaxes makes the language larger

All this said, I have hope that since -noindent is opt-in, the community will largely not use it. EDIT: I misread that and it’s opt-out. wow

9 Likes

Experimentation is great and it would be terrible for Martin and co to have been burdened by the SIP process every time they want to experiment. I think everyone’s ok with the SIP process not being involved so early. My understanding was that between the experimentation and Scala 3.0 final being released, the SIP process would kick in so that people can both 1) understand the proposed changes, and 2) contribute to the proposal, usually to strengthen it. It doesn’t seem to be the case anymore. Even yourself is expecting that “the SIP process will be observed for most changes within the mainline language path once Scala 3 final is released” which doesn’t make sense to me because once it’s released we become slaves to backwards-compatibility (unless a feature is hidden behind a -Y flag) and major changes either become very slow to implement (eg. waiting 3 years for 3.1) or impossible until 4.0.

According to the more recent comments (kind of like rumour but by people in the in-group which wouldn’t be the case if we have an official SIP), when 3.0 is released significant whitespace is going to be enabled by default, and won’t be considered experimental. Once that happens, a retrospective SIP would be very limited in terms of efficacy. It’s like me building a granny flat for myself in your backyard, all the while saying “don’t worry, once it’s built and I move in, we can have a discussion about it”.

I mean I’d actually be fine if Martin just said “our team gave this a lot of thought, we’re just going to do what we believe is best.” The mixed messages, lack of clarity and (what seems to me to be) contradictory behaviour is the problem. SIPs or no SIPs anymore? SIPs only for some? What’s the “some” criteria? If SIP for this then when? If post 3.0, why and how limited? Shouldn’t the Scala Center being publicly clarifying this because the current state doesn’t seem to match the process last publicly declared by the Scala Center?

6 Likes