Feedback sought: Optional Braces

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

Can someone explain to me what the value is in having a keyword or any other symbol to essentially be some kind of sugar for {}? It seems completely backwards to me. If I’m supposed to read code and then mentally insert { and } everywhere before I can begin to understand it, why not just use {} and be done with it?

The same question also regarding using some small inconspicuous symbol with no meaning. Why not go full YAML and just use indentation all the way instead of small symbols that just gets lost in everything else?

The same goes for end. Why does the end of a block get a keyword but the beginning gets a short almost invisible symbol? It doesn’t make any sense to me whatsoever.

5 Likes

I agree, of course! Re-reading my post, I see how I can sound l like a condescending brat, so I apologise for that.

Not only h/v spacing mind you: line endings and beginnings matter too. I am definitely biased in having as little syntactic devices as possible and let coding style do the rest, but it only works if the syntax is used consistently, so if I see a line ending with : I expect the next line to be a type, not an expression. Whereas, if it ends with =, the opposite is true.

3 Likes

I am sorry for offtopic, but it is really very hard construction for such simple and common thing.
For example in swift:

  if let value = myOption {
    val x = value + 1
    x * x
 }else{
     val x = defaultValue
     x + 1
 }

I would be very glad to have somthing like:

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

fold is good too, but as you have sad it is less readable.

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.

Are you saying we should not have methods that take multiple functions as arguments?

This looks pretty cool, but it’s flying pretty close to the sun in terms of ambiguity and parse-ability by both computers and humans. Coffeescript does exactly this, e.g. see the -> lambda in the example below:

$ 'body'
.click (e) ->
  $ '.box'
  .fadeIn 'fast'
  .addClass 'show'
.css 'background', 'white'

But Coffeescript is also well known for going too far in the direction of optional syntax.

If we could pull it off that would be great, but it would demand the greatest scrutiny to make sure it behaves properly and looks reasonable in all sorts of scenarios and edge cases, and doesn’t fall into the same human-readability pitfalls that Coffeescript finds itself in.

5 Likes

Ughh, I find ( ) for multi-lines much more annoying than { }. Please not. In fact I prefer to use curly braces for lambdas on single lines as well. They are only bad if you use a German keyboard layout (but who does when writing in a curly braces language?)

1 Like

I’m a bit curious why you prefer them even for single line lambdas as well. I mean, I do it too, but mostly out of habit because it’s annoying having to switch the whole time when you add a line, or when you want to make it a pattern matching function (*). Not because there’s any kind of advantage to it.

And then there are these methods such as Either.fold that have 2 function parameters in 1 parameter list and suddenly replacing ( ) with { } doesn’t work anymore.

If I’m not mistaken foo{ ... } is actually just short notation for foo({ ... }). So once you start inferring the curly braces it makes sense to only write the parentheses.

(*) Another one of those confusing syntax gotchas by the way. When a beginner asks me why they have to surround a PartialFunction / pattern matching function in curly braces the only possible answer seems to be “because that’s just the way it is”.

3 Likes

To this point, I think it may help to define a Scala 3 Style Guide, like Python’s PEP 8. The use of whitespace seems to be more important in an indentation style.

(This would be part of an update to the current Scala 2 Style Guide.)

3 Likes