Make "fewerBraces" available outside snapshot releases

Yes but again I think that’s just like } {.

Both things can be true–you can be motivated by one thing, but something else can be the main argument for it once it’s there. groupMapReduce has improved safety and ergonomics, which is a reason to use it even when performance isn’t relevant.

2 Likes

Sure, but the functionality is important, and every other definition was worse in Scala 2, and still might be worse in Scala 3 (we’ve had less time to explore the space of reasonable syntax).

The reason, in this case, is that the zero is very often very short (can fit in the same line).

If you try to define

def fold[Z](zero: Z, op: (Z, A) => Z): Z = ..

then you end up with a variety of ugly, confusing choices about how to manage the parameters:

// Current definition, prior to fewerBraces
xs.fold(0){ (z, a) =>
  val temp = zeeify(a, "Some message")
  temp mergeInto a
}

// Two parameters, one block, style A
xs.fold(0, (z, a) => {
  val temp = zeeify(a, "Some message")
  temp mergeInto z
})

// Two parameters, one block, style B
xs.fold(
  0,
  (z,a) =>
    val temp = zeeify(a, "Some message")
    temp mergeInto z
)

The downside is that when the zero gets long, it’s ugly. Except making it ugly is a choice. You can always fix it.


xs.fold{
  val something = whatever()
  createZFrom(something)
}{ (z, a) =>
  val temp = zeeify(a, "Some message")
  temp mergeInto z
}

// Look, we can use a variable!
val zero = {
  val something = whatever()
  createZFrom(something)
}
xs.fold(zero){ (z, a) =>
  val temp = zeeify(a, "Some message")
  temp mergeInto a
}

// Oh noes, xs is really part of a long fluent chain!
complicatedFluentChain.pipe{ xs =>
  val zero = {
    something = whatever()
    createZFrom(something)
  }
  xs.fold(zero){ (z, a) =>
    val temp = zeeify(a, "Some message")
    temp mergeInto z
  }
}

If you try using a transient object to name the parts, it looks fine:

xs.fold
  .zero(0)
  .acc{ (z, a) =>
    val temp = zeeify(a, "Some message")
    temp mergeInto z
  }

and you can collapse the .zero onto the same line as the fold if you want.

However, setting this up is a pain, and it makes you remember three names in place of one, which is also a pain, and it’s hard to arrange so there isn’t a performance penalty (because you have to store two arguments), which is also a pain.

Maybe the cleanest way is symbolic:

(0 /: xs){ (z, a) =>
  val temp = zeeify(a, "Some message")
  temp mergeInto z
}

:crazy_face:

Anyway, it’s all well and good to hope for better syntax, but in the absence of an actual proposal, how do we know it exists?

For the most part, I find that multiple parameter lists are used for good reason in ways that make things easier, including fold and groupMapReduce.

3 Likes

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?