Make "fewerBraces" available outside snapshot releases

Regularly I have students that are color blind, have a diagnosis on the autistic spectrum, need silent rooms to concentrate, need visual aid due to sight impairment, hearing aid, etc. I have taught approaching 1000 students with Scala as first language since 2016 and last year almost 200 with optional braces and I have not yet observed a single “brace blind” student. (That does not mean they do not exist.)

My experience tell me that what students need in order to be fluent in reading scopes that are nested is a firm understanding of the concept of name scopes, shadowing etc. Once they are comfortable with those ideas the reading of code does not seem to be hampered by inferred braces, but for the students I have met instead increase readability, just like with inferred semicolons.

I have observed a significant effect of the influence of previous habits from those that have coded using mandatory braces before, but it seems to take around one week to get used to reading and writing code with inferred braces and the vast majority prefers them after getting used to them.

A small minority hang on to braces longer, but my understanding is that this small minority find braceful code more familiar or aesthetically pleasing, rather than not readable, and it is exclusively students with pre-experience with mandatory braces. I might investigate this a bit more the coming fall. That’s another reason I’d like fewerBraces so that we can work with them and investigate how the learning goes. Teaching is learning :slight_smile:

3 Likes

As someone who grew up in the 80s and 90s, and falls into 2 of the above categories, I can tell you that unless explicit effort is made to normalize a systemic difficulty, it’s all too easy to internalize it as something you are just too stupid or lazy to “get”.

This is incredibly hard to unlearn. It took reading your comment 5 times, and going back and forth on it 3 or 4 times, to decide to mention that having no line breaks makes your comment really hard to visually parse - and this is both a fairly common issue (particularly for people who are, like myself, on the autism spectrum), and relevant to the discussion at hand.

Point is, I wouldn’t put too much stock in your students not bringing this up to you. If they can’t read a program they’re supposed to analyze for an exam, they’re far more likely to write it off as a personal failure to handle exam stress, rather than approaching you about a visual processing issue that’s not really discussed often.

For context, my first 2 languages did not have braces (HyperScript and AppleScript use begin/end blocks) and I have no issues programming in Ruby, so this isn’t universally related to familiarity.

Braces themselves aren’t the issue, it’s the lack of explicit scope delimiters. Braces are just a common solution because they’re already in the language and begin/end blocks are awkward when chaining method calls that accept lambdas.

1 Like

Just added line brakes. Sorry for that. I also find my previous post easier to read now :slight_smile:

1 Like

Thanks for the pun! “Embrace” gets me every time.

I guess the new syntax after a while is

while b
do body

for the bonus pun.

Also thanks for the thoughtful conversation. I feel strongly that people deserve access to computer stuff as to music, art, literature, and clean water, food, and a warm bed.

5 Likes

Colon is a vambrace that protects us from bad scope, a bracelet shining as a token of bracelessness. </Non-concrete poetry>

2 Likes

My 2 cents …

I’ve worked extensively with Python, and with CoffeeScript. And have played with F# and Haskell, which also have significant white-space.

I never liked significant white-space because:

  • it can lead to situations in which the compiler gets confused, multiple interpretations being possible, as it happened in CoffeeScript; this is less of a concern in Scala, because it’s statically typed, and there are no instances in which you expect a function value and get a dictionary instead, but still, after all this time, the concern persists;
  • it can lead to the syntax becoming conservative; Python has not implemented multi-line anonymous functions, not just because of their culture, but also because it would cause trouble with existing syntax;
  • there are multiple ways to express white-space, see the endless spaces vs tabs debate, and you need smart good tooling to recognize this and do conversion on the spot, which is much easier when you have braces, and more difficult when that whitespace has meaning;
  • in Python, it led to very long lines of code, although this happened as a combination with other Python warts and practices;
  • in Python, you really need 4-space indentation, in order to visually discern where blocks begin or end; I remember experimenting with 2-spaces in my own code, and it wasn’t working well;
  • operations that are simple in Scala 2, like reformatting, or copy/pasting of blocks of code, become more complicated, increasing tooling dependence;

I don’t like that Scala switched to significant whitespace syntax, as I feel that Python had success not because of its significant syntax, but in spite of it. But then I don’t really care that much, as long as it’s properly implemented and a standard.

My wish is for the syntax to be clear, with no ambiguity. And preferably with the number of choices limited.

I don’t think we can say that the syntax is unambiguous, and Scala has been sliding more and more towards Perl’s TIMTOWTDI. We aren’t able to write in the Klingon language yet, but we aren’t very far; and it’s not just the new significant whitespace, but also the ambiguity of new keywords; like I’m still struggling to like given and using instead of implicit, and I can’t find a reason to, especially as both exist and usage of given yields surprises, but that’s for another day, another rant :slight_smile:

Either way, I believe that such experimental syntax changes shouldn’t be rushed, and it isn’t too late to retract them completely.

4 Likes

I feel like the discussion has strayed further and further away from “should we have fewerBraces given that we have indentation-based syntax” towards “should we have indentation syntax”.

Whether people like it or not (and whether I like it or not), the ship for “should we have indentation syntax” has sailed.

Therefore, I would like everyone to refocus on the question “should we have fewerBraces given that we have indentation-based syntax”.

7 Likes

I think this is kind of victory through definition. The question “should we have fewerBraces?” is effectively impossible to tease out from “should we have indentation-based syntax” because the indentation-based syntax we have was not sufficiently thought through.

If you think that indentation-based syntax is a good change, then you’re going to want fewerBraces because of the ways the current implementation is has some really ugly inconsistencies even for people who want indentation-based syntax.

If you think that indentation-based syntax is not a good idea, then you’re not going to want it expanded, and my impression is there’s a lack of trust that fewerBraces is going to be any more comprehensively thought through than the current indentation-based syntax.

There might be some people who think that fewerBraces is unnecessary because they don’t see any faults in the current implementation of indentation-based syntax, and so don’t see a need for it - but they’re being really quiet about it, if they are out there.

2 Likes

Ok thanks for moderation; back to the issue: I think fewerBraces is very much in the spirit of pragmatic Scala. As I understand it, whether fewerBraces requires a language import or not, the braceful syntax still remains valid for eternity, and can be enforced by any organization with a formatter/linter if they like to do that.

There are many use cases for fewerBraces, esp. wrt embedded DSLs, esp. in combination with contextual abstraction and the new builder pattern with context functions.

I think Scala should allow fewerBraces as optional syntax, preferably already from 3.2, in the spirit of pragmatism, and thereby giving the choice of fewerBraces or not to the writer. I very much like to be in control of the layout of my code and use the options available in relation to what I (and my team, or my students etc) find most easy to read.

I’d also prefer if fewerBraces was on by default, but I can live with a language import similarly as scala.language.postfixOps, but I don’t think fewerBraces has the same risks as postfixOps.

use cases for fewerBraces, esp. wrt embedded DSLs, esp. in combination with contextual abstraction and the new builder pattern with context functions.

OK “show me the code” :slight_smile:
Here is an example of a DSL for slides that would be very much cluttered without fewerBraces IMHO:

@main def run = slides.toPdf()

def slides = document("Scala 3 goodies"):
  frame("Goals"):
    itemize:
      p("Showcase cool new stuff in Scala 3")
      p("Help you get started with Scala")
      p("Illustrated by Scala 3 DSL (for these slides...)")
      p("https://github.com/bjornregnell/new-in-Scala3")

  frame("A slide DSL embedded in Scala 3"):
    codeFrom("slides.scala"):
      "frame" -> """frame("Background"""

The code that implements this DSL using contextual abstractions is available here:

4 Likes

I figured a way to solve some of the fewer braces problems by introducing a different block initiation marker (not :). You could do it with : too, but I don’t think it’d work as well.

It has to be a single character; I propose \. The rule to open a block is that \ has to be at the end of the line or at the following line indented with the same amount of whitespace as the previous line. In the latter case, any whitespace after the \ sets the new indentation depth.

This solves tricky cases like fold(f: A => P)(g: B => Q) because you write

xs.fold
\ a =>
  println(a)
  a
\ _.toA

If you wanted it at the end, that’d also be okay:

class Q() \
  def q = "q"

The > character would work well also. If we want something easier to pick out visually, # does the trick.

Another proposal of mine, .. or ... as the universal block opener, has the downside of not working well with 2-space indents, despite solving all the other problems (save familiarity).

Note that : could be used too; it’s just weird-looking.

xs.fold
: a =>
  println(a)
  a
: _.toA

If you wanted to enforce an end marker, an indented . on a line on its own would space things out reasonably.


I do think fewerBraces should be enabled. But I think it remains kind of broken, and something like the above is needed to rescue the problematic cases.

2 Likes

I would possibly put myself in this category, assuming that this is the proposed syntax then I find some of the examples to be quite intuitive and readable and other examples to look very confusing. I don’t particularly like the infix example and I find the fold example to be very unreadable.

So I think that I would prefer “slightly fewer braces” but think that the current proposal probably goes a bit far.

Anyway, just my 2 pence.

Rob

5 Likes

Completely agree with this. The multiple parameter list example probably shouldn’t be allowed in that form:

val firstLine = files.get(fileName).fold:
      val fileNames = files.values
      s"""no file named $fileName found among
         |${values.mkString(\n)}""".stripMargin
   :
      f =>
         val lines = f.iterator.map(_.readLine)
         lines.mkString("\n)

The infix one I think makes sense because you’re just using a block expression as the right hand argument:

credentials ++ :
   val file = Path.userHome / ".credentials"
   if file.exists
   then Seq(Credentials(file))
   else Seq()

I don’t feel like that is very harmful even though I personally don’t find it very attractive. One caveat here is that we already have significance for colons in infix notation like:

x +: xs
xs :+ x

so…maybe it’s not very smart to have that after all

3 Likes

I would not get hung up about the fold example. The point is, fold is already a very bad definition as is. One should never force the user to write two curly brace sections in sequence, that’s just bad style. Likewise for (...) after {...}. That’s weird code that’s hostile for newcomers.

So, there is a question whether we want to enable a : on its own line to make fold and friends go through, or whether we want to disable it and thereby braces mandatory for applications like fold. Either way is acceptable, because fold should preferably not be used at all.

In any case, we should not judge syntax on how it performs on non-idiomatic code. If syntax nudges you to avoid non-idiomatic code so much the better.

1 Like

What about groupMap and groupMapReduce, then — two methods added in Scala 2.13 too address a very common need? Are they also non idiomatic?

2 Likes

It’s an extremely common idiom in Scala. There are two reasons:

  1. To help type inference
  2. To make the syntax nicer when you accept a main function argument that is likely to have multiple statements, and other arguments too. By putting the function in its own argument list you can use curly braces without making the other arguments get lost in the noise. (This is often poorly termed “custom control structure syntax.”)

Scala 3 may make both of those reasons less valid, but the fact is lots of such APIs are out there.

How can you say that? Syntax has to be judged on how it performs in the real world, not some imaginary world. If the so-called non-idiomatic code were some beginner mistake and the syntax would guide those newcomers to write more idiomatic code that would be one thing. But this is just a recipe for increasing the association some people have of Scala with unreadable code.

4 Likes

How can you say that? Syntax has to be judged on how it performs in the real world, not some imaginary world. If the so-called non-idiomatic code were some beginner mistake and the syntax would guide those newcomers to write more idiomatic code that would be one thing. But this is just a recipe for increasing the association some people have of Scala with unreadable code.

Completely disagree. It was (and is) Scala’s syntactic freedom that unfortunately led people to design library functions that are ugly to call. It’s not a matter of guiding newcomers, the damage is already done by the library function. It’s a matter of nudging library designers not to do ugly things. And we could do better on that account.

Regarding groupMap and friends: Note that they are optimizations. The clean code would be

xs.groupBy x =>
     ...
  .map x =>
    ...
  .reduce (x, y) =>
    ...

We mash it together to

xs.groupMapReduce { x =>
  ...
}{ x =>
  ...
}{ (x, y)
  ...
}

which is offputting and unreadable. The library designer should try to make something like the original code work efficiently, and there are ways to do that (see lazyZip). Sure, it requires more implementation effort from the library designer, but the better user experience is worth it, IMO.

So, again, I am very zen about requiring braces for fold and groupMapReduce. It will simply make these functions stick out more, which will be an in incentive to come up with better designs.

3 Likes

In this case, I wouldn’t lay the blame on “syntactic freedom”. The reason we have methods with those signatures is because of properties of the language:

Type inference works left-to-right param list by param list, and if you want type inference to flow nicely for a single multi-lambda function, then you break it into multiple param lists. If type inference within a param list could work param-by-param, I bet we wouldn’t see so many of these curried methods floating around the ecosystem.

You can always work around it with method chaining, but that has its own issues, is tedious to implement, and it’s not surprising people reach for the easier thing to do, which is curried parameter lists. After all, curried parameter lists are built into the language, while method chaining is a user-land design pattern. Again, it’s the language encouraging these behaviors, and not all the blame can be placed on library authors.

If the “correct” way to write these APIs is via chained methods, then the language should make it convenient to def foo(...).bar(...).qux(...) a chained method just like it makes it easy to def foo(...)(...)(...) a curried method. Or if we think a single param list is the way to go, then we should tweak type inference to allow def foo(p = ..., bar = ..., qux = ...) to infer types like the curried version does.

Either of these would go a long way to give people alternatives to curried methods, which I agree are used far more commonly than I would like, (even in my own code, due to the above constraints!)

8 Likes

this makes me think that for these cases perhaps we should require named arguments for methods like groupMapReduce when using fewerBraces?

this is possibly not the way to do it but an example:

xs.groupMapReduce
  :key = x =>
    ...
  :f = x =>
    ...
  :reduce = (x, y) =>
    ...
7 Likes

groupMapReduce is not about the optimization. It’s about ease of use. Your two examples are not equivalent. The equivalent of

xs.groupMapReduce { item =>
  key(item)
} { item =>
  value(item)
} { (value1, value2) =>
  reduce(value1, value2)
}

with the proposed syntax, and using groupBy, map and reduce separately is

xs.groupBy item =>
    key(item)
  .map (key, items) =>
    key -> items
      .map item =>
        value(item)
      .reduce (value1, value2) =>
        reduce(value1, value2)

which is totally incomprehensible, and is why groupMapReduce was introduced in the first place.

Also, it is less obviously safe. reduce on its own throws an exception if there is no element. But in groupMapReduce we can guarantee that there is always at least one element in each group, by construction of the groups.

5 Likes