Feedback sought: Optional Braces

I emphatically disagree. In source files like this one it’s page up and down with type class and given instances, all use where:
https://hackage.haskell.org/package/base-4.14.0.0/docs/src/GHC.Base.html#Monoid

Look, I realize I’m probably already coming across as an obnoxious know it all here. I think all the work you and others have been doing for the last 8 years is fantastic. I also think indentation syntax in the end is inevitable. I feel convinced that it will be a change for the better if done right. I also understand that a lot of hours went into this, but I don’t understand how a change what will essentially dictate any future change to the language is not worth considering if another syntax would be better.

This is also a complete nightmare to me:

times(10):
  println("ah")
  println("ha")

This conflicts with the existing syntax

times(10): SomeType

and I don’t buy the argument that that debate is separate from this. : simply doesn’t scale.

I don’t mind a compromise allowing : for starting a template body for a foreseeable future if that makes things easier, but saying that : now has three different meanings until the end of life is… I don’t know what to say.

2 Likes

I know, and I think having with start a refinement-type-block makes a lot of sense. Sure you could write:

Foo &
  type T

But it looks a bit weird.
Having

Foo where
  defs

mean a structural type that extends Foo makes more sense to me than the other way around.

So maybe my guess why it works in Haskell and not in Scala was wrong. But our experience was still that it does not work in Scala. Maybe if Scala would have had “where” from the start it would look more natural now. As it is, it looked decidedly non-Scalaish to most people.

I guess = and all +=-like operators are part of the first category, as keywords?

Yes, and in my proposal all the uses stay within that category. Nothing special.

1 Like

I’ve played around with replacing : with where in some examples. I don’t think it looks non-Scalaish at all. Sure it’s a new syntactic construct, so it will take some time to get used to, but then by the exact same argument you could argue that the whole optional braces syntax looks non-Scalaish.

Using : in this way is fundamentally non-Scalaish. Haven’t you yourself said introducing something like
forall T . List[T] is non-Scalaish because it introduces new meaning to “.”? How is this any different?

4 Likes

I think we can still integrate this concern in my proposal by making the with optional for class/trait/object, the same way it’s optional for method application, and even for extension. In fact, that’s already exactly what it is in Scala 2!

class Foo() // optional 'with' removed here
  ... 
given Bar with // required here, always
  ...
extension (xs: List[Int]) // optional 'with' removed here
  ...
xs.map with // required here if we want the braceless block
  x =>
    ...
xs.map with x => // required here if we want to braceless block
  ...
xs.map(x => ...) // optional 'with' removed here

We showed earlier that the : was not required for disambiguation. We rejected the no-marker for class based on the concern that a stray whitespace would change the meaning of the code below. But IIRC we never actually ran into this issue in a way that didn’t cause a compile error to highlight the fact it was problematic.

And with applying strict indentation rules, like has been mentioned several times here, I think it will be even less of an issue.


I think this will be my last message here. I’ve said everything I had to say about this whole topic.

4 Likes

There are a few competing priorities when trying to decide on a indentation delimiter:

  1. Conciseness: this thing will occur almost every line of your program, so anything more than a few characters becomes unacceptably wordy
  2. Consistency: having fewer indentation delimiters makes it easier to parse code in your head
  3. Unambigousness: the indentation delimiter cannot clash with something else that may appear at/near the end of the line which already has some other meaning, whether to a computer or to a human
  4. Familiarity: While keyboards have tons of symbols (and unicode!) we don’t want our delimiter to look too alien to normal Scala code

Here is where various options stand:

  • Mixing-and-matching, which is the current approach, fails (2). I agree with @sjrd that having a wide range of ways of starting indentation blocks is bad
  • Purely using : fails (3), since we already use it for type ascriptions which sometimes may appear uncomfortably close to the end of a line
  • Purely using with fails (1); 4 characters is simply too wordy for such a common thing
  • Characters like @ and $ fail (4); it may look great in Haskell, but I don’t think it fits into Scala
  • do I think fits the above criteria, but the english-dictionary meaning isn’t perfect
11 Likes

What exactly were the arguments that this looks non-Scalaish? It looks very Scalaish to me, and during the time it was tried, reading code using it always seemed to make sense syntactically.

I have a small code base using indentation syntax heavily, including -Yindent-colons. (By the way, I love indentation syntax; it does make everything look much cleaner, and is easier to work with.)

I tried to see what things would look like with with by doing a global search-and-replace, and I quite like the results. Here are two examples.

Example 1

In this example, the use of : is quite suboptimal. It’s doesn’t really catch the eye, and visually merges with the token flow.

  case class Ctx(
    parent: Opt[Ctx],
    env: MutBindings,
    lvl: Int,
    inPattern: Bool,
  ):
    def +=(b: Binding): Unit = env += b
    def ++=(bs: IterableOnce[Binding]): Unit = bs.iterator.foreach(+=)
    ...
  object Ctx:
    def init: Ctx = Ctx(....)
  end Ctx

I like the version with with much better, as it clearly says “okay, I’m defining this class/object with these properties/methods”.

  case class Ctx(
    parent: Opt[Ctx],
    env: MutBindings,
    lvl: Int,
    inPattern: Bool,
  ) with
    def +=(b: Binding): Unit = env += b
    def ++=(bs: IterableOnce[Binding]): Unit = bs.iterator.foreach(+=)
    ...
  object Ctx with
    def init: Ctx = Ctx(....)
  end Ctx

Example 2

Colons fusing with the token stream is even more pronounced in higher-order functions (using -Yindent-colons). I really like using indentation syntax for these expressions, but it’s not as clear as it could be.

    val termAtom: P[Term] = parse:
      case s @ at('(', sp(s0)) =>
        term(s0).flatMap:
          case (t, sp(at(')', s1))) => t withState s1
          case (_, s1) =>
            break("unmatched opening parenthesis", s)
      ...
    val space: P[Unit] = parse:
      case s => text(s).takeWhile(_ == ' ').map((_, _) => ())

Using with, the start of indented function blocks seem more cleanly delineated.

    val termAtom: P[Term] = parse with
      case s @ at('(', sp(s0)) =>
        term(s0).flatMap with
          case (t, sp(at(')', s1))) => t withState s1
          case (_, s1) =>
            break("unmatched opening parenthesis", s)
      ...
    val space: P[Unit] = parse with case s =>
      text(s).takeWhile(_ == ' ').map((_, _) => ())

Also notice that it’s very nice being able to put with and function parameters together on the same line.

12 Likes

I’m completely sold on the with variant given your examples.

Can you or someone else try to come up with some counterexamples where with is confusing or wrong?

I’m okay with having : as a special indentation marker too. But I really like the general withness because of how regular it is. It’s extremely easy to understand; with no training I’m reading it as fluently as braces.

Colons take a little longer to learn to pick out.

Personally, I’m totally happy to move two extra fingers. (Colon is shift-; whereas with is four keys.) with also distributes well across the two hands, making it fast to type. It’s superior to where in this regard.

(It’s true that do is the same number of keystrokes as :, but I think the ill-fitting meaning is too great a flaw.)

5 Likes

The discussion veers into -Yindent-colons territory, but since it’s connected to what we do after classes, let’s go ahead. (although we really have discussed this at length already). The problem with any keyword connective is that it works sometimes and does not work in other places:

xs.foreach do
  println(_)       // great!

xs.foreach with
  println(_)        // wat?

xs.exists with
  _ > 0            // ok!

xs.exists do
  _ > 0            // wat?

: is neutral; that’s its strength

xs.foreach:
  println(_)        // works

xs.exists:
  _ > 0             // works as well

More examples where with would not work:

locally {
  ...
}
atPos(p) {
  ...
}
T {
  ...
}

In each of these cases the important thing is the block itself, the thing before it is a decorator. with reverses that relationship.

2 Likes

what about let instead of : or with ?

I would think one could get used to with even in your examples, though I don’t actually have strong feelings about the -Yindent-colons use cases. Using any separator other than : (which has two major drawbacks: overloading of meaning and impossibility to put on the same line) would be fine with me.

For the record, here is the example with @:

    val termAtom: P[Term] = parse @
      case s @ at('(', sp(s0)) =>
        term(s0).flatMap @
          case (t, sp(at(')', s1))) => t withState s1
          case (_, s1) =>
            break("unmatched opening parenthesis", s)
      ...
    val space: P[Unit] = parse @ case s =>
      text(s).takeWhile(_ == ' ').map((_, _) => ())

It doesn’t look that good, but it’s still better than colons IMHO.


In any case, I still think with is most appropriate for object, given, and class declarations, and that using colons there feels a bit uncomfortable. I could get used to it, but using with only for given and not for classes and objects seems very inconsistent.

4 Likes

I read it as, “xs.foreach with an argument of println(_)”. So it doesn’t look weird to me.

But your point is taken…I can at least see how it could be interpreted as weird.

I still think this is less weird than the weirdnesses with : given that we use it for type ascription, but since things already are this way maybe it’s an argument that it’s not worth considering a change.

This looks awful to me, way worse than colons. You should add an annotation as well, just to use every different form of @.

The only way this could be passable to me is if @ to bind a name in a pattern match was changed to as, and even then I think it’s not very attractive.

3 Likes

I was convinced by previous conversations that we would not allow indentation in this case:

if (x < 0)
  handleNegative()

or that it would be quickly deprecated.

We’ve had a pretty hard time trying to figure out all case in the Scalameta parser and we still haven’t solved all possible problems. This one is particular difficult since we do know now at all at the tokenizer level that nothing at all can start an indentation region.

This might have had a wrong assumption I had, but no one really responded to my questions in https://github.com/lampepfl/dotty/issues/9746

Another problematic case is else being on the same level of indentation as the indented block:

val kind = if true then
        1
        else 2

I raised it under: https://github.com/lampepfl/dotty/issues/10372

From what I’ve seen in the Dotty codebase indented tokens are actually added or really observed at the parser level, which is really difficult to do in the Scalameta parser as we need to also keep the Scala 2 syntax. The clearer the rules the more probable it is that we will be able to properly support Scala 3 syntax.

I think that at this point we will need to stop working on making the parser working with optional braces until everything stabilizes and rules become much cleaner. Or we might need some help figuring this out, because I’ve been unable to solve all the possible issues.

7 Likes

I can understand your logic here, but I still suspect that I will get this wrong, and naturally put in a “:” or “{}” because I logically think of them as a block of methods all extending the same class.

Hopefully the complier will spot “:” after an extension definition and give a helpful warning message if/when folk get this wrong.

7 Likes

Thanks for this, I think that’s why I always want to use a : there.

1 Like

How about using hash # to start blocks? A single hash is not a legal bare identifier and pretty rarely used, and (usually?) never at the end of a line.

Also, hash # looks like a rectangle, and blocks are usually oblong objects, or rectangles if projected to two dimensions, so there is a good mnemonic. :grinning:

We could keep allowing an optional :, at a loss of uniformity. Not sure about this one.

No, thank you, that’s okay, I think it’s easier to understand with just one approach. I personally prefer the :, it “feels right” to me right now, but I also see all the arguments above against using it.

When I saw @rgwilton’s post that made me think, “That’s why the : feels right to me, declaring a series of extension methods is like declaring a class, or more generally, like some sort of container of methods.”

3 Likes

I’m not sure I would like it, but one obvious candidate that was almost never mentioned is the backslash, a la bash. I think Visual Basic uses \ too. Although in Scala that could be a method name.

Speaking of which, one issue with “where” is it could be a method name. I think it was, in Squeryl?

If we require an indentation marker then it’s not “optional braces” it’s “choice of either braces or a marker + indentation.”

The only case where an indentation marker was actually needed is an empty class followed by indented sibling code. Technically that could be worked around by putting a semicolon after the class definition. Or maybe an extra blank line?

One way or another, I’d much rather have no special indentation marker at all, even for classes, and deal with this issue directly. If that requires modifying badly indented code – or disabling indentation mode for that file or project – so be it.

Regarding labeling it experimental, maybe it’s the wrong word but there should be some way to give it similar status to Scala 2 macros and still be able to use it in courses and recommend it. It’s just a disclaimer: “we still reserve the right to tweak this; only use it if you’re okay with possibly but hopefully not making some changes later.”

FWIW this practice of including features before they’re finalized has been used by Java in the last few versions.

3 Likes