Proposal to remove auto application from the language


#1

There’s a Scala 3 proposal to remove the addition of a missing empty argument list on nullary methods without arguments. For example, the following code would no longer compile:

def next(): T = ...
next     // is expanded to next()

In Scala 3, the application syntax would need to follow exactly the parameter syntax. Next, I quote the rest of the article motivating this removal from the Dotty website:

Excluded from this rule are methods that are defined in Java or that override methods defined in Java. The reason for being more lenient with such methods is that otherwise everyone would have to write

xs.toString().length()

instead of

xs.toString.length

The latter is idiomatic Scala because it conforms to the uniform access principle . This principle states that one should be able to change an object member from a field to a non-side-effecting method and back without affecting clients that access the member. Consequently, Scala encourages to define such “property” methods without a () parameter list whereas side-effecting methods should be defined with it. Methods defined in Java cannot make this distinction; for them a () is always mandatory. So Scala fixes the problem on the client side, by allowing the parameterless references. But where Scala allows that freedom for all method references, Dotty restricts it to references of external methods that are not defined themselves in Dotty.

For reasons of backwards compatibility, Dotty for the moment also auto-inserts () for nullary methods that are defined in Scala 2, or that override a method defined in Scala 2. It turns out that, because the correspondence between definition and call was not enforced in Scala so far, there are quite a few method definitions in Scala 2 libraries that use () in an inconsistent way. For instance, we find in scala.math.Numeric

def toInt(): Int

whereas toInt is written without parameters everywhere else. Enforcing strict parameter correspondence for references to such methods would project the inconsistencies to client code, which is undesirable. So Dotty opts for more leniency when type-checking references to such methods until most core libraries in Scala 2 have been cleaned up.

Stricter conformance rules also apply to overriding of nullary methods. It is no longer allowed to override a parameterless method by a nullary method or vice versa . Instead, both methods must agree exactly in their parameter lists.

class A {
  def next(): Int
}
class B extends A {
  /*!*/ def next: Int // overriding error: incompatible type
}

Methods overriding Java or Scala-2 methods are again exempted from this requirement.

The goal of this proposal would be to also provide rewrites to Scala 2.x users so that the previously valid behaviour is now migrated to the stricter checking.

There is a related discussion on this topic started by @lihaoyi here: Getting rid of use-site method-call-parens-count?

This proposal is open for discussion in the community and will be discussed in our next SIP meeting, where we’ll take all your feedback into account for the approval/dismissal of this feature.


First batch of Scala 3 SIPs
#2

What is expected in the following case?

def next(implicit foo : Foo) : Int

Will a call to next be accepted here?


#3

This change doesn’t really bother me but I don’t think this post or the “motivating” article that it links gives any actual reasons for the change. It’s more like “We are doing this but here’s some cases where we can’t and why we can’t in those cases.” Would it be possible to augment this proposal slightly to include its motivation?


#4

@soronpo Yes, in that case x.next will be accepted, and x.next() will be rejected. This is unchanged compared to the current specification.


#5

Just to get this straight:

Let’s say I am writing someObject.next in my Scala 3 code to call method next() from library A (Scala 3), which overrides next() from library B (Scala 3), which overrides next() from library C. Does this proposal mean that whether the call will succeed will depend on whether library C is written in Java, Scala 2 or Scala 3?


#6

I guess the motivation can be seen more in the issue linked on the bottom of the page, opened by Haoyi Li (which he had previously blogged about): https://github.com/lampepfl/dotty/issues/2570


#7

I think that library B chooses whether to override it as next() or next (if C is Java) and the rest has to follow. Now the question is: if library B doesn’t override next but just inherits it unchanged, will it be fixed to next() for all dependents, or is it open until some Scala code overrides it? Or am I just wrong?


#8

Yes. In fact next() would be illegal.


#9

If next is defined in Scala 3 but overrides a next coming from a Java or Scala 2 library, the old semantics applies, i.e. the () can be dropped. So the new restrictive rule only applies if all overridden versions are defined in Scala 3.


#10

And if next is defined in Scala 3 but overridden in a Java library, which in turn is used in a Scala 3 project?


#11

Same thing. As long as there is a Scala-2 or Java version in the set of overridden variants, the rule is relaxed.


#12

For reference, related: we had a discussion about which mehtods should get a () parameter list (vs no parameter list) in the contrext of the new collections: https://github.com/scala/collection-strawman/issues/520.


#13

One case I wonder about is interaction with type parameters. For example, I can write

import scala.concurrent._

val p = Promise[Unit]

but I can also write

trait Foo[A] {
  def run(): Future[A] = {
    val p = Promise()
    p.future
  }
}

With the change I would have to write Promise[Unit]().

Nevertheless, I’m in favour of the change - I think it helps with DSLs which often have to carefully design around the invocation of apply methods, and the change will remove ambiguity from the parser.


#14

I’d argue that there shouldn’t be any conditions to this. If the method was defined without parens, it can only be called without parens. If it was defined with parens, it must be called with parens. This removes any special “unless it’s the third Tuesday after a full moon”-type confusion.

It would mean that Java methods would always have to be called with parens. To which I say, “so what?” It is a small price to pay to simplify the language. Writing toString() isn’t a big deal (at least in my opinion), and it may even make more sense than toString – especially in the context of Java methods, any of which might fire the missiles.

Is a couple of extra parens really so distasteful that it warrants special rules in the language?


#15

I want to like this change, but it is hard to enjoy remembering something that is usually an irrelevant detail and only sometimes reflects programmer intent (but intent which is not backed up by the compiler). Having unambiguous parses is great, but I think the programmer is the one who needs help, not the compiler. So if the programmer can’t tell whether the following lines are equivalent:

f()()
f.apply().apply()
f().apply()

at a glance, then I don’t think making the compiler know which is which is a very big help.

Also, this change isn’t enough, assuming implicit parameter blocks can still be elided; you still have ambiguity.

And the uniform access principle carries less weight in Scala than it could because you don’t get direct field access anyway. Everything’s through an accessor (meaning you still write the parens in Java).

I would prefer the following rules to be applied consistently:

(1) Empty parameter lists, whether implicit or explicit, whether zero-parameter or filled completely with default parameters, may be elided. This matches what you’re allowed to do with overriding vs overloading anyway at the JVM level.

(2) Ambiguous parses are forbidden at the use-site. If foo() may be elided to foo, then if its return value has an apply method, that apply method cannot be called using foo().

(3) .() is another synonym for .apply(), so you can compactly disambiguate parses.

Having different rules for Java and Scala is already a source of pain for cross-compatibility. I really do not like the idea of “best practices” for some libraries being to define accessors in Java so you can have both () and not.

Overloading the semantics of () to carry information about an expectation of consistency of return value is not, I think, a very robust way to do things. There are a lot of ()'s anyway; it’s a slightly helpful but not incredibly clear signal, and it’s not universally applied, and in the case of things with arguments it doesn’t apply anyway and is just is as sorely needed.

As an example of a case where the required parens could be really annoying, consider a random number generator with compact generating methods I, L, F, D, etc… These obviously mutate the random number stream, so def I() = nextInt would be the recommended way to write it. But if you’re trying to make it easy to grab random numbers, you might import rng._; myThing(L, L, D, F) which is incredibly clear and easy to read, but myThing(L(), L(), D(), F()) is all cluttered. So that would suggest that you have to def I = nextInt in this library, and this change actually prevents adherence to the uniform access principle rule.

And to reiterate the collections story, if you have an inheritance hierarchy where foo might be mutable/mutating, the UAP would tell you that you have to def foo(). But if in a subclass you can replace it with a val, you couldn’t because of the parens. So you’d you’d have annoying boilerplate private[this] val myFoo = "thing"; def foo() = myFoo, and again, get the UAP wrong when the precise type is known.

So I like the idea of reducing ambiguity, but I don’t think this proposal goes far enough to really fix that, and I think it levies a substantial burden on programmer memory (when writing, not reading, which admittedly is less problematic) which is already in limiting supply.


#16

I’m very much in favor of the spirit of this change, but a bit worried about the actual result.

The idea is to normalize things, which makes total sense, but after this change we end up with more inconsistencies than before, due to the backward compatibility towards Scala 2.

My modest proposal would be to set a stricter limit to the drop of Scala 2 backward compatibility, in order to provide a stronger incentive to adapt those methods. For example, the backward compatibility could be explicitly dropped in Scala 3.1 or Scala 3.2.

I’m afraid that “until most core libraries in Scala 2 have been cleaned up” is too vague to bother anyone to go fix their libraries.


#17

Is this proposal only for definitions? I don’t know what the exact rules are but I think they should be consistent with class constructor argument list as well.


#18

I wonder why the special rule for Java method is necessary.

There is no parenthesis in Array.length definition: https://github.com/scala/scala/blob/86e75db7f36bcafdd75302f2c2cca0c68413214d/src/library/scala/Array.scala#L586

Can we remove the parentheses from Any.toString() and Any.getClass() as well? https://github.com/scala/scala/blob/86e75db7f36bcafdd75302f2c2cca0c68413214d/src/library-aux/scala/Any.scala#L74


#19

How about String.length


#20

Then the rule that constructors always have at least 1 argument list should be removed. If I define class Foo now the compiler rewrites that to class Foo().