Make "fewerBraces" available outside snapshot releases

OK, we can make an argument that SOME curried curlie applications are defensible. But they are a tiny majority, in my estimate less than 0.1% of all calls that could profit from indentation syntax. That means many programs will have not a single call like this. So, I would make the argument that for indentation syntax we can either accept that it will look ugly for these or require braces. In any case, that should not be the showstopper.

I have noted a recurring theme of tunnel vision in language discussions. We concentrate so much on the corner cases that we forget the bigger story.

1 Like

I’d love to see some data on this because these methods (particularly foldLeft) are frequently extremely useful. I can’t recall a program I’ve worked on that didn’t have them sprinkled liberally throughout the codebase.

Might be worth considering that what defines “corner case” will really depend on the style and idioms of a particular codebase. Maybe, just maybe, we focus on what you consider corner cases because they’re more common in the code we work with with than the code you work with, and there might be a reason beyond tunnel vision that we care about these details.

2 Likes

I may be the exception, but I use double-curried-lambdas a lot in my code bases. Beyond just fold and groupMapReduce, I define things like:

extension [A](self: mutable.Set[A])
  def setAnd[R](x: A)(ifSet: => R)(ifUnset: => R): R =
    if (self.contains(x)) ifSet else
      self += x
      ifUnset

so that I can write things like:

  visited.setAnd(v) {
    println("Already visited " + v)
    v.value
  } {
    println("Visiting " + v)
    visit(v) + 1
  }

I’d prefer to write:

  visited.setAnd(v):
    println("Already visited: " + v)
    v.value
  end:
    println("Visiting " + v)
    visit(v) + 1
4 Likes

Well both things could be true yes, but in this case only one seems to be. If you look at any of the discussions we had while designing and merging this features, it was always about performance.

Yes of course, I agree with the sentiment in the present conversation about coding style! Mine was just a historical remark.

1 Like

foldLeft works perfectly well with the proposed syntax:

xs.foldLeft(z) (x, y) =>
  ...

It’s when you chain multiple {...}s or (...) after {...} that you get in trouble.

1 Like

That’s true, but it cuts both ways – to me, much of this still feels like a tunnel-visioned attempt to rationalize and expand on what I still think was an iffy decision.

Now that more and more braceless code is out there and in my face, my overall position hasn’t changed: I still find it delightful for small code sections, but much harder to visually parse and interpret in blocks of non-trivial size. Completely braceless code is hard for me to read, pretty consistently. Scala 2 syntax may have been slightly “noisy”, but it’s generally clear and unambiguous – I don’t intuitively find the braceless syntax to be the same.

And it feels like folks are doubling down on it for the sake of doubling down on it, not because there is a coherent and demonstrable improvement: nearly all of the proposals here leave me scratching my head, completely unable to make heads or tails of the proposed code. Much of it literally isn’t comprehensible to me…

7 Likes

Can you elaborate on the nature of the trouble? (This is an genuine question. I’ve got students using a common custom HOF to do feedback control loops. It takes a handful of functions for arguments, and seems pretty solid in context.)

1 Like

I’m someone that really likes the indentation syntax in general, I think it’s wonderful to write in, and the option to add an end marker makes it for me pretty much a strict upgrade to brace syntax

Now that I understand better the proposal that is fewerBraces, I am somewhat strongly against it, I’ll expain those feelings in the following, by splitting two cases with different needs: Regular Scala, and DSLs

Regular Scala

I feel like this change actually makes the language harder to read and learn, for example:

// A
xs.foldLeft(0){ (x,y) =>
  ...
}
// becomes: B
xs.foldLeft(0) (x,y) => // for me, this reads as "(  xs.foldLeft(0) (x,y)  ) =>"
  ...
// but: C
xs.foldLeft(0){ (x,y) => ... }
// doesn't become: D
xs.foldLeft(0) (x,y) => ...
// as this is illegal

The difference between B and D is not at all obvious, it only somewhat makes sense when you remember the following is valid:

// E
xs.foldLeft(0)( (x,y) => ... )

On top of that, it breaks the normally valid “if it fits in one line, you can remove the linefeed” rule, that I take as a given
( “if it fits in one line, you can remove the linefeed, but you might have to add parens” doesn’t feels as good)

As a scala student, the thing that made me fall in love with the language is that there is always a simple and elegant rule beneath everything:

  • 1 + 2 is actually 1.+(2) so you can override it like any other method
  • {} just means "multi line ()" (at least in the context of lambdas)
  • “if it fits in one line, you can remove the linefeed” as discussed above

And then there’s fewerBraces: Sometimes indented suffices, sometimes you need :, sometimes you need parens instead

It’s not clean, it’s not scala !

DSLs

However, indentation syntax needs to be accessible from DSLs !
Therefore, we need a way for users to use indented sections as parameter blocs, hence:

My humble proposal:

An indented bloc starting with : is syntactic sugar for a regular {} bloc:

// P1
xs.map: x =>
  ...
// becomes:
xs.map{ x =>
  ...
}
// P2 The case where the indentation bloc is of width zero is also accepted:
xs.map: x => ...
foo()
// becomes:
xs.map{ x => ... }
foo()

Examples:

// E0  disallowed 
xs.foldLeft(0) (x,y) => 
  ...

// E1  allowed, discouraged ? -> R1
xs.foldLeft(0): (x,y) => ... 

// E2  allowed, discouraged ? -> R1
xs.foldLeft(0): (x,y) => 
  ...

// E3  allowed, even more discouraged ? -> R1
xs.foldLeft(0): 
  (x,y) => ...

// E4  allowed, discouraged ? -> R1
credentials ++ :
   val file = Path.userHome / ".credentials"
   if file.exists
   then Seq(Credentials(file))
   else Seq()

// E5 allowed and encouraged !
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")

// E6 allowed
extension (thing: String):
  def f = 2
  def g = "Hello"
// desugars to:
extension (thing: String){
  def f = 2
  def g = "Hello"
}

// E7 still allowed, of course 
// (I would be in favor of deprecating it in favor of E6, but that is for another day)
extension (thing: String)
  def f = 2
  def g = "Hello"

Potential Restrictions:

  1. :-blocs are discouraged outside of DSLs, as brace syntax is good enough for regular scala
  2. restrict where :-blocs are allowed, for example:
    2.1. not after another :-bloc, to avoid groupMapReduce issues mentioned above
    2.2. not after a {}-bloc, to avoid }:
    2.3. your suggestion here
    2.4. this seems easy to detect → potential for helpful errors
  3. potentially a different character than :, for example |
    3.1. : can create problems/confusion as it is also used for types, but already used with this meaning
    3.2. I see this as somewhat secondary to the proposal, comment on the rest first and foremost

Conclusion

For me, scala is a language of powerful, simple ideas

In my opinion, most of the negative view of fewerBraces (and potentially indented syntax as a whole) is a lack of regularity, which fails to make it simple

As is, I am not in support of fewerBraces, I think however with some changes it could be a very clean and powerful feature, and I provided a draft of what that could look like.

P.S: I tried to give names for everything, to simplify speaking about specific things, for example, if you want to talk about the fact that :-blocs are not allowed after a {}-bloc, you can simply refer to R2.2

4 Likes

33 posts were split to a new topic: On braces and indentation-based syntax

In the problematic case of:

// E8.0
foo(
  xs.map: x =>
    x * x, // <-- misleading comma here
  otherArgument
)

As is, this would not compile, as the comma would be understood as inside the body of the lambda
( I would wager it not compiling is a good sign)
If you really want this to compile with my proposal, here are two solutions:
(Do remember however that this would most likely be discouraged, as it is outside of DSL use)

// E8.1
foo(
  xs.map: x =>
    x * x
  , // not ideal, but at least closes bloc
  otherArgument
)
// E8.2
foo(
  xs.map: x =>
    x * x
  end, // also not ideal, but closes bloc even more clearly
  otherArgument
)
2 Likes

I agree with this. We need a regular syntax not a smorgasbord of special cases. If the block doesn’t begin with an indentation then it can begin with a colon on the same line.

Another benefit of this is I can get my wish to write the ternary operator in shorthand if this: … else that.

Note that would not work, as it desugars to the following:

if this {
 … else that
}

You can however already use:

if this then … else that

(For some reason, the “then” is not highlighted as a keyword)

A dangling else can’t appear in a block that has no matching if. The else should terminate the block. Parsing rules still apply (i.e. you’re not factoring in the governing if-block-else-block grammar parsing rule that was in play).

(Reposting because forgot to mark it as a reply to you.)

This is only true when the zero value is small, if you’re working with complex types, it can rapidly get out of hand. It also doesn’t address the bulk of my question, which is asking for context on why do don’t think these methods are particularly common.

I agree this is a mess because the .toSet are dangling in the braceless variant. I opine that this mess started with what I think was an incorrect design decision for braced block as function parameters when a lambda and not a Unit.

I would have preferred instead (which if had been done consistently then a bijection between braces and blocks would have enable automatic tooling to help those who can’t read braceless):

    val perCompanyOrNeutral: Set[AssetType] = Company.AllCompaniesAndNeutral
      .flatMap company => {
        companyDamAssetNames(company)
          .map (level, assetName) => {
            AssetType.Image(assetName, AssetPath(s"assets/dam$level-${company.stringID}.png"))
          } .toSet
      } .toSet

When there is chain of method calls, I argue the parentheses should be required because otherwise we’re repurposing braces to act as parentheses which is how Scala becomes so darn confusing with so many special cases. Please stop trying to be cute. This was the mistake Rust made also. Be regular in syntax and usage if you want to repair the reputation of Scala as an unnecessarily complicated language:

    val perCompanyOrNeutral: Set[AssetType] = Company.AllCompaniesAndNeutral
      .flatMap(company => {
        companyDamAssetNames(company)
          .map((level, assetName) => {
            AssetType.Image(assetName, AssetPath(s"assets/dam$level-${company.stringID}.png"))
          }).toSet
      }).toSet

In which case we can drop the braces, so the utility of the braceless style returns. Tada problem solved!

    val perCompanyOrNeutral: Set[AssetType] = Company.AllCompaniesAndNeutral
      .flatMap(company =>
        companyDamAssetNames(company)
          .map((level, assetName) =>
            AssetType.Image(assetName, AssetPath(s"assets/dam$level-${company.stringID}.png"))
          ).toSet
      ).toSet

I never have liked much Scala’s abstruse Haskell-like invocation of functions with spaces instead of parentheses. The feature can be useful rarely but I think should be discouraged in braceless style if we are indeed targeting the onboarding of junior programmers from Python. Function invocation with parentheses is more well understood. Okay there can be exceptions where it’s elegant to not employ paratheses so let’s not throw the baby out with the bathwater. But don’t get going in certain direction of attempting to minimize everything and be so cute that you tie yourself in knots and confuse the heck out of newbies. In short lean to regular syntax and usage please.

Due to unfortunate timing, I’ve not been able to receive a lot of feedback on my proposal to make fewerBraces more regular

In the meantime, I looked more into it, and it seems like it was originally like that, is this the case ?
If yes, why was it changed ?

And more generally, what do you think about my proposal ?

Why are you against not just using the => as legal at the end of a line that opens a new braceless block where a lambda is accepted at that position?

It’s somewhat irregular in the sense that it’s not as general as a rule that a colon admits that the rest of the line will join with a braceless block that follows. But it’s easier on the eyes and I have read that some people (e.g. dyslectics and those accustomed to colon as a delimiter for terms and definitions) get distracted by the colon. The colon preceding the lambda parameters is arguably symbol soup-ish? I liked the colon idea because it also enables for example a shorthand ternary with a colon in place of the then, but I am not sure I like that benefit more than I dislike the symbol soupness of the colon in front the lambda.

Were there other benefits of your proposal? Does your proposal enable the single line lambda without parentheses?

The thing is, I’m also dyslexic :sweat_smile:, and for me at least:

xs.map x =>
  ...
// reads as:
(xs.map x) =>
// (Of course, this makes no sense, but it's not the logical part that does the intuitive reading)
  ...

The reason being that => is a “strong separator”, whereas there’s no separator between map and x, by adding :, we explicitly add a separator, which makes the parsing way easier (for me):

xs.map: x =>
  ...

(I have the same issue with the extension syntax, hence why I proposed E6 and E7)

Of course the main problem is that : already has two really distinct meanings, one in typing, and the other in introducing “bodies”, so there’s only two options:

  1. Keep this ambiguity and adapt as much as possible (imo this spells trouble in the long term)
  2. Remove this ambiguity by using two different symbols (or potentially keywords, but I’m not in favor)

Finally, the if question, I’m strongly against this:

if c: a else b
//parsing as
if c {a} else b
//instead of (even tho this makes "no sense")
if c {a else b}
  1. The then is already a perfectly fine and flexible solution
  2. This would be an exception to my description of colon-bloc syntax, as we now need to lookahead to know where the bloc ends (see below)
  3. The above makes it harder to develop parsers for scala which affect both the compiler, and even more importantly tooling (for example a simple regex* is no longer sufficient to swap between syntaxes)
// cannot simply go forward until finding the first "else"
if c1: if c2: e1 else e2 else e3

*it would need to know about indents, but the language is regular modulo that

Yes, my proposal allows single line lambda without parentheses, see P2, E1

The other very important advantage is that it can become the rule for all braceless syntax with a very simple tweak:
"Braces can be dropped if

  1. the bloc is correctly indented, and
  2. the opening brace follows a keyword, or is replaced by :

"

(There’s probably cases I’m missing, do point them out)

3 Likes

Wouldn’t this be parsed as an attempt to cast c to the type a?

1 Like

This is sadly an issue, but even with:

xs.map: x => x
// identity function or cast to x => x

This is why I would encourage us to 1) move past the : symbol to introduce blocs, and 2) discourage braceless lambdas, as braced are clear enough (in my opinion, except for DSTs)

2 Likes