Make "fewerBraces" available outside snapshot releases

Overall, I think it’s pretty clear that a broad consensus about the details of what fewerBraces should look like doesn’t really exist. My feelings about the advisability of this aside, that’s probably a strong indicator that promoting it out of snapshot releases after only 2 minor versions would be premature.

Separate discussions about modifications to fewerBraces seem like a good idea, and though I’ll probably recuse myself from those discussions because it’s all equally incomprehensible to me and the ableist subtext of these discussions is frankly unpleasant to engage with, if this is the path Scala is headed down, you might as well do it right - and that means not rushing to publish something when advocates of fewerBraces can’t agree on what it should look like.

2 Likes

Nothing will change even if we continue for 20 more minor versions. We either do it or we don’t.

1 Like

I would hope that by 3.22.x there would be sufficient time to experiment with variations in the implementation that it would converge at a something which a strong majority of the fewerBraces advocates support.

It’s better to get the syntax adjustments done before trying to make it available outside the snapshot releases, and there have been a lot of proposals for syntax changes in this thread, so it seems like there’s still work to do here.

2 Likes

IMHO: It is very difficult to achive consensus when the rules too confused.
I have looked at python grammar(10. Full Grammar specification — Python 3.12.1 documentation).
It seems very clear. There are blocks and expresions.
When I am looking at scala gramma I cannot understad rules.
There are certain points, fortunately.
But it seems every new point increase dificulty non-linearly.
Is there any way to describe grammar more easy, without detaching indentation rules from main syntax?
May be easy documentation of syntax can help to achive consensus.

I think this has to be allowed, if it isn’t ambiguous.

The reason is that refactoring from a one-liner to multi-liner is important.

If I have the braces/parens already, why would I remove them just to go multi-line? It’s an extra hassle. It’s as easy, and less ambiguous, to keep them.

So, that means if we want to be able to write

foo: x =>
  bar(x)

we should allow

foo: x => bar(x)

Note that anything that works on xs.map should work on foo() alone, so I’ve listed that case to highlight the potential for ambiguity.

This is the troublesome one to my eyes:

val bippy = foo: x => f(x)
val quoxo = (foo: x) => g(foo)
val snyth = (foo: x => f(x))

In the first case you have function application; in the second, you have type ascription; in the third you have function application again.

This is why I yet again argue for ... or somesuch to generically open an indentable block. It’s completely unambiguous:

val bippy = foo ... x => f(x)

But, failing anything more adventurous like this, I think requiring the : is better than not.

I won’t likely be using the feature anyway, because of the visual ambiguity, but I think the regularity of having : is probably better. Still, since I’m probably opting out, don’t listen to me.

I do think it’s better to do something than nothing; I do think brace-free is great for short blocks as it is; and I never use the :-ending forms anyway because it’s confusing. Right now, that’s the only way to generate confusion, but if this also provides a way to be confusing, well, I can just not use this one too!

However, the lack of consistency in being able to elide braces is I think moderately worse than the inconsistencies and edge-cases introduced by a proposal like this. So I am in favor of something.

1 Like

We cannot, because of the ambiguity you noted. Also, I think I don’t want a general “apply” operator which : would become if we allowed this. I believe the analogous $ operator in Haskell was a mistake; it is used a lot and makes code less readable for non-experts.

We will need some careful engineering of error messages to correctly diagnose attempts to use : in this way. But that looks doable.

2 Likes

The surprising difference between

Seems pretty bad to me, very reminiscent of x += (y, z). To the extent that it is still an option, I would add a vote for required a newline after : as you mention above

xs.map:
  x => x + 1

It’s true that having the parameter on the same line is something Scala programmers are used to, but the whole point of this new syntax is to bring in newcomers. I think they would probably prefer to remember the simple rule that “: followed by newline begins a block, otherwise it is a type ascription”. I can’t tell from your post if there was any reason to abandon that option other than holding on to the old patterns in Scala. Another option is

xs.map x => :
  x + 1

which is either the best of the both worlds or the worst of both world depending on your aesthetics. I would take the regularity of “`:\n begins a block” in exchange for the slight ugliness. I don’t know if there’s an ambiguity in this version or not.

2 Likes

What difference? There should not be any.

foo: x =>
  bar(x)

and

foo: x => bar(x)

should be the same.

7 Likes

Maybe it could be surprising that

foo: x => bar(x)

and

foo: bar

are not the same because normally you can substitute x => bar(x) with bar.

Sorry, you said there was no ambiguity above and I didn’t internalize because I didn’t know about this:


def bar(foo: Int => Int): Int => Int = 
  foo: (Int => Int) // doesn’t compile without parens

Yikes, but I guess the wart is already there.

As others have said, the surprising behavior happens anew with

foo: arg
foo:
  arg

and in any case, the rules around when : means block are going to be surprising to folks for sure.

I agree that it’s much less surprising than I thought, sorry for missing that.

1 Like

Should

foo: bar _

Be accepted ?
(Question directed at everyone eventho it’s a reply)

I would say no, as it is not very useful and should be relatively easy to catch with an error

I agree, I don’t think it would be worth the trouble, and foo(bar) is perfectly fine IMHO.

I’m not convinced. That said, I agree that the byname/lambda or not distinction is useful, especially in conjunction with the custom control flow issue. In fact, we already went too far in that respect with built-in control structures. For instance, the condition in a if statement is not by name, hence we should not have removed the parentheses.

if
  val a = 1
  a > 2
then
  println("yes")
else
  println("no")
;

is only marginally better than:

val a = 1
if (a > 2):
  println("yes")
else
  println("no")
;

Same for while and for loops. If we decide(d) to stay with the parentheses syntax, custom function syntax is simple:

x.foldLeft(zero): (l, r) =>
  ...
;

as opposed to for instance:

x.foldLeft zero do (l, r) =>
  ...
;

There’s now a PR that implements the proposed scheme. Change `fewerBraces` to always require a `:`. by odersky · Pull Request #15258 · lampepfl/dotty · GitHub.

While the changes to the existing syntax are small and manageable, the changes to the parser were extensive. The commits in the PR give some indication what had to be done. The most tricky part was about distinguishing tupled expressions and lambda parameters. This is always hard for an LL parser.
The particular trick we used before no longer works with the new rules.

To see why, consider the lambda (x: Int => Int) => Int. We used to parse (x: Int => Int) as an expression (i.e. an ascription with a Typed node) and then converted this to a parameter ValDef when the second => was encountered. Because of this hack, we had to allow function types like Int => Int in type ascriptions inside parentheses, but not elsewhere, even though this relaxation is not backed by the official Scala syntax. With fewerBraces the first expression inside (...) would be parsed as an application of the form x(y => z), which would complicate things further.

Instead of parsing as expression and converting later, we now employ lookahead. We scan forward when a : ascription inside parentheses is encountered until we hit the closing parenthesis. If that is followed by => or ?=> we classify the prefix as a list of bindings and parse appropriately. There’s no need anymore to convert type ascriptions to parameters. This means we can now align parser and official syntax and always require that a function type in an ascription is enclosed in parentheses.

However, this change does invalidate some existing code. I had to patch 9 community build projects, which used the loophole in some code: akka, cats, http4s, monocle, play-json, protoquill, scalatest, scalaz, and zio. There were between 1 and 4 changes per project that had to be made. The need for change is clearly communicated. In each case you see an error message like
this:

2 |val x1 = (f: Int => Int)  // error
  |             ^^^^^^^^^^
  |             function type in type ascription must be enclosed in parentheses

I did not touch by-nameness, since there are also use cases where we want to use : in a call-by-value setting, and I did not want to change syntax and semantics at the same time. : will expand to braces, and will behave the same.

6 Likes

If it requires changes to that many major projects, I really think this needs a proper SIP…

4 Likes

Note that the changes are in projects that did not conform to the Scala spec but that the parser accepted anyway. The changes brought the projects in-line with the Scala spec. A SIP will be required, but since the SIP process is about spec and not compiler, not for this reason.

1 Like

That said, I agree the changes to function type ascriptions are the most contentious part. And I notice the problems arise only if we accept : lambda on a single line. If we always require an indent after =>, the ambiguity goes away in practice, and we could keep the previous parser behavior. Maybe a SIP could then try to spec that behavior, but that’s another topic.

So, what about we do not allow : lambda on a single line? I.e. this is OK:

  xs.map: x =>
     x + 1

But this would now be an error:

  xs.map: x => x + 1

The downside is that refactoring a lambda to be on a single line is something you want to do.

The upside is that we avoid the ambiguities with type ascriptions – which don’t apply formally in the grammar but do apply for the parser behavior and do apply visually. For instance:

foo(bar: baz => bam)

Is it a type ascription or a lambda? Yes, we often have upper/lowercase to tell, but it does feel fragile.

The other upside is that it would establish : firmly as a convention linked to indentation. I.e.

  transactional:
     send(msg)

cannot be replaced by transactional: send(msg), so why should we have a different expectation if transactional: is followed by x => or something like it?

In other words, fewerBraces is about dropping braces, not parentheses. Instead of xs.map: x => x + 1 you should just keep writing xs.map(x => x + 1). There’s no point in having multiple ways to do it, with no clear guidelines which is better.

I’ll refactor my PRs so that we can study both options.

6 Likes

This sounds promissing!
I think I might perhaps prefer to write either xs.map(x => x + 1), where I’m totally fine with parenthesis, or

xs.map:
  x => x + 1

if the lambda is a one-liner. Otherwise if the body of the lamba is multiple lines:

xs.map: x =>
  /* more lines here */
  x + 1
1 Like

(taken from tests in the PR)

val y = List() map: x =>  // error
  x + 1

Should this be an error ?
(it does look odd, but disallowing it breaks regularity a little)
I am not up to date on infix syntax, apparently it is discouraged, is this the reason this should be an error ?

Unfortunately it seems the ship has sailed on reconsidering whether including this is a good idea at all. Even though there have been various concerns regarding readability, consistency and yet more ways to write Scala that have not been addressed.

That said, if this has to go in, I would like to emphasize that

would be a show stopper for me. I don’t really like the colon in most places, but at least being able to go from one or multiple lines and back without re-adding or removing parenthesis/colon made the latest proposal better that the one before IMO.