Feedback sought: Optional Braces

We could do one of two things

  1. Not use with for refinement types. Demand braces (that’s the status quo anyway).
  2. Disallow refinement types after given, the same as for parent types of classes or objects. A refinement type would have to be defined as a separate type alias or put in parentheses.

UPDATE: On reflection, it’s clearly (1): don’t use with for type refinements. We just fixed the error in Scala 3 that intersection and mixin composition used the same notation, even though they are algebraically different. Refinements behave like intersection, so with is off limits for them. If we want to allow an operator, it should be &.

2 Likes

It could allow customized end markers as in sh

task <| THE_END
  val x = ...
  val y = ...
  f(x, y)
THE_END

I’d like to help by contributing some empirical data from a quick poll I just posted in our closed Discord among my 130+ students who just finished their first semester of a 5 year program in Computer Science and Engineering at Lund University in Sweden, learning Scala as first language. I also invited my 15+ teaching assistants (older students) to vote.

The poll first shows this braceful code:

  case class Duration(nanos: Long){
    def +(other: Duration): Duration = 
      copy(nanos + other.nanos)
  }
  object Duration {
    def time[T](block: => T): (T, Duration) = {
      val t0 = System.nanoTime
      (block, Duration(System.nanoTime - t0))
    }
  }

And then gives these three alternatives to vote for as a quick click reaction in our Discord:
:one: Optional braces but add keyword with

  case class Duration(nanos: Long) with
    def +(other: Duration): Duration = copy(nanos + other.nanos)
  
  object Duration with
    def time[T](block: => T): (T, Duration) = 
      val t0 = System.nanoTime
      (block, Duration(System.nanoTime - t0))

:two: Optional braces but add delimiter colon

  case class Duration(nanos: Long):
    def +(other: Duration): Duration = copy(nanos + other.nanos)
  
  object Duration:
    def time[T](block: => T): (T, Duration) = 
      val t0 = System.nanoTime
      (block, Duration(System.nanoTime - t0))

:three: Optional braces, nothing extra

  case class Duration(nanos: Long)
    def +(other: Duration): Duration = copy(nanos + other.nanos)
  
  object Duration
    def time[T](block: => T): (T, Duration) = 
      val t0 = System.nanoTime
      (block, Duration(System.nanoTime - t0))

So far (after the poll has been out for less than 30 min in our closed Discord) there are already on a Sunday almost 30 quick answers (a hot topic :)). At the moment of writing :three: is the clear winner with just above 50% votes, :two: is a close second with almost 40% votes and very few (currently less than 10%) is in favor of with.

This is an indication of the first impression of students who have studied Scala for a semester (14 weeks of studies from September to December, exam in January). Around 75% had at course start programmed in other languages before (mostly C#, Java, Python) but non had coded in Scala before, and 25% had never coded at all before the course. BTW: Almost all of my students seem to just love Scala 2 after a semester of introduction. :heart:

I will have the poll open a bit more and report back during the day if more answers are given and if that makes a significant changes in the distribution pattern. I will also soon in a coming post here summarize my own subjective views from a teacher perspective after during the last months having tried with and without with in some of my code. Hang on. :slight_smile:

Update: After the poll has been out for just above an hour, the number of answers has increasd to 36 and the distribution is similar (with no token gaining traction): 53% for no token, 39% for colon, 8% for with.

13 Likes

Thanks for making this survey!

Unfortunately, :three: has been shown to be too ambiguous/error-prone.

I’d wager the popularity of :two: is due almost exclusively to people who are simply used to Python. But while it seems like a good idea at first, similarity with Python is probably more bad than it’s good, as most other indentation rules actually are very different from Python (notably, keywords like then being used to introduce indented blocks), so you’d have people confused about when or when not to use Python indentation style.

Finally, how advanced was the student’s exposure to Scala’s class and type systems? Did the student see mixin composition (which uses with)? Were they exposed to structural type refinement (also uses with)? These uses are in line with :one:, using with for adding members to a template definition, but the students probably missed this for lack of familiarity with the language.

5 Likes

The most common pre-knowledge languages of my students this year were C# and Java, in comparison not that many in Python, and some JavaScript but declining compared to previous years.

Yes, they have seen mixin of traits using with, but its not a big thing in the course, and only used in advanced examples during week 10 (of 14), where we discuss inheritance in-depth. They have not seen the cake pattern etc.

I think colon or no token is much “nicer to read” and this (non-scientific) poll indicates that first impressions of junior Scala devs might be in favor as little noise as possible…

My hope is that ambiguity problems with no token can be solved by stricter rules and good warnings from the compiler.

2 Likes

First of all: Thanks for considering this! I understand changing this would mean much work!

As I’ve said before, even though I would be very happy with with instead of :, I think where would be a better fit for starting template bodies. It makes a lot of sense language wise that you “define a class A that extends B with C where the member x is defined like this and y is defined like that”.

I’ve been playing around with this syntax, and changed the parser to accept where as an alternative to COLONEOL and WITHEOL.

In a different branch, I replaced : before template bodies with where. These are the files with the largest diff, and so the most extensive use of :

(Code doesn’t compile of course)

I think the main reason it might look weird, is that it’s new and unfamiliar. This is more a question of habit and when I’ve used it on the REPL with my modded compiler it looks beautiful with syntax highlighting.

I think one reason why with might have looked weird is that to me at least, since I’m used to with being followed by a constructor, this just looks like the author forgot to write the last trait constructor:

class A extends B with C with
  def x = ...
  def y = ...

Which is why I personally prefer this:

class A extends B with C where
  def x = ...
  def y = ...

Of course, this is a matter of taste, and it’s probably just a question of getting used to it. I do however want to again mention one advantage of using where:

Using where for begining template bodies leaves with free to be used for type refinements, which would make it possible to distinguish type refinements from a structural instance.

A structural instance could be written like this:

given righteousDude: Record with Dude where
  val name: String = "Frank"
  val age: Int = "57"
  val righteous: Boolean = true

An abstract instance with a refined type could be written like this:

given abstractDude: Record & Dude with
  val name: String
  val age: Int

And a simple alias instance like this:

given aliasDude: Record & Dude = richard
1 Like

I’d like to address this if I may. I asked some friends some week ago that aren’t Scala programmers, or at least less experienced Scala programmers. While my data set is nowhere near 150 students, I’d like to compliment your quantitative study with my qualitative.

Their first immediate reaction was that they liked : for starting indentation blocks.

Then I asked them what they thought : meant in all occurrences. Where it meant context bound, where it meant type ascription, and where it meant start of indented block.

It became clear immediately that they had not for a second tried to understand what the code snippet I showed them meant and only gone on aesthetics when deciding. It’s also relevant here that, if I have see the snippet with braces, and then compare it with other snippets, I don’t have to consider readability, I already know the answer!

When I asked them afterwards if they had changed their opinion, they all said that it was confusing that : meant both start an indentation block, and type ascription.

What I’m trying to say here is not that the opinion of 1 year students doesn’t matter, what I’m saying is that one must be very careful in measuring which syntax is “best”. An interesting study would be to give a complicated snippet of code written with different syntax to people and see how long it takes for them to read and understand it.

I also again want to point out that there are many situations in which multiple : meaning different things have been concluded to be awkward, confusing and ugly. It would be very unfortunate if the braceless syntax had such a big influence on other syntax decisions. Some influence is unavoidable, but I really think the cost of : is too high.

3 Likes

What about where:? Where without any indication of opening seems fitted for one-liners only in my mind. So you’d have

class A extends B with C where:
  def x() = ???

or

class A extends B with C where def x() = ???

As I have argued before, I believe the fear of ambiguities of : is overblown. The best counter-example is Python, where : is also used both as a type ascription and as an indentation marker. So we can argue many points for and against : and with but discarding : because of this alone won’t be convincing (at least for me).

I also want to focus the discussion on with vs :. I don’t think another alternative will have a realistic chance to be adopted at this stage.

  • where would be even heavier and less familiar than with, and its meaning is less of a good fit IMO.

  • Nothing at all (i.e. the student’s preferred choice) violates our rule that a single missing or added space should not change the meaning of a program. It’s also uncomfortable in situations like this:

         class C extends B
        
           /** Here is a doc comment
            *  spanning several
            *  lines
            */
           def f(x: String) = ...
    

    It’s here that two-space indent becomes uncomfortable since we have to squint our eyes to decide whether the class and the def line up or one is indented relative to the other. A : or with after B is reassuring in this respect: yes, what follows is part of the class. Interestingly, I have found it much easier to ascertain visually that two definitions line up than that one is nested wrt the other. The problem would be alleviated with larger indent margins but I am skeptical we can mandate that in the short run.

  • I could respond to other proposals in detail as well but I don’t think that’s a good use of our bandwidth. Let’s keep it between with and :.

3 Likes

Imagine like you have a list of stuff to buy and you go either

i need to buy an apple

or

I need to buy:

  • an apple
2 Likes

Here comes my personal views of optional braces from a university teacher’s perspective (apologies for long post, and I might now be spoiling my poll as I give the teacher’s preferences that might bias late answering students if they happen to read this):

I think optional braces will be a great improvement, as lagom (meaning well-balanced) conciseness can make code much easier to read for beginners.

I also think surface syntax is important in the sense that, ideally, optional braces should be exactly just that: namely optional; and thereby, in its literal meaning, be removable or addable without further change (or if that turns out to be “impossible”, with as few changes as possible).

When with was available as indentation token in older dotty versions, I gave it a serious try by porting significant amount of code and also write a good deal of new code from scratch, but I could never get used to with instead of braces. It looks bloated, feels non-intuitive, and I often forget to write it even after a great deal of practice.

Colon is much easier for me to adapt to and I don’t find it disturbing or bloated. I sometimes forget to write colon, and sometimes I even have written = after class and object instead of : when I get carried away in a coding flow, but that happens less and less often as I practice more.

I am strongly opposed to the with keyword after classes and objects; I can appreciate that, from intellectual argumentation on e.g. consistency, there may be advantages, but with just don’t fly for me, after writing a whole bunch of lines with it. So I really really hope, and keep my fingers crossed, that it does not re-appear as the final choice.

I have during the fall tried to follow the development of Scala 3 by trying out the new and changed stuff in my own code with focus on the stuff that will impact my students in introductory programming and my teaching material. Optional braces will impact almost all of my code, but I deliberately intend to blend styles in our course material, to make students comfortable with reading and writing both styles. I think as a community we must be prepared for and open to a “blended” style wrt braces, in mixed code bases, Q&A-sites, blog posts etc.

I understand and agree with the arguments put forward by Martin, not to restrict optional braces behind a compiler flag and to include this change in 3.0.0 and not later. But the implication of that is, IMHO, that the release of 3.0.0 should be delayed until this is settled on the best possible solution. And with is just not the best possible solution, IMHO.

There will be many fantastic new things in Scala 3 that will help beginner learners (including enum, top level defs, main-functions etc.). So let’s make optional braces intuitive, beautiful and concise.

POLL UPPDATE: after 5 hours since the poll was posted (sent to 134 students and 17 teaching assistant via Discord) there are now 41 responses:

93% want colon or nothing while only 7% (3 persons) want with

(The 93% is summed up by 51% nothing and 42% colon, but if nothing is not available, I believe the nothing voters would all go with concise colon rather than the longer with.)

7 Likes

Interesting feedback from your friends’ first impressions @mbloms, such indications can help understanding qualities of surface syntax. Yes colon is intuitive to many, probably because it signals “here comes stuff”.

I’m not at all worried about the colon after classes and objects being mixed up with return type and type ascription – it’s not just there you think of that, IMHO… You expect a template body of what’s “inside” the class/object there and then don’t think so much about other meanings of colon.

The problem is not at all that it’s confusing all the time. For example: in a definition for a top level singleton object with no parents, there simply are no ambiguities. My concern is purely that there are some situations where multiple colons, meaning different things are too close to each other. This looks weird:

class Foo:
  def bar: Bar = new Bar:
    def m: List[Foo] =  List(new Foo)

Sure, you could argue I constructed a weird example to make my point, but why do I succeed?

We also have the domino piece which started this whole discussion:

given [T](using Ord[T]): Ord[T]:
  def ..

There may be valid reasons to use with instead of : in given syntax, but “the indentation : looks weird next to the type :.” really shouldn’t be one.

It’s unfortunate that now, every time some new feature is discussed, the weirdness of different : meaning different things too close to each other is something that has to be discussed.

I think this is exactly the opposite of lagom. Lagom would’ve been to have something which works in every situation.

I would also like to add that the reason why I think a poll from students doesn’t really say that much is that the issues with using : aren’t things you see after looking at some snippet for 5 minutes. It’s much more subtle. And it’s only really an issue in more complicated code.

5 Likes

Thank you for the survey and the detailed feedback, Bjorn! I would like to go back and look at the points in favor for : and with again:

Points in favor of ::

  • Shorter, quieter, and often more pleasing typographically (exception: classes with complex parameter or parent lists, where closing with with works better)
  • Learners seem to prefer it
  • Visually less of a departure from current Scala 2 style

Points in favor of with:

  • The language grammar becomes more regular, and will be easier to explain.
  • The visual appearance also becomes more regular, at the price of more verbosity.
  • We are true to the idea of optional braces (since with can also appear in front of {, but we are
    not suggesting that for :).

One could summarize that in theory with has the better arguments going for it, whereas in practice, : seems to work better.

I believe that my previous “false friends” argument against : has become weaker. As long as Scala allows : in a well-defined subset of where Python allows it, I believe we are good. The problem would be worse if there are also situations where Scala allows : but Python doesn’t. But with our latest thinking on how to handle function arguments that looks unlikely.

1 Like

Does it really though? Every single argument in favor of : has been about ease of writing. None are about ease of reading. Am I really the only one that thinks the single most important factor in deciding the best syntax is readability? I have absolutely no doubt that : is easier to write, which is why people prefer it. But how is this more important than people who are actually going to read and understand the code later? Has it really been shown in practice to be easier to read code with :? Or has people been asked what they prefer to write?

I think there has been many examples that show that : doesn’t really work everywhere, even in practice. Is this really something that should just be shrugged off because it seems to work in most places?

6 Likes

I think colon is better for reading than with. If you read it as “here comes what’s inside” then it makes sense. In general colon indicates “here comes stuff”. Even in your example, I think it reads nice with “and inside new Bar we have method m”.

3 Likes

That’s the perfect definition of syntax: natural to write, easy to read.

1 Like

I personally favor the “skilled Scala software engineer re-reading the code” around 20 to 1 over the “skilled Scala software engineer writing the code”. I easily read code on a 1,000 to 1 basis over composing or editing it.

1 Like

Yes, agree, Martin. Thanks for the clear summary of points in favor of with and colon, respectively! I’d go with what works best in practice :slight_smile:

Thank you for the example! I think the most problematic aspect is the : after new Bar. I also find : least convincing in an anonymous class. Here are some possibilities to avoid that (I’m just brainstorming here).

  1. Disallow : in anonymous classes. Always require braces.

  2. Change the syntax to with but leave : for named classes. I.e.

    new Bar with
      def m: List[Foo] =  List(new Foo)
    

    This might not be as crazy as it sounds at first. We already use with for given instances, where we also create an unnamed class. So in a sense if we go with : for classes we already accept the inconsistency with given instances. We might then draw the line elsewhere, for instance between explicitly named and anonymous forms.

  3. Go one step further and also drop the new in anonymous classes. I.e. then it would be

    Bar with { def m: List[Foo] =  List(new Foo) }
    

    or

    Bar with
      def m: List[Foo] =  List(new Foo)
    
  4. Allow both : and with (but only with for anonymous classes).

1 Like