Make "fewerBraces" available outside snapshot releases

@odersky This is maybe a bit tangential, but how hard would it to be to allow us to write

xs.groupMap(
  group = x => x * x,
  map = x => x.toString
)

IMO this looks far better than any of the alternatives presented, but we can’t do it because of type inference limitations. But you all are actively working on the compiler: how hard would it be to make type inference allow this kind of signature? Like just make type inference loop over the individual arguments rather than loop over the argument lists?

That seems to me like it would be the best outcome of all, and bypass almost all the use cases where multi-param-list callsites look ugly. The remaining use case would be for things like context managers:

withFileOpen("foo.txt", append = true): f =>
  f.write("hello")

In these cases, having multiple blocks is not common, and so using parens to group the earlier sets of arguments is fine. Even when multi-lined:

withFileOpen(
  "foo.txt", 
  append = true
): f =>
  f.write("hello")

While it looks a bit awkward, it’s no more awkward than the brace-wrapped equivalent today

TBH if we can fix the type inference issues, we might even be able to get away with deprecating support for multi-curly/indent-block function calls entirely, unless someone can bring up other use cases where they’re necessary. Otherwise it feels like we’ll never be able to make it look good, and such a limitation would fit right into Scala3’s philosophy of codifying observed best practices and restricting unwanted flexibility

4 Likes

The problem is there’s a tradeoff. Given an application

f(a, b)(c, d)

we use a and b to determine an overloaded variant (or a type instance) of f and then we use that variant to determine the expected types of c and d. If we used the same algorithm per parameter, we would have to determine the overloaded variant of f only by the type of a. Obviously, this would break some code.

We do have some refinements of the scheme where we suspend typing of a lambda without parameter types since that would cause an error. But even so, there’s probably a lot of code out there that would break.

2 Likes

Hah ad-hoc overloading rears its ugly head again.

I wonder if there’s some tradeoff that could work. E.g. if we do the per-param type inference for non-overloaded methods only. After all overloads are often discouraged, and already have limitations (e.g. around default params, type erasure, …) that are not present for non-overloaded methods.

Another possibility: we only need per-param type inference for scenarios where the expression type is un-inferrable, such as function literals without typed arguments. Those scenarios do not compile as is, so nothing should break? And anyway unknown typed expressions cannot participate in overload resolution anyway?

I’m just spitballing here, I don’t know the compiler implementation details w.r.t. what is or isn’t feasible. But as an end goal it would be lovely to be able to write the single-param-list groupBy call as written above. I think we all agree that we want fewer curried methods rather than more, and some kind of improvement here may help move us in that direction

2 Likes

IMHO overloading the meaning of : will only unnecessary raise the entry bar and uncertainty for newcomers and irregular polyglot developers, strengthening Scala’s perception as a complex language

4 Likes

I agree that the overloading of : is not ideal, but what do you propose instead ?
(I count abandoning fewerbraces as a proposition)

forgive me for my ignorance, as an everyday business scala developer I would prefer to have a simple rule of thumb: either I put my block of code in braces, or alternatively, I start a new line and increase the indentation, just this

2 Likes

for the basic case the rules are pretty simple, and even downright pleasant. it would be nice for this to handle every situation, but i would also be fine if it only was applicable to single argument clauses. the most important use case for me is using this for block enclosures with either no, or only implicit, arguments. im less concerned with chained applies, multiple arguments, and multiple argument clauses.

expanding the scope past the simple use case would be great for the perceived regularity of the language, but unless there is a clear winner syntax wise, i would even be fine with a hard limitation to strictly this use case.

either way this would be a welcome change, and i cannot wait until business code can start using this syntax.

1 Like

fewer braces but more colons, not exactly a win

3 Likes

fewer braces but more colons, not exactly a win

I think it actually is a significant win:

  • it is more regular to have a colon (almost) everywhere when braces are dropped
  • colon + indentation is a way to, with much less ceremony than braces, open a block, for those who like the indentation syntax
  • it feels natural (at least to me), as it is similar to how we use colon in natural language (see the first sentence of this post :slight_smile: )
4 Likes

Yes, that might be feasible. We’ll have to try it.

1 Like

FWIW, I find the second-class status of overloaded methods one of the most enduring pain points in my usage of Scala. I generally try to avoid features that break on overloaded methods, and when they’re just too good to avoid (e.g. default arguments), I nonetheless end up with all sorts of ridiculous repetition like

// Yay!
def display(f: Foo, prefix: String = "", copies: Int = 1) = ...

// Uh-oh....
def display(i: Int) = display(i, "", 1)
def display(i: Int, prefix: String) = display(i, prefix, 1)
def display(i: Int, copies: Int) = display(i, "", copies)
def display(i: Int, prefix: String, copies: Int) = display(i, prefix, copies)

// Oh no...
def display(s: String) = display(s, "", 1)
...

More features that don’t work with overloaded methods just sound to me like more features that should be avoided if at all possible in the interests of consistency.

(Yes, I’m aware that one can solve the problem as I’ve written here using transparent inline methods e.g. to dispatch to methods that redundantly name the first type. Transparent inline methods are awesome! But second-class overloaded methods are not awesome.)

1 Like

Yes, I guess the only way out is to eliminate overloaded methods. That’s what every language designer and compiler writer will tell you. The effort to make them work with everything else is heroic but often ultimately unsuccessful.

4 Likes

The effort is worth it, though! Even a partial success can be extremely beneficial for ergonomics. Naming is one of the hard problems of computer science, and admitting overloaded methods is a powerful solution: don’t force yourself to come up with different names when the types are unambiguous!

Having even second-class overloaded methods is one of the main reasons why Scala is still my primary programming language instead of Rust. Not as important as givens, but still, it makes some types of code much cleaner. (Don’t language designers also insist that you have to make typeclasses unique?)

This is rather an aside from the braces discussion, however, except as a rather suggestion to prefer braceless function call syntax that does not require further compromises to overloaded methods.

5 Likes

No, that’s just the Haskell folks (and Rust copied it). OCaml and Coq do it the Scala way with multiple possible instances.

3 Likes

I saw there is a related SIP here: https://github.com/scala/improvement-proposals/pull/45
The syntax to handle the “Handling Edge Cases” section in this SIP looks odd because now we need to use three ways (locally, (, .apply:) to handle these cases after we choose the : which is far from perfect though is short to write :.
Maybe we could consider the with or the @ proposal mentioned in this thread1 2?
It seems to me that we eventually will phase out the with usage in new Foo with Bar so I think using the with here might be a viable choice.
Also, I like with because it is similar to then, do, yield, or even match we use in other syntax.

2 Likes

The point is with the SIP proposal there is no new syntax. One can use locally and .apply: if one wants to avoid braces. I think this is vastly superior to designing new obscure syntax for some corner cases.

5 Likes