Skip to content

Scala Wart: Callers of zero-parameter methods can decide how many parens to use #2571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
lihaoyi opened this issue May 29, 2017 · 10 comments
Closed

Comments

@lihaoyi
Copy link
Contributor

lihaoyi commented May 29, 2017

Opening this issue, as suggested by Martin, to provide a place to discuss the individual warts brought up in the blog post Warts of the Scala Programming Language and the possibility of mitigating/fixing them in Dotty (and perhaps later in Scala 2.x). These are based on Scala 2.x behavior, which I understand Dotty follows closely, apologies in advance if it has already been fixed


Scala lets you leave off empty-parens lists when calling functions. This
looks kind of cute when calling getters:

@ def getFoo() = 1337
defined function foo

@ getFoo()
res8: Int = 1337

@ getFoo
res9: Int = 1337

However, it doesn't really make sense when you consider how this works in
most other languages, such as Python:

>>> def getFoo():
...     return 1337
...
>>> getFoo()
1337
>>> func = getFoo
>>> func()
1337

After all, if getFoo() is a Int, why shouldn't getFoo without the
parens be a () => Int? After all, calling a () => Int with parens
give you an Int. However, in Scala methods are "special", as shown above,
and methods with empty parens lists are treated even more specially.

Furthermore, this feature really doesn't make sense when you start pushing it:

@ def bar()()()()() = 2
defined function bar

@ bar
res11: Int = 2

@ bar()
res12: Int = 2

@ bar()()
res13: Int = 2

@ bar()()()
res14: Int = 2

@ bar()()()()
res15: Int = 2

@ bar()()()()()
res16: Int = 2

@ bar()()()()()()
cmd17.sc:1: Int does not take parameters
val res17 = bar()()()()()()
                         ^
Compilation Failed

Is this really the behavior we expect in a statically-typed language, that you
can call this method with any number of argument lists 0 < n <= 5 and
it'll do the same thing regardless? What on earth is the type of bar? The
Scala community likes to think that it's "definition-side variance" is better
than Java's "use-site variance", but here we have Scala providing
definition-site parens where every caller of bar can pick and choose how many
parens they want to pass.

I think the solution to this is clear: methods should be called with as many
sets of parentheses as they are defined with (excluding implicits). Any
method call missing parens should be eta-expanded into the appropriate
function value.

Concretely, that means that given these two functions:

object thing{
  def head: T = ???
  def next(): T = ???
}

They currently behave like this:

val first1: T = thing.head   // Works!
val first2: T = thing.head() // Compile Error: T is not a function and cannot be called
val first3: T = thing.next   // Works!
val first4: T = thing.next() // Works!

And will there-after behave like this:

val first1: T = thing.head   // Works!
val first2: T = thing.head() // Compile Error: T is not a function and cannot be called
val first3: T = thing.next   // Compile Error: found () => T, expected T
val first4: T = thing.next() // Works!

Notably, this does not take away the ability to control how many empty-parens
a function is called with; rather, it shifts that decision from the user of a
function to the author of a function. Since the author of a function already
decides everything else about it (It's name, arguments, return type,
implementation, ...) giving the author the decision over empty-parens would
not be unprecedented.

No-parens "property" functions would still be possible, the author of the
function would just need to define it without parens, as is already
possible:

@ def baz = 3
defined function baz

@ baz
res11: Int = 3

@ baz()
cmd12.sc:1: Int does not take parameters
val res12 = baz()
               ^
Compilation Failed

The only reason I've heard for this feature is to "let you call Java getFoo
methods without the parens", which seems like an exceedingly weak justification
for a language feature that so thoroughly breaks the expectations of a
statically-typed language. If that was the problem, one option would be to
allow use-site optional empty-parentheses only at Java call-sites or Scala
call-sites with a particular annotation (@optionalParens def foo = ...?).
This would limit the scope of this behavior to a mild Java-interop quirk
(one of many), rather than a wart affecting the core of the Scala programming
language

PostScript:

One reason in support of the current behavior I have encountered repeatedly in online forums is that this allows you to "fix" library APIs so they better match the pure/non-pure semantics of their functions: so if a library author writes a pure function with parentheses, you can "fix" the API and call it without parentheses. That way your code looks "idiomatic" and "correct" w.r.t. purity and parens even if upstream authors mess up.

I don't really think this is a good justification, for the exact same logic can be used to justify unlimited monkey-patching of upstream code, which I think most will agree does not make sense. Reductio ad absurdum

@tekacs
Copy link

tekacs commented Jun 1, 2017

I've expanded a lot more elsewhere in opposition to this change (https://news.ycombinator.com/item?id=14431208), but:

If both:

  • this feature were kept intact on the Java boundary as you now propose (which is where most of my examples of this breaking down were from and where the upstream author has no power to mark something as a property).
  • we don't see the automatic eta-expansion in Scala Wart: Weak eta-expansion #2570 (others are arguing against that too - now that 'breaks the expectations of a statically typed language' much more, in my mind)

Then I think that this behaviour makes sense 👍, if only because it makes the behaviour more consistent (where no parens -> () and () -> no parens are both disallowed).

I think that this behaviour in combination with the automatic expansion of no-parens methods in #2570 makes for really confusing behaviour, as I've expanded upon in that above news.yc link.

@dhoepelman
Copy link
Contributor

dhoepelman commented Jun 1, 2017

From the perspective of the uniform access principle, it makes sense that a caller can use foo whether that is defined as val foo, var foo or def foo.
I subjectively would say this extends to def foo(), especially since this is the only JVM representation of a property, but not to def foo()().

Would it make sense to view this as similar to an implicit conversion (() => T) => (=> T)?
This would indicate def foo() is allowed to be called as foo, butdef foo()() is not (since implicit conversions cannot be chained)

@lihaoyi
Copy link
Contributor Author

lihaoyi commented Jun 2, 2017

@tekacs yeah I've tweaked the text since the original blog post to better incorporate the discussion I had with people and clarify points I think were confusing. Thanks for all your feedback on the original post!

@odersky
Copy link
Contributor

odersky commented Jun 2, 2017

Scala 1 (2004-2005) had the precise behavior advocated by this issue, but here is what killed it:

val xs = "123"
println(xs.length)   // prints "<function1>"

val xs = Array(1, 2, 3)
println(xs.length)   // prints(3)

Same for toString, and any number of other commonly used methods. That's simply unacceptable behavior. It was unacceptable then and is unacceptable now. It means the uniform access principle would not work at all for anything we get from Java. It would stop being a principle then.

For a short while, Scala then had the rule that () was optional for Java-defined methods but not for Scala-defined ones. This looks like an interesting alternative, and we should consider going back to it.

The reason for dropping this rule at the time was that with it, converting libraries from Java to Scala would risk breaking client code. Let's say you have

int length()

in a Java library. Clients can call this with length or length(). But if we convert the definition to Scala, one of the two idioms will break no matter how we port length. So we generalized further and said that () is always optional. But, I believe nowadays we have stricter conventions to use () exlusively on side-effecting methods. So, assuming length is side-effect free, we'd port it to

def length: Int

and not

def length(): Int

Clients that used length() before would break, but they should be rewritten anyway, because they use length in a misleading way. Similarly, porting

int next()

on an Iterator should be

def next(): Int

because there's a side effect. So clients using it as next would break, but again, that's arguably a good thing. Things get hazier for methods like

int hasNext()

which often only have a "read" effect. The uniform access principle allows such methods to be parameterless, but some writers might prefer the parens to emphasize the read effect. So we are on more shaky ground here. If hasNext was ported to Scala it could go either way. But overall, I believe the breakage on rewrite risk is small enough to consider the change.

@felixmulder
Copy link
Contributor

👍 to disallowing calling nullary methods without parens.

To build on what @odersky said in the previous post. I believe it would be a good compromise to allow calling Java-defined methods without the () iff they are annotated with something like @pure.

AFAIK Kotlin does similar things for non-nullable types using annotations. Which allows them to write Kotlin-friendly libraries in Java.

@smarter
Copy link
Member

smarter commented Jun 2, 2017

To build on what @odersky said in the previous post. I believe it would be a good compromise to allow calling Java-defined methods without the () iff they are annotated with something like @pure.

That's too restrictive, unless Oracle suddenly starts annotating the whole standard library.

@felixmulder
Copy link
Contributor

felixmulder commented Jun 2, 2017

How often are you using the entire standard library from Java? Stop writing Java in Scala, Guillaume! 😂

I would not mind writing parens when calling Java methods. But sure, I see your point. Perhaps I'm being too harsh 👍

Martin's proposal is more pragmatic for sure.

@lihaoyi
Copy link
Contributor Author

lihaoyi commented Jun 2, 2017

I'd be happy for the "restricted to Java APIs, not Scala APIs (unless you add a special annotation)" solution. Having the behavior accessible via an annotation would also solve this problem:

The reason for dropping this rule at the time was that with it, converting libraries from Java to Scala would risk breaking client code

Apart from allowing us to migrate libraries without breaking client code, it's probably also a good idea just to have all the various semantics expressible in the Scala language, rather than having special behavior that is only expressible by writing Java source code. An annotation that can allow Scala methods to achieve the same call-site magic would allow that

@SethTisue
Copy link
Member

Scala then had the rule that () was optional for Java-defined methods but not for Scala-defined ones. This looks like an interesting alternative, and we should consider going back to it.

I'm in favor of this change.

(This has been discussed at least twice before, at scala/bug#4506 and a previous discussion I dimly recall but can't find (Paul on 4506: "I think we should either be stricter at the call sites (and there was some momentum for that a while ago, but I think martin decided against)"). But, I think the commenters on this ticket have done a really good job of making the relevant points again, so I don't think anybody else needs to go digging.)

@smarter
Copy link
Member

smarter commented Jun 24, 2017

Fixed by #2716

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants