Feedback sought: Optional Braces

I think it tilts the balance towards :. : at end of line means {...} are implied. When this was first discussed the main objection was that then we could not write a function like map with bound name on the same line as the map. Now we have a natural way to do so. => already opens an indentation region so no new convention is needed.

@odersky Nothing at all for lambdas simply does not work in at least two cases: by name params (or value params presented as block) and lambdas where the parameter list needs to go to the next line. And then there’s methods with multiple lambda parameters (e.g., folds of ADTs).

val tree = atPos(pos) {
  val x = ...
  x
}

val result = myFairlyLongListExpression.foldLeft(nonTrivialZeroValue) {
  (prev, elem) =>
    ...
}

val result = myOption.fold {
  val x = defaultValue
  x + 1
} { value =>
  val x = value + 1
  x * x
}

None of those can be expressed at all if there is nothing to introduce block lambdas.

Using : does not really solve at least the third case, and perhaps not the first case either.

7 Likes

I am grappling with it. I want to believe that with or where would work. But the fact was that I have been there before, changed everything to use them, and they didn’t work very well. And I still try to put my finger on why that was the case. At this point it’s probably too late to go against the evidence we have collected.

Well. I still don’t like this, and I’ve expanded too many times already why I think : after a term is confusing. To me, it simply doesn’t matter if there’s a newline after the :. I obviously parse code differently in my head.

I will be able to get used to : before template bodies, but for terms I think a keyword preferrably do makes more sense. do is also the same amount of keystrokes as :.

Absolutely love the quiet syntax! Braces are just noise to a typographer’s eyes, when horizontal and vertical spacing are sufficient to establish hierarchy. People’s eyes are way more powerful than old parsers.

Using words to denote the transition from a signature to an expression is not the best idea, as they muddy the water between real code and connectives; that is why people designed punctuation marks so that they stand out and are easy to find in one’s peripheral vision. So I hope with and where and such will be considered failed experiments. So do I for .. which is two characters, not one, and is seldom used to denote ranges in situations where using an em-dash is not visually distinct enough.

I find the choice of : very strange though, and inconsistent with the language which already uses = as the connective for functions. Using = to separate template signature from template body would only follow existing rules and open up the opportunity to get rid of the horrendous extends keyword.

def foo(x: Int): String =
  expr

class Foo(x: Int): SuperFoo & SuperBar(x) =
  expr

given [A: Ord]: Ord[List[A]] =
  expr

Now all one needs to understand the context of a definition is to look at the first word on the line.

5 Likes

@odersky, I would still really like to know (even if you prefer :) if you see any appeal at all in this:

I don’t see why it would not solve the first case. It’s true that it does not solve the third case. But maybe we don’t need to. I am not a fan of multiple {...} blocks in a row. I believe it tends to lead to obscure code and is overused. So, maybe not having a convenient brace-less syntax for it is OK. As an example, I would write the Option.fold example like this:

val result = myOption match
  case None =>
    val x = defaultValue
    x + 1
  case Some(value) =>
    val x = value + 1
    x * x

I would argue that’s much clearer!

EDIT: I see now what you mean with by-name parameters. Yes, if the argument is not a lambda you still need something else. I would argue for : in that case.

2 Likes

I really get this argument, but I still think the problem that’s very hard to get around is this:

: works much better in a language like python where it doesn’t have a more important meaning elsewhere.

My personal conclusion is this:

  • Code using : is harder to read than braces to me.
  • Code using where at least as easy to read as code with braces.

So it were up to me, (of course it’s not) the conclusion from where or with not working would simply be that braces are better.

Thanks for the examples, I really love this.

The use of with and .. is one of the cleanest and easiest-to-read alternative IMHO.

Though @odersky’s nothing-at-all proposal does make code a bit more quiet visually (unfortunately, it has some limitations, as pointed out by @sjrd).

Compare

  broke.foreach ..
    case (_, msg) => 
      alertables
      .map .. a =>
        (a, a alert msg)
      .collect .. case (a, No(e)) =>
        s"Failed to send alert $a\n=========\n$e\n==========\n\n"
      .mkString
      .tap .. x =>
        if (x.nonEmpty) println(x)

with

  broke.foreach:
    case (_, msg) => 
      alertables
      .map a =>
        (a, a alert msg)
      .collect case (a, No(e)) =>
        s"Failed to send alert $a\n=========\n$e\n==========\n\n"
      .mkString
      .tap x =>
        if (x.nonEmpty) println(x)
2 Likes

I would call it constructor inference and it would also work for methods:

def foo(x: Int): T = new {...}

, where it would not be incompatible with type inference:

def foo(x: Int) = new {...}

would translate to:

def foo(x: Int) = new Object {...}

It would make with available for other uses, namely type refinements as proposed by @mbloms:

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

Yeah, I feel very much the same wrt :. From what I can see in this thread, : is overloaded so much that it loses a lot of its appeal. I think the multi-line lambda example is the one that stands out most as having nothing to do with it. The name just before the lambda is the only that is needed, isn’t it?

broke.foreach
  case (_, msg) => 
    expr

… and one-liners are easy to disambiguate with brackets, which is how disambiguation has been done since humans have been disambiguating!

xs map (x => x + 1) filter (x => x > 0)

I converted the example to four spaces, and it ends up looking like this:

    case tree: Select =>
        val qual = tree.qualifier
        val qualSpan = qual.span
        val sym = tree.symbol.adjustIfCtorTyparam

        registerUseGuarded(qual.symbol.ifExists, sym, selectSpan(tree), tree.source)

        if qualSpan.exists && qualSpan.hasLength then
            traverse(qual)

    case tree: Import =>
        if tree.span.exists && tree.span.hasLength then
            for sel <- tree.selectors do
                val imported = sel.imported.name

                if imported != nme.WILDCARD then
                    for alt <- tree.expr.tpe.member(imported).alternatives do
                        registerUseGuarded(None, alt.symbol, sel.imported.span, tree.source)

                        if (alt.symbol.companionClass.exists)
                            registerUseGuarded(None, alt.symbol.companionClass, sel.imported.span, tree.source)

            traverseChildren(tree)

That’s a lot of whitespace. Far too much for me.

6 Likes

This was my spontaneous thought to begin with, but I agree with Martin here. It’s would be weird and unexpected if the type of foo is narrower than T. And it must be, otherwise a lot of stuff breaks.

Can we circle back to why we actually need special syntax for higher order methods? A lambda argument is just an argument, just like a, "foo", Some(42). How do we pass arguments to functions or methods? Between ( and ). Why can’t we simply pass lambda arguments between ( and ) like we do with all other arguments? In Scala 2 we have to use { } for multiline lambda’s because ; inference between parentheses doesn’t work. My guess is this whole discussion wouldn’t even exist if a different design had been picked in Scala 2, as suggested in this post:

The only issue I see is that Scala’s “user-defined control structures” become less native looking. But that seems to be almost inevitable with optional braces. All native syntax now defines the start of the “code block” with different keywords such as then, do, with, :. So perhaps if we want to keep native-looking user-defined control structures just pick one of those keywords and roll with it:

until(foo < bar) do
  println(1)
  println(2)

test("println should print to the console") do
  println(1)
  println(2)

// or

until(foo < bar):
  println(1)
  println(2)

test("println should print to the console"):
  println(1)
  println(2)

But in that case I think we should emphasize that the accepted way to pass lambdas and multiline arguments is between regular ( and ), and this is only for usage in DSLs.

Or alternatively don’t pick any code block starting keyword, and let DSL writers get creative with what’s available. E.g. this is already possible:

scala> object Foo { def update(s: String, f: => Unit) = {println("start"); f; println("end")} }; def test = Foo
// defined object Foo
def test: Foo.type

scala> test("println should print to the console") = 
     |   println(1)
     |   println(2)
     | 
start
1
2
end

*** As a side note, may I add that using { } instead of ( ) for passing arguments is actually a source of confusion for beginners in scala. I’m 99% sure that virtually all suggestions I have seen here will only worsen the initial confusion.


I passionately agree.

7 Likes

I don’t understand. The type of foo is T. The type of the RHS is narrower than T. IIUC that’s what we call avoidance. I do not see it as a problem. It can even be useful to hide implementation details (encapsulation).

So far, at least two people have said that the Dotty codebase is hard to read. Is there any one who finds the Dotty codebase easy to read?

And for the record, I find most examples posted here hard to read. Maybe using something other than colon :, something that stands out visually, as a block starter, and using that consistently for every block start, would make it easier. Maybe.

My Python is a bit rusty, but isn’t it the case in Python that a block starts if and only if a line ends with a colon :?

Multiple ways to start a block, some of which look very similar to other often-used language constructs, that’s pretty much the most efficient way to make a language unreadable.

3 Likes

The problem is that the freshly defined and specialized members defined in the body of the given must be visible on the outside. That’s extremely useful and I’d rather change all the syntax to S-expressions than give that up.

Just a reminder that not all developers are typographers, and some of us find that “noise” to be exceptionally useful for visually picking out boundaries, as horizontal and vertical spacing are not sufficient to establish hierarchy at a glance (at least for me).

Here’s my 2 cents:

  1. I don’t like that we reuse : as much as we have used _ symbol for everything…
    I’d like maybe if we used is token instead of it, for definitions of class/object/given/extension, for example:
trait A is
  def f: Int

class C(x: Int) extends A is
  def f = x

given [T](using Ord[T]): Ord[List[T]] is
  def compare(x: List[T], y: List[T]) = ???

extension (xs: List[Int]) is
  def second: Int = xs.tail.head
  1. I don’t understand why cases are special cased here?
    Is it really needed? It’s just more stuff to learn and parse in my head.

  2. It’s a bit weird to me that we want to get rid of braces, but then we have end markers… Which are 3+n-characters longer to type! :smiley:
    And not that useful… I always fold the text to see where it ends in my IDE.

  3. Scala is (AFAIK) one of rare languages that have multiple parameter lists.
    It is not yet clear how to write these in a braceless style… We would need some special keywords for those also…

  4. Why don’t we leave lambdas and blocks as they are now, I don’t understand the whole fuss about them? Do we really have to do everything right now?

3 Likes

I have to disagree. It would be far better practice to define a new trait/abstract/class for this.

1 Like