Make "fewerBraces" available outside snapshot releases

I think this is something I can get behind. It’s still a bit clunky going back and forth between one line and multi line syntax, but not that much worse than going between parens and braces today. Overall a reasonable amount of inconvenience that’s not worse than the status quo

Significant whitespace is something that has to be used carefully. Python does it right, Coffeescript goes too far, and Fsharp is kind of borderline. I think that one reason Coffeescript goes too far is that it attempts to replace both braces and parens with whitespace. From that perspective, avoiding replacing parens with whitespace in Scala seems like the right thing to do

7 Likes

IMHO you could expand this reasoning to multi line lambdas as well (and even by-name arguments for that matter). I don’t see why

xs.map x =>
  val y = x / 2
  x + y

is to be preferred over

xs.map( x =>
  val y = x / 2
  x + y
)

Especially when not viewed in isolation, but in a world where method calls can be chained.

Why should function or by-name arguments require different syntax than other kinds of arguments? Because of “user defined control structures” perhaps, but that went out the window a long time ago anyway. Even with : they are still distinct from actual built-in control structures.

2 Likes

One would normally use braces for this, not parens. So it’s a strawman argument. And it won’t work for cbn:

  xs.logged (
    println(1)
    println(2)
  )

will not work.

I have now re-organized PRs. PR Change fewerbraces to always use a colon, even before lambdas by odersky · Pull Request #15273 · lampepfl/dotty · GitHub implements mutli-line lambdas. PR Change `fewerBraces` to support inline lambdas after `:` by odersky · Pull Request #15258 · lampepfl/dotty · GitHub supports in addition single line lambdas. My current opinion is that we should go with #15273. Do the change, keep it experimental in 3.2 and lobby for adoption by default in 3.3.

3 Likes

My vote is also to go with 15273 requiring colon.
And I agree with the suggested: not allowing inline lambdas after colon (thus requiring newline after =>) as I think type-ascription should get priority when possible.

1 Like

In Scala 2 you would use braces because you needed braces around blocks and via a special rule ({ ... }) can be shortened to { ... }. I think Scala is pretty unique in that aspect. If you design a syntax without braces it may not be necessary to build on that but instead just avoid it.

Not yet, but it would if you allow blocks inside of parens.

That would be intriguing, but I believe it will not be possible for backwards compatibility. Traditionally (...) was used to suppress semicolon inference. I fear we can’t just undo this now.

OK, I think we are converging on 15273 as the solution. It’s the most conservative alternative in any case. In that spirit, I think we should also try to avoid situations where the “:” looks like line noise. Examples to avoid:

    xs.groupMap:
      x => x * x
    :
      x => x.toString

or

    m.size > 0 && :
      val firsts = m(0)
      firsts.length > 0

To that purpose, I propose a simple and quite restrictive lexical rule: A “:” can only start an indented block or lambda if it follows an identifier, a ")" or a “]” with no intervening whitespace between the two tokens. That will rule out the two uses above and would also prevent a few fancy DSLs that I can see coming otherwise.

Does it mean that I have to use braces for curried blocks and blocks as operands? Not necessarily. We can use indentation by adding explicit function names. In the first case:

    xs.groupMap:
      x => x * x
    .apply:
      x => x.toString

That works today. It’s currently not as efficient as two inline arguments since it performs an eta expansion of the part before the .apply. But we can fix this in the compiler with a peephole optimization.

In the second case, we can write:

   m.size > 0 && nested:
     val firsts = m(0)
     firsts.length > 0

nested would be an inline function defined like this:

    transparent inline def nested[T](inline x: T): T = x

We could also use locally, which exists today, but that one is not inline, so unlike nested it does leave a footprint in the code. I find nested a better name than locally, so we might want to add that to Predef and deprecate locally at some point.

But of course, it’s also fine to keep using braces in both of these cases. It’s just that if the tendency is at some point to avoid braces everywhere, there is a way to achieve that.

3 Likes

If we’re inventing identifiers, how about block:? Not every block is nested, but every nested block is a block. If it wasnt a block, and was just an expression, I’d argue we shouldn’t be using identation syntax at all for it. i.e. the following would be considered “bad”:

println:
   "hello"
4 Likes

This gets my vote.

1 Like

I don’t like the whitespace sensitivity of this proposal. Even though code with whitespace before the : is “always” bad, plenty of other code is always bad too, but we leave enforcement to the linter. Python doesn’t have whitespace constraints before :s, despite being indentation-delimited throughout the rest of the language. Very few languages have whitespace constraints around this sort of “punctuation” character

Can we accomplish the same outcome without whitespace constraints, by instead prohibiting non-alphanumeric identifiers before the :? This is also a restriction, but it feels a lot more natural, as many languages treat operators and identifiers differently so Scala having a similar distinction here would fit right in.

3 Likes

I am not sure if someone has proposed with before:

   xs.map with x => x + 1
     .foldLeft(4) with (x, y) => x + y

    xs.groupMap 
      with group = x => foo(x)
      with map = x => x.toString

    obj.foo 
      with a = bla, b = bla
      using cap1, cap2

If it is possible to write e with e1, e2, ... to mean e(e1, e2, ...), it seems to have the potential to (1) reduce the usage of parenthesis, (2) deal with long arguments(such as lambdas) and (3) works very well with named arguments.

However, I’m not sure it makes sense technically — disambiguation seems to be complicated.

4 Likes

Can we accomplish the same outcome without whitespace constraints, by instead prohibiting non-alphanumeric identifiers before the : ? This is also a restriction, but it feels a lot more natural, as many languages treat operators and identifiers differently so Scala having a similar distinction here would fit right in.

Yes, we could. On the other hand, we have already jumped the shark wrt whitespace sensitivity for infix operators.

  x
  + y

is parsed differently than

  x
  +y

I have not seen it, but also thought of it. I think it looks reasonable but is pre-mature for now. We want to find the most restrictive proposal that can get rid of {...}. We can always invent new syntax later. But we have to keep anything we add to the proposal when it goes out of experimental.

That’s true. We also have places where two newlines is different from one. We still shouldn’t aggravate the issue though if it’s easily avoidable without much downside

2 Likes

Big +1 to using with (or even with: if that makes anything less ambiguous). It’s already the case that givens using a dangling with to start a block (and I’m guessing that’s precisely because of the ambiguity with a type ascription?). It’s true that more places now use :, but it’s not obvious to me that the careful balancing required to get : => to work is any more “restrictive” than using with?

I think you might even be able to get x.map with foo to work (and mean x.map(foo)) where x.map: foo wouldn’t, and I think it even reads quite nicely.

Ideally, you might have both available in 3.2 and decide on which one to move forward with for 3.3?

2 Likes

Yes, I’m pretty sure it’s been proposed, maybe even a couple of times.

But I think it’s ambiguous in the presence of new:

// Does this:
new Foo with Bar
// mean
object Tmp extends Foo, Bar; Tmp
// or
new Foo(Bar)
// ?

Even if the language moves away from new and with in extends clauses, it’ll still look surprising and ambiguous to programmers who were already used to the old syntax.

What I proposed at the time was to repurpose @ in expressions, since it’s currently only used in patterns. It’s such a handy symbol and it’s easy to remember it stands for @pplication…

   xs.map @ x => x + 1
     .foldLeft(4) @ (x, y) => x + y

    xs.groupMap 
      @ group = x => foo(x)
      @ map = x => x.toString

    obj.foo 
      @ a = bla, b = bla
      using cap1, cap2
3 Likes

True. I think with: doesn’t have that problem though?

I’m not a fan of @, since I unavoidably read it as “at”, and xs.map at x => x is quite confusing

This might be something one can get used to, but it might deter new users ?

1 Like

Since with is a keyword, it feels weird to put colon after, like if we did:

if c then:
  foo
else:
  bar

It feels like we’re saying twice “there’s a block coming now”

1 Like

Very true. I don’t think it’s great, but I think the rules are already pretty confusing about what can begin a block (sometimes =, sometimes with, sometimes then, sometimes :, sometimes : =>, probably other stuff too). I’d personally rather be able to write xs.map with: x => x + 1 on one line or two than have the terser

xs.map: x => 
   x + 1

but not be able to write xs.map: x => x + 1. The combination with: is a little ugly, but it’s at least unambiguous.

The only reason for braceless style is prettiness. We should reject ugly unambiguous solutions.

I think requiring a new line is the cleanest solution for now, all things considered. I dislike the difficulty of refactoring from one-liners, but everything else seems worse. And it works nicely with case statements, which you would put on a separate line anyway (and don’t have to be indented).

So

xs.map
  x => x + 1

xs.map
  x =>
  x + 1

xs.map
  case Some(x) => x + 1
  case _ => 0

Add : if you prefer; I don’t really have an opinion on that.

It doesn’t solve the multiple parameter blocks problem, but at least with foldLeft and so on, it works okay as long as you can open a block after parens:

xs.foldLeft(0L)
  (acc, x) => acc += longify(x)

Not too shabby. If you combine that with allowing a zero-indent : you at least would have a full solution.

eitherWithTwoArgFold.fold
: left => tryToRescue(left)
: right => allGood(right)

But it’s a little ugly, which brings me back to the beginning again…

8 Likes