Should we gather usage statistics about SIP-44 (fewer braces)?

fewer braces every day - sometimes still useful to use curlies around a long closure.

Other notes - i don’t get bothered if there is a mix of braces/no braces

2 Likes

I find it a bit strange that making syntax more similar to Python, one of the most used programming languages, especially among beginners and non programmers, in the age of AI, should make the code unapproachable for senior Scala developers. That makes me sad to hear.

I think Scala’s way of doing this is superior even to Python, because you cannot error out on the return type, the compiler holds your back all the way.

At scaladays non of the people I talked to complained about the new syntax. I have done training for Java, Kotlin and Typescript developers and everyone found that syntax nice to work with. They spent 0 time getting into it.

Of course, as everything, it takes some time getting used to, but for me at least, it definitively was worth it. Doing some research around how to use the syntax efficiently is welcome of course.

I wonder if these problems maybe could be more related to tooling ? In Elixir you get automatically the ‘end’ marker injected when you create a function or module. In python you can also get your IDE to enforce indentation correctly so you (almost) never have a problem with that. Maybe some experiments with optional configuration in metals could be better time spent. I encourage to test out vscode or Intellij on Elixir or Python and do a comparison.

I hope however, I will not have to spend time on such a poll.

4 Likes

That’s the thing about this syntax though - it seems to appeal to beginners and non programmers, but it’s just a gimmick that gets old really quick as your codebase grows.

In other words, we’re not beginners forever, and optimizing for “hello world” is just not it.

I will get that going at work. Do you have ideas for questions? I’m thinking to start with:

  • which do your prefer
  • has your view on this changed in the past? why?
  • free-form textbox to elaborate on your reasons

Apologies for digging up a month-old thread…

6 Likes

I will possibly have an opinion on this when I finally find time to migrate my code to Scala 3.

One thing though, wasn’t the whole purpose of the more “lightweight” syntax to attract more people to Scala? So far it doesn’t seem like that has worked. But let’s keep in mind that adding more rules that must be memorized will not increase our numbers.

2 Likes

One thing I mentioned on the original discussion is that indentation-delimited blocks really depend on you having enough indentation, and to that end 4 spaces is much better than 2. I think that if people try using 4-space indentation, a lot of visual confusion from using SIP-44 will go away.

Consider this example from @bishabosha ‘s recent work on Cask:

1-space:

package app
object MinimalApplication extends cask.MainRoutes:
 @cask.get("/")
 def hello() =
  "Hello World!"

 @cask.post("/do-thing")
 def doThing(request: cask.Request) =
  request.text().reverse

 initialize()

2 space:

package app
object MinimalApplication extends cask.MainRoutes:
  @cask.get("/")
  def hello() =
    "Hello World!"

  @cask.post("/do-thing")
  def doThing(request: cask.Request) =
    request.text().reverse

  initialize()

4 space:

package app
object MinimalApplication extends cask.MainRoutes:
    @cask.get("/")
    def hello() =
        "Hello World!"

    @cask.post("/do-thing")
    def doThing(request: cask.Request) =
        request.text().reverse

    initialize()

8-space:

package app
object MinimalApplication extends cask.MainRoutes:
        @cask.get("/")
        def hello() =
                "Hello World!"

        @cask.post("/do-thing")
        def doThing(request: cask.Request) =
                request.text().reverse

        initialize()

Of these examples, 1-space is definitely too little. 2-space is livable, but IMO pretty borderline. 4-space is where the indentation-delimited blocks become clear as day and easy to skim without a millimeter ruler or counting pixels. 8-space is perhaps a bit excessive. And this is a tiny trivial example, larger and more convoluted real-world examples would benefit even more from the additional indentation making scopes and blocks clear

Of course, 4-space indents have their own trade-off: more consumption of horizontal space. This can be mitigated in some many contexts, e.g. having match case or catch case indented once rather than twice, or taking full advantage of top-level definitions to avoid wrapper objects. There are probably more things can be done with language changes to allow users to minimize redundant indentation to compensate for the cost of using 4-space indents.

There’s a lot of reasons why indentation-delimited blocks may work or not work, but IMO the use of 2-space indents in Scala is a big factor towards “not working”: we can’t say we are following Python’s popular style if we don’t follow one of the core conventions, which is 4-space indents! But that is definitely solvable if we try using 4-spaces for indentation-delimited code, evangelize that style on docs and examples, and put some minor effort to compensating for the increased horizontal cost.

2 Likes

Agree. 4-space indents work better with “significant indentation”.
This is why I personally ended up with this option in my .scalafmt files:

indent.significant = 4

I.e. it makes scalafmt using 4 spaces for significant indentation and 2 for regular one. Not a perfect solution, but works for me.


That said, it is also worth to mention that “significant indentation” doesn’t seem adding any value to my code.

  • Does it make the code more readable? Not really. The regular curly-braced syntax may look uglier, but in fact it is more “eye-catchy”, if I may say so. It is easier to follow nested blocks when they are closed with special characters like curly braces.
  • Does it make the code easier to write? No, it doesn’t either. With curly-braced syntax I can just write and then hit “format code” and it’s done. With the significant indentation I have to put more effort to keep my code properly formatted all the time while I’m writing. In fact, I spend more time formatting my code manually than I used to with the old style.

So perhaps the ultimate goal for SIP-44 was to make code easier to both read and write, and I gave it a shot. But from my perspective, the goal is not achieved (if not made things a little worse).

All my personal perception, of course. I also assume that all mature projects are usually employ code formatting tools like ScalaFmt.

5 Likes

This is such an interesting suggestion. I have a codebase that very randomly uses significant indent or not, and just played around with this setting for a bit. At first glance, it makes significant indent much easier to read for me (as in, I can see the extent of scopes quicker).

This is also my experience. Tooling seems to have caught up by now, but the inherent ambiguity makes moving code around harder, as one always has to manually adapt indentation to match the scope of the new location.

(Side note: If only people would use tabs for indentation, then everyone could just set this as they find comfortable, without having to advocate for everyone to adapt)

As far as I can tell, Python only allows blocks within statements. That makes nested scopes much less common and much less desirable. Scala is a different language.

2 Likes

I don’t think it’s really a fair test to not have deeply nested code shown too, unless we are making the opinionated statement that deeply nested code is bad. Personally, I think it is an extremely effective way to delimit context, so all else being equal, I think deeply nested code (where the context really does change) is a good match to what is happening. Gratuitous nesting is bad, of course, but having a heading that introduces context followed by indented code in that context is great–it’s so great that this is how multi-level bullet lists are done even for non-coding tasks.

With that in mind, here is an edited bit of code that I wrote somewhat recently which I don’t think is at all unreasonable for Scala (I’ve left a bit out and rearranged a little so the example is shorter but shows the actual depth). The key idea here is that certain types of concurrent patterns work best if the sharing happens inside a class body because we then have a nice place to hang common data structures. So:

1 space:

abstract class Percolate(parallelism: Int, maxPermits: Option[Int] = None):
 abstract class Work(
  final val inheritsPermit: Boolean = true,
  final val isCore: Boolean = false,
  final val touch: Option[() => Unit] = None
 ):
  def work(): Array[Work] Or Err =
   rss match
    case Active(r) =>
     source.get().flatMap: 
      case None =>
       close(r).use: _ =>
        once := noop
        rss = Completed
      case Some(a) =>
       Err.Or:
        if a.isEmpty then
         if empty then
          Thread.sleep(1)
        ...
 

2 space:

abstract class Percolate(parallelism: Int, maxPermits: Option[Int] = None):
  abstract class Work(
    final val inheritsPermit: Boolean = true,
    final val isCore: Boolean = false,
    final val touch: Option[() => Unit] = None
  ):
    def work(): Array[Work] Or Err =
      rss match
        case Active(r) =>
          source.get().flatMap: 
            case None =>
              close(r).use: _ =>
                once := noop
                rss = Completed
            case Some(a) =>
              Err.Or:
                if a.isEmpty then
                  if empty then
                    Thread.sleep(1)
                ...

4 space:

abstract class Percolate(parallelism: Int, maxPermits: Option[Int] = None):
    abstract class Work(
        final val inheritsPermit: Boolean = true,
        final val isCore: Boolean = false,
        final val touch: Option[() => Unit] = None
    ):
        def work(): Array[Work] Or Err =
            rss match
                case Active(r) =>
                    source.get().flatMap: 
                        case None =>
                            close(r).use: _ =>
                                once := noop
                                rss = Completed
                        case Some(a) =>
                            Err.Or:
                                if a.isEmpty then
                                    if empty then
                                        Thread.sleep(1)
                                ...

8 space:

abstract class Percolate(parallelism: Int, maxPermits: Option[Int] = None):
        abstract class Work(
                final val inheritsPermit: Boolean = true,
                final val isCore: Boolean = false,
                final val touch: Option[() => Unit] = None
        ):
                def work(): Array[Work] Or Err =
                        rss match
                                case Active(r) =>
                                        source.get().flatMap: 
                                                case None =>
                                                        close(r).use: _ =>
                                                                once := noop
                                                                rss = Completed
                                                case Some(a) =>
                                                        Err.Or:
                                                                if a.isEmpty then
                                                                        if empty then
                                                                                Thread.sleep(1)
                                                                ...
 

I think the minor effort should go into syntax highlighting the indentation depths to make them clear via auxiliary cues (like shading), not recommending that people use Scala in a way that penalizes one of its bigger strengths.

But far more than that, I think we should give people a representative set of examples, so that if they’re choosing based on sample, they can decide “that looks fine” or “that doesn’t but I’d never do that” or “I do do that and I can immediately see how it isn’t going to work for me”, etc..

Edit–just in case someone doubts that there is a lot of syntax-highlighting space available, including terminal-compatible highlighting, here is something that is overkill unless the rest of your text is very vigorously highlighted, but illustrates how thinner indentation can be made visually more salient than four spaces:

3 Likes

I didn’t know about this setting! I’ve been using this since I switched to Scala 3 and have been very happy with it:

indent.main = 3

BTW, I use 2 in Java and 4 in Python, and I agree that 2 is not enough for significantl indentation.

2 Likes

@Ichoran the 4-space example looks a lot better if you avoid double-indenting match-case blocks, as I mentioned

2-space before:

abstract class Percolate(parallelism: Int, maxPermits: Option[Int] = None):
  abstract class Work(
    final val inheritsPermit: Boolean = true,
    final val isCore: Boolean = false,
    final val touch: Option[() => Unit] = None
  ):
    def work(): Array[Work] Or Err =
      rss match
        case Active(r) =>
          source.get().flatMap: 
            case None =>
              close(r).use: _ =>
                once := noop
                rss = Completed
            case Some(a) =>
              Err.Or:
                if a.isEmpty then
                  if empty then
                    Thread.sleep(1)
                ...

4-space, single-indented match-case blocks

abstract class Percolate(parallelism: Int, maxPermits: Option[Int] = None):
    abstract class Work(
        final val inheritsPermit: Boolean = true,
        final val isCore: Boolean = false,
        final val touch: Option[() => Unit] = None
    ):
        def work(): Array[Work] Or Err =
            rss match
            case Active(r) =>
                source.get().flatMap: 
                case None =>
                    close(r).use: _ =>
                        once := noop
                        rss = Completed
                case Some(a) =>
                    Err.Or:
                        if a.isEmpty then
                            if empty then
                                Thread.sleep(1)
                        ...

There’s still some cost to 4-space indents, no doubt, but I suspect such cost can be mitigated and the readability benefit is very substantial (as has been noticed by people in this thread)

1 Like

This can’t be said enough. Under the new context functions, let’s say you have code like

def someFunction(…): C1 ?=> C2 ?=> Result = {

  // you want to call into someOtherFunction, but you want to change the C2 context,
  // the explicit `using` syntax for calling it doesn't work nicely, because C1
  // comes first, so really you need to override the C2 that's in scope.
  // intuitively you'd try to do:
  //    val c2 = summon[C2]
  //    given C2 = modifyContext(c2) 
  // but it doesn't work because now there are two possible C2 in scope
  // and the definition of the val c2 is the one to throw a compiler error,
  // with "C" is a forward reference extending over the definition of c2",
  // so you are forced to introduce a nested lexical scope:

   val c2 = summon[C2] // mind the gap, the following blank line is necessary so
                       // the compiler doesn't think you're calling an
                       //`apply` method. And it has to be blank because a comment
                       // would still join it with the previous line

  {
    given C2 = modifyContext(c2)
    someOtherFunction(...)
  }
}

def someOtherFunction(...): C1 ?=> C2 => Result = ...

what do you propose to do here without braces? And even this situation is just so mind puzzling for a language that wants to have few orthogonal rules.

This is what locally is for:

locally:
  given C2 = ...

yes, it’s another thing to learn, but also, the need for it doesn’t come up very often.

2 Likes

and it’s another “multiple ways to achieve the same” wart. I quite dislike locally for this, specially since nested scopes is very common in the language family of which scala is a member of.

2 Likes

Zero-indent matches make it inordinately hard to figure out what you’re matching on, especially when there are multiple matches. It looks nicer, but is considerably less functional, which is a loss.

If you can get syntax highlighting for that (color binding, maybe, of the header line and the match statements), and get it ubiquitous, then maybe it’s worth considering. Otherwise

val buffer = Array.newBuilder[String]
val errors = Array.newBuilder[Throwable]
f1.open() match
case Right(data) =>
    buffer ++= data.toLines
case Left(error) =>
    errors += error
println("Finished with f1")
f2.openAll().flatMap:
case Right(data) =>
    buffer ++= data.toLinesWith(foo)
case Left(error) =>
    errors += error

is pretty hard to parse. So then you start adding non-significant vertical whitespace to try to keep things comprehensible:

val buffer = Array.newBuilder[String]
val errors = Array.newBuilder[Throwable]

f1.open() match
case Right(data) =>
    buffer ++= data.toLines
case Left(error) =>
    errors += error

println("Finished with f1")

f2.openAll().flatMap:
case Right(data) =>
    buffer ++= data.toLinesWith(foo)
case Left(error) =>
    errors += error

but now your indentation clarity goes down because your length-to-width ratio is worse, and your code per screen also goes down. And it’s harder to see what you’re matching over because an indent is a stronger visual cue than a gap above (because the gap above could be for many reasons, but the indent has only one purpose).

So it seems like an overall bad compromise to me. One could run usability tests and see, but from the standpoint of optimizing psychophysics, it seems a lot worse than thinner indents with styling for emphasis. Also, do you recommend writing folds like this?

f1.open()(
data =>
    buffer ++= data.toLines
)(
error =>
    errors += error
)

(Braces are okay too, but you don’t need them if you do it this way.) If you choose something else, the two forms have a greater visual disparity, despite basically being two ways to do the same thing.

Furthermore, is that how ordinary functions should be used?

val ys = xs.map:
x =>
    x*x + 3*x + 2

If not, it doesn’t seem to generalize very well.

3 Likes

I imagined ordinary functions would be used as follows:

val ys = xs.map: x =>
    x*x + 3*x + 2

There is only one correct way to do it: locally. We should start deprecating nested imperative blocks without it.

2 Likes

I would accept this once locally is implemented as inlinein Scala 3 library, otherwise it introduces lambda, complicating both byte code and exception callstacks.

Disclosure: I am not a fan of braceless syntax, I still use “old style” braces when writing Scala 3 code, and nested scopes look natural with braces.

1 Like

We should start deprecating nested imperative blocks without it.

I think I’ll stop mentioning the things I like and dislike about the language for my own sake :sweat_smile:

4 Likes

I’d argue if nested scopes go out, then the follow syntax should also be removed

  val someValue = {
     ... //some expression
  }

since it makes the language more irregular once it stops supporting expression blocks, and it would then look like:

val someValue = locally {
  ...
}

“Don’t tell Odersky” is the new “Don’t tell Mom”.

Another example (of deceptive braces) is braces for collective extensions, where people wanted a scope. “Where does my import go?”

(I’m a fan of locally for explicitness.)

4 Likes