Dotty: Drop Local Blocks

Created on 16 Nov 2019  路  22Comments  路  Source: lampepfl/dotty

Scala allows local blocks as in

def foo(x: Int) = {
  val y = x

  { val z = y * y
    println(x)
  }
  y -1 
}

This is dangerous, as it only works if there is an empty line in front of the local block. Dropping the empty line can give obscure type errors or even change the meaning of the program since then the block is treated as an argument to a function on the preceding line.

Scala already has locally in Predef. The example above is better written like this:

def foo(x: Int) = {
  val y = x
  locally { 
    val z = y * y
    println(x)
  }
  y -1 
}

I believe Scala 3 should mandate locally and not allow naked local blocks. Changing this would entail:

  • Make local blocks a deprecation warning under -language:Scala2Compat and an error otherwise (using the errorOrMigrationWarning in dotc).
  • Add a rewrite rule to insert the locally.
  • Fix tests and community build to always use locally
  • Add a page in the reference section of the Dotty docs that the feature has been dropped.

It would be good to get this in by feature freeze at the end of this release cycle. If there's a volunteer I am happy to review.

good first issue help wanted language enhancement

Most helpful comment

I am not sure dropping naked block will have any positive effect in the long run. In a world where naked blocks have completely disappeared, I will still be able to write

def foo(x: Int) = {
  val y = x
  { 
    val z = y * y
    println(x)
  }
  y -1 
}

and it will still do the wrong thing, without any warning. So what did I gain?

I agree that the blank line thing is annoying, but IMO the fault lies in allowing the block to start on the next line rather than at the end of the previous line. I'd like that to be deprecated, so that I actually receive a warning when I did the wrong thing, and no warning when I did the right thing. But that is probably not possible because it would break all the code that uses that style.

(Also, this change will break the consistency rule that every expression is also a valid statement.)

All 22 comments

yes please!

not only are local blocks dangerous to write, they're hard to read. the parser in our heads, too, normally expects braces to mean something else. it's better to give our mental parsers the explicit locally to avoid any guessing/backtracking

I suspect it will turn out that such blocks are pretty rare in actual code. at least, I can't remember the last time I encountered one. (in my own code, I have always used locally instead, but even that need rarely arises.)

assuming this goes forward in Dotty, then a deprecation PR in 2.13.x would also be accepted, I think.

question, though: how does locally look in the indentation-based syntax?

As a fan of local blocks to better demarcate lifetimes of local vals/vars I think I should weigh in.

May I humbly suggest that the name locally is a suboptimal name鈥攅specially for those of us who very frequently write distributed code?

Possible alternatives:

  • scope
  • section

EDIT: I realize that it is possible to do import Predef.{locally => scope}, but since we are looking to expose a rather new name in a bunch of existing sources, I'd rather get it right now rather than later.

It would be good to have locally be an inline def so that the compilation of the old and new styles are identical. Importantly, we want to avoid boxing vars.


scala> def actLocally: Unit = { var x = 0; locally { x += 1 } }
def actLocally: Unit

scala> :javap actLocally
...
  public void actLocally();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: getstatic     #25                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
         5: iload_1
         6: iconst_1
         7: iadd
         8: istore_1
         9: getstatic     #31                 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit;
        12: invokevirtual #35                 // Method scala/Predef$.locally:(Ljava/lang/Object;)Ljava/lang/Object;
        15: pop
        16: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            1      15     1     x   I
            0      17     0  this   L$line18/$read$$iw$$iw$;

I am not sure dropping naked block will have any positive effect in the long run. In a world where naked blocks have completely disappeared, I will still be able to write

def foo(x: Int) = {
  val y = x
  { 
    val z = y * y
    println(x)
  }
  y -1 
}

and it will still do the wrong thing, without any warning. So what did I gain?

I agree that the blank line thing is annoying, but IMO the fault lies in allowing the block to start on the next line rather than at the end of the previous line. I'd like that to be deprecated, so that I actually receive a warning when I did the wrong thing, and no warning when I did the right thing. But that is probably not possible because it would break all the code that uses that style.

(Also, this change will break the consistency rule that every expression is also a valid statement.)

locally is a suboptimal name

We cannot do this in 3.0 without pulling additional tricks, becsause 3.0 ships without its own standard library, it uses the binary one from 2.13.

without any warning. So what did I gain?

It's an error though :-) We could probably improve the compiler's message?

It's also an error today, when you're not unlucky. My point is, the proposed change will not improve the situation. It will only make today's correct programs invalid, without making today's buggy programs invalid. So I repeat my question: what have I gained?

@lrytz

We cannot do this in 3.0 without pulling additional tricks, becsause 3.0 ships without its own standard library, it uses the binary one from 2.13.

We get around this currently by having a DottyPredef besides Predef that contains some inline functions (and, yes, locally or whatever it ends up being called should be an inline function). But I think it might also be possible to make a 3.0 specific Predef that contains these inline functions but is binary compatible to the 2.13 Predef.

@sjrd

So I repeat my question: what have I gained?

We removed for future programs the ambiguity and the trap it results in. It is one thing to say: "Don't do that" and another to forbid it. If there are no local blocks, period, people will be less inclined to use them wrongly.

question, though: how does locally look in the indentation-based syntax?

The same as now. The currently implemented scheme requires braces. Under the non-standard option -Yindent-colons you could write

def foo(x: Int) =
  val y = x
  locally:
    val z = y * y
    println(x)
  y - 1 

But the status of that option is quite uncertain now. That part of the proposal was retracted since it got too much opposition.

@sjrd

But that is probably not possible because it would break all the code that uses that style.

Do we have any cases we know use this style? Or should we actually look at how this affects the community build?

@odersky

We removed for future programs the ambiguity and the trap it results in. It is one thing to say: "Don't do that" and another to forbid it. If there are no local blocks, period, people will be less inclined to use them wrongly.

I agree with removing ambiguities and traps. But what don't you like about Seb's idea of forbidding applications to start on a new line? I would've thought you would've liked not losing the "every expression is also a valid statement" consistency rule.

Question: do we need this now? I get the argument for many features for Scala 3, but the "need to rewrite the textbooks" argument doesn't seem to hold for this relatively rarely-used aspect of the language. It seems like it could do with more discussion, and a proper deprecation cycle; I don't see a lot of need to rush it into Scala 3.0.

It seems this is a hornets nest. I withdraw from the discussion for now. If someone wants to move this forward, you have my support.

I don't see a lot of need to rush it into Scala 3.0

Local blocks are definitely a trap, it would be good to do something about it.

How about

  • Add an inline def scope to DottyPredef
  • Unless -language:Scala2, add a warning and rewrite rule on local blocks to use scope
  • Provide additional information in the error message (typically does not take parameters, missing argument or type mismatch) if the function is a value (not a method) and the argument is a block on a new line - we can maybe not make this perfect, but there might be an 80/20 solution.

IMO, @sjrd's suggestion is too breaking, some people use the c-style syntax. If we support it for built-in control structures, it has to work for custom too:

scope
{
  println()
}

While there may be some projects that use c-style wrapping (or "indentation") for definitions, I would want to see if there's actual usage of it for block arguments... Similarly for built-in control structures.

While there may be some projects that use c-style wrapping (or "indentation") for definitions, I would want to see if there's actual usage of it for block arguments... Similarly for built-in control structures.

In my experience there's always a lot more usage than I first thought, but it tends to come out of the woodwork very late. Typically, enterprise projects differ more in their coding conventions than open source. So, disabling a common (in other languages) convention like that would make me very nervous.

Example: Nobody thought that early initializers were used much, until I learned that Expedias entire architecture was built on them.

Will the following still work after the proposed change?

def foo(arg : Int) : Unit = ???
foo({println("something"); 0})

It has to, good example! It might be not that easy to identify local blocks. While your example would also work without the parentheses, foo(0, {println(); 0}) cannot be shortened.

Do we really need locally? The meaning of the following might be clearer:

def foo(x: Int) = {
  val y = x
  val () = { 
    val z = y * y
    println(x)
  }
  y -1 
}

@soronpo Yes. The definition of a local block would be a block that appears as one of the statements in a statement sequence.

Was this page helpful?
0 / 5 - 0 ratings