Make "fewerBraces" available outside snapshot releases

I feel like the discussion has strayed further and further away from “should we have fewerBraces given that we have indentation-based syntax” towards “should we have indentation syntax”.

Whether people like it or not (and whether I like it or not), the ship for “should we have indentation syntax” has sailed.

Therefore, I would like everyone to refocus on the question “should we have fewerBraces given that we have indentation-based syntax”.

7 Likes

I think this is kind of victory through definition. The question “should we have fewerBraces?” is effectively impossible to tease out from “should we have indentation-based syntax” because the indentation-based syntax we have was not sufficiently thought through.

If you think that indentation-based syntax is a good change, then you’re going to want fewerBraces because of the ways the current implementation is has some really ugly inconsistencies even for people who want indentation-based syntax.

If you think that indentation-based syntax is not a good idea, then you’re not going to want it expanded, and my impression is there’s a lack of trust that fewerBraces is going to be any more comprehensively thought through than the current indentation-based syntax.

There might be some people who think that fewerBraces is unnecessary because they don’t see any faults in the current implementation of indentation-based syntax, and so don’t see a need for it - but they’re being really quiet about it, if they are out there.

2 Likes

Ok thanks for moderation; back to the issue: I think fewerBraces is very much in the spirit of pragmatic Scala. As I understand it, whether fewerBraces requires a language import or not, the braceful syntax still remains valid for eternity, and can be enforced by any organization with a formatter/linter if they like to do that.

There are many use cases for fewerBraces, esp. wrt embedded DSLs, esp. in combination with contextual abstraction and the new builder pattern with context functions.

I think Scala should allow fewerBraces as optional syntax, preferably already from 3.2, in the spirit of pragmatism, and thereby giving the choice of fewerBraces or not to the writer. I very much like to be in control of the layout of my code and use the options available in relation to what I (and my team, or my students etc) find most easy to read.

I’d also prefer if fewerBraces was on by default, but I can live with a language import similarly as scala.language.postfixOps, but I don’t think fewerBraces has the same risks as postfixOps.

use cases for fewerBraces, esp. wrt embedded DSLs, esp. in combination with contextual abstraction and the new builder pattern with context functions.

OK “show me the code” :slight_smile:
Here is an example of a DSL for slides that would be very much cluttered without fewerBraces IMHO:

@main def run = slides.toPdf()

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")

  frame("A slide DSL embedded in Scala 3"):
    codeFrom("slides.scala"):
      "frame" -> """frame("Background"""

The code that implements this DSL using contextual abstractions is available here:

4 Likes

I figured a way to solve some of the fewer braces problems by introducing a different block initiation marker (not :). You could do it with : too, but I don’t think it’d work as well.

It has to be a single character; I propose \. The rule to open a block is that \ has to be at the end of the line or at the following line indented with the same amount of whitespace as the previous line. In the latter case, any whitespace after the \ sets the new indentation depth.

This solves tricky cases like fold(f: A => P)(g: B => Q) because you write

xs.fold
\ a =>
  println(a)
  a
\ _.toA

If you wanted it at the end, that’d also be okay:

class Q() \
  def q = "q"

The > character would work well also. If we want something easier to pick out visually, # does the trick.

Another proposal of mine, .. or ... as the universal block opener, has the downside of not working well with 2-space indents, despite solving all the other problems (save familiarity).

Note that : could be used too; it’s just weird-looking.

xs.fold
: a =>
  println(a)
  a
: _.toA

If you wanted to enforce an end marker, an indented . on a line on its own would space things out reasonably.


I do think fewerBraces should be enabled. But I think it remains kind of broken, and something like the above is needed to rescue the problematic cases.

2 Likes

I would possibly put myself in this category, assuming that this is the proposed syntax then I find some of the examples to be quite intuitive and readable and other examples to look very confusing. I don’t particularly like the infix example and I find the fold example to be very unreadable.

So I think that I would prefer “slightly fewer braces” but think that the current proposal probably goes a bit far.

Anyway, just my 2 pence.

Rob

5 Likes

Completely agree with this. The multiple parameter list example probably shouldn’t be allowed in that form:

val firstLine = files.get(fileName).fold:
      val fileNames = files.values
      s"""no file named $fileName found among
         |${values.mkString(\n)}""".stripMargin
   :
      f =>
         val lines = f.iterator.map(_.readLine)
         lines.mkString("\n)

The infix one I think makes sense because you’re just using a block expression as the right hand argument:

credentials ++ :
   val file = Path.userHome / ".credentials"
   if file.exists
   then Seq(Credentials(file))
   else Seq()

I don’t feel like that is very harmful even though I personally don’t find it very attractive. One caveat here is that we already have significance for colons in infix notation like:

x +: xs
xs :+ x

so…maybe it’s not very smart to have that after all

3 Likes

I would not get hung up about the fold example. The point is, fold is already a very bad definition as is. One should never force the user to write two curly brace sections in sequence, that’s just bad style. Likewise for (...) after {...}. That’s weird code that’s hostile for newcomers.

So, there is a question whether we want to enable a : on its own line to make fold and friends go through, or whether we want to disable it and thereby braces mandatory for applications like fold. Either way is acceptable, because fold should preferably not be used at all.

In any case, we should not judge syntax on how it performs on non-idiomatic code. If syntax nudges you to avoid non-idiomatic code so much the better.

1 Like

What about groupMap and groupMapReduce, then — two methods added in Scala 2.13 too address a very common need? Are they also non idiomatic?

2 Likes

It’s an extremely common idiom in Scala. There are two reasons:

  1. To help type inference
  2. To make the syntax nicer when you accept a main function argument that is likely to have multiple statements, and other arguments too. By putting the function in its own argument list you can use curly braces without making the other arguments get lost in the noise. (This is often poorly termed “custom control structure syntax.”)

Scala 3 may make both of those reasons less valid, but the fact is lots of such APIs are out there.

How can you say that? Syntax has to be judged on how it performs in the real world, not some imaginary world. If the so-called non-idiomatic code were some beginner mistake and the syntax would guide those newcomers to write more idiomatic code that would be one thing. But this is just a recipe for increasing the association some people have of Scala with unreadable code.

4 Likes

How can you say that? Syntax has to be judged on how it performs in the real world, not some imaginary world. If the so-called non-idiomatic code were some beginner mistake and the syntax would guide those newcomers to write more idiomatic code that would be one thing. But this is just a recipe for increasing the association some people have of Scala with unreadable code.

Completely disagree. It was (and is) Scala’s syntactic freedom that unfortunately led people to design library functions that are ugly to call. It’s not a matter of guiding newcomers, the damage is already done by the library function. It’s a matter of nudging library designers not to do ugly things. And we could do better on that account.

Regarding groupMap and friends: Note that they are optimizations. The clean code would be

xs.groupBy x =>
     ...
  .map x =>
    ...
  .reduce (x, y) =>
    ...

We mash it together to

xs.groupMapReduce { x =>
  ...
}{ x =>
  ...
}{ (x, y)
  ...
}

which is offputting and unreadable. The library designer should try to make something like the original code work efficiently, and there are ways to do that (see lazyZip). Sure, it requires more implementation effort from the library designer, but the better user experience is worth it, IMO.

So, again, I am very zen about requiring braces for fold and groupMapReduce. It will simply make these functions stick out more, which will be an in incentive to come up with better designs.

3 Likes

In this case, I wouldn’t lay the blame on “syntactic freedom”. The reason we have methods with those signatures is because of properties of the language:

Type inference works left-to-right param list by param list, and if you want type inference to flow nicely for a single multi-lambda function, then you break it into multiple param lists. If type inference within a param list could work param-by-param, I bet we wouldn’t see so many of these curried methods floating around the ecosystem.

You can always work around it with method chaining, but that has its own issues, is tedious to implement, and it’s not surprising people reach for the easier thing to do, which is curried parameter lists. After all, curried parameter lists are built into the language, while method chaining is a user-land design pattern. Again, it’s the language encouraging these behaviors, and not all the blame can be placed on library authors.

If the “correct” way to write these APIs is via chained methods, then the language should make it convenient to def foo(...).bar(...).qux(...) a chained method just like it makes it easy to def foo(...)(...)(...) a curried method. Or if we think a single param list is the way to go, then we should tweak type inference to allow def foo(p = ..., bar = ..., qux = ...) to infer types like the curried version does.

Either of these would go a long way to give people alternatives to curried methods, which I agree are used far more commonly than I would like, (even in my own code, due to the above constraints!)

8 Likes

this makes me think that for these cases perhaps we should require named arguments for methods like groupMapReduce when using fewerBraces?

this is possibly not the way to do it but an example:

xs.groupMapReduce
  :key = x =>
    ...
  :f = x =>
    ...
  :reduce = (x, y) =>
    ...
7 Likes

groupMapReduce is not about the optimization. It’s about ease of use. Your two examples are not equivalent. The equivalent of

xs.groupMapReduce { item =>
  key(item)
} { item =>
  value(item)
} { (value1, value2) =>
  reduce(value1, value2)
}

with the proposed syntax, and using groupBy, map and reduce separately is

xs.groupBy item =>
    key(item)
  .map (key, items) =>
    key -> items
      .map item =>
        value(item)
      .reduce (value1, value2) =>
        reduce(value1, value2)

which is totally incomprehensible, and is why groupMapReduce was introduced in the first place.

Also, it is less obviously safe. reduce on its own throws an exception if there is no element. But in groupMapReduce we can guarantee that there is always at least one element in each group, by construction of the groups.

5 Likes

Actually, this is the first one that I like! Maybe we could even drop the : in front (though I don’t see yet how :thinking: )

5 Likes

Sure, but I still think that this layout is visually better:

credentials ++ {
   val file = Path.userHome / ".credentials"
   if file.exists
   then Seq(Credentials(file))
   else Seq()
}

because it makes scope of the block really obvious. There is always the choice to use a def to avoid the curly braces if that is really important to the author. E.g.

def user_creds =
   val file = Path.userHome / ".credentials"
   if file.exists
   then Seq(Credentials(file))
   else Seq()
credentials ++ user_creds
4 Likes

Yes, and my thought is that we could enable “:” for the basic case where it works well and explicitly prevent it for cases where it looks like it could easily cause more harm than good, which for me would include both the infix case and multiple parameter blocks case.

But I basically agree that having a function take multiple unlabeled blocks of code does not lead to good readable code, and I think that a solution that allows those blocks to be cleanly labelled would probably improve code readability.

On the fewerBraces page, I also wasn’t that convinced whether:

val xs = elems.map x =>
   val y = x - 1
   y * yy

Is always better than:

val xs = elems.map
  x =>
    // <some longer block of code>

or even:

val xs = 
  elems.map
    x =>
      // <some longer block of code>

Finally, on the page, is the foldLeft example missing a default, e.g., should it be:

xs.foldLeft (0) (x, y) =>
   x + y

How about using a keyword, e.g. then?

xs.groupMapReduce: 
  x =>
    ...
  then x =>
    ...
  then (x, y) =>
    ...

Wouldn’t the natural thing be to use end markers here?

We’re already used to } { meaning “end the previous block, and start a new one as the next curried argument”, so why not also allow spelling that out?

xs.groupMapReduce item =>
  key(item)
end item =>
  value(item)
end (value1, value2) =>
  reduce(value1, value2)

By the way, I was the one who originally proposed this method, and it was specifically for performance. groupBy is just a stupidly inefficient interface. It builds a mutable map of sequences, then rebuilds an immutable map of these from scratch. This is especially a problem because most uses of the original groupBy simply ended up iterating on the result and reducing the sequences anyway, making the intermediate collections completely pointless.

4 Likes

The idea is neat, but at the same time it feels weird that it keeps going after end

1 Like