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

Further enhance `new` syntax to reduce boilerplate #5509

Open
propensive opened this Issue Nov 23, 2018 · 13 comments

Comments

Projects
None yet
10 participants
@propensive

propensive commented Nov 23, 2018

I'm proposing this in the spirit of identifying "warts" with the language that could be cured, mainly to stimulate discussion.

In Scala 2.x, it is possible to write,

case class Foo[T]()
def foo: Foo[String] = new Foo()

and have the expected type Foo[String] able to infer the type parameter T = String.

Dotty goes further in allowing an anonymous class to be coerced into the expected type, e.g.,

trait Bar { def bar: Int }
val b: Bar = new { def bar = 42 }

But we could go even further along this road. In the presence of an expected type, we could invoke new eliding the type completely, for example,

class Baz(x: Int, y: String)
val baz: Baz = new(1, "two")

This could reduce the local boilerplate in many one-liners which assign a new instance to a value with an identical type. I searched my 50K lines of source code for, lines of the form,

... : X = new X ...

with

grep -r ': \(.*\) = new \(\1\)' *

and about 0.5% of the lines matched the pattern. So it happens quite frequently.

This would further be an opportunity to normalize the syntax of new. Currently, in a call such as,

new Foo().bar

the parser will read this as (new Foo()).bar. This is an anomaly in that the new (always followed by a space) has higher precedence than the . which comes after it.

The situation is further confused should the parentheses be omitted:

new Foo.bar

will attempt to instantiate a class called bar on the object Foo. Given that new Foo and new Foo() behave identically in isolation, this may be surprising, particularly to beginners looking for simple universal rules like ". always has higher precedence than ".

I therefore propose, for consistency with almost all other syntax in Scala, that new should accept a type, in square brackets immediately following it, which may be omitted if the type can be inferred from the expected type, just as with a method call, for example

class Foo(x: Int)
val f1: Foo = new(1)
val f2 = new[Foo](2)
val f3 = new(3) // error

In the cases where a type constructor is provided as a type parameter to new, instead of a proper type, this would be unified with the expected type to infer the type parameter, as currently happens (though generalized to type constructors, only really for consistency rather than utility), e.g.

class Foo[T]()
val f: Foo[String] = new[Foo]()
class Bar[A, B]()
val b: Bar[Int, String] = new[[T] => Bar[T, String]]()

All this would only work for class types, as is currently the case. Anonymous classes would still be written, new { ... }. Additionally, the existing new syntax should remain for the foreseeable future because the cost would drastically outweigh the benefits otherwise. Maybe it could have a 3-4 year window of deprecation, much like vals in for-comprehensions. But usage could certainly be opt-in to begin with.

@jducoeur

This comment has been minimized.

Contributor

jducoeur commented Nov 23, 2018

My lizard-brain goes, "Ack! Too different from other languages! Scary!"

But it does fit Scala's "Consistency is Good" philosophy nicely -- stepping back and looking at it in the context of the rest of the language, the square brackets seem to make a lot of sense.

With the suggested timeline, I could see this one...

@lihaoyi

This comment has been minimized.

Contributor

lihaoyi commented Nov 24, 2018

C# is going to have this, they call it target-typed new expressions:

Dictionary<string, List<string>> musicians = new() {   { "John", new() { “guitar”, “bass” } }};

Would be nice to remove the boilerplate of defining implicit instances:

implicit val IntOrd: Ord[Int] = new {
  def compareTo(this x: Int)(y: Int) =
    if (x < y) -1 else if (x > y) +1 else 0
}
@propensive

This comment has been minimized.

propensive commented Nov 24, 2018

Yes, most of my examples were lines defining implicits, being the cases where I never omit the return type.

@arturopala

This comment has been minimized.

arturopala commented Nov 24, 2018

This should be an easy candidate for automatic rewrite, so no need for a long transition. Too many syntax variants does not make Scala user-friendly.

@propensive

This comment has been minimized.

propensive commented Nov 24, 2018

@arturopala Agreed. Though it's also only a tiny benefit, so I'd want to strike a good balance between forcing users to make changes they don't perceive to be particularly beneficial (automatic, or otherwise) and the good point you make about accommodating syntax variants. I don't remember the exact timings of the removal of val from for-comprehensions, but I think it was several years, but by the time it was removed, basically nobody was using it any more. :)

@hepin1989

This comment has been minimized.

hepin1989 commented Nov 24, 2018

I am not sure if this is enabled now in Dotty, But I wondered this kind of object literal several times:

case class Person (name:String,age:Int)
val person = new {
 name = "me"
 age = 1
}
@nafg

This comment has been minimized.

nafg commented Nov 25, 2018

@propensive

val baz: Baz = new(1, "two")

Why is that better than

val baz = new Baz(1, "two")

@lihaoyi

Would be nice to remove the boilerplate of defining implicit instances:

I'd rather if the requirement to explicitly type implicits were relaxed a bit. There are many cases where the type is known to be inferrable locally. For instance, if the right hand side is a simple new expression it should be permitted to rely on type inference. Similarly, implicit object should be permitted by the same logic.

@propensive

I therefore propose, for consistency with almost all other syntax in Scala, that new should accept a type, in square brackets immediately following it,

Why do we suddenly have a need to squeeze every good idea into dotty? Languages should evolve over time. Dotty should not be the once-in-a-lifetime opportunity to rethink Scala from the ground up. Unless this is supposed to be a new language. The goals of Dotty are

  1. To have strong mathematical foundations
  2. To clean up as many language warts as possible
  3. To be a major version bump in the language
    Since it is indeed a major version bump, we can indeed evolve the language, adding, changing, and removing features, but the same is true for all future major version bumps. Major version bumps shouldn't be so rare. C# is not much older than Scala and it's up to 8 already.
@jducoeur

This comment has been minimized.

Contributor

jducoeur commented Nov 25, 2018

Why is that better than

val baz = new Baz(1, "two")

It isn't necessarily -- but it's fairly common nowadays for company coding standards to require type ascriptions for all public members. So in practice, boilerplatey duplication is common.

I agree that we should be getting more comfortable with language bumps: now that Scalafix is becoming real, they should start being less traumatic. (Although we're going to learn a lot from the process of moving to Scala 3.)

That said, I think the proposal on the table -- which is, after all, suggesting that new is currently a language wart -- is worth considering for 3.0, especially if we do gradually transition. We can do it later, so I don't think it's a crisis, but it looks like a smallish change compared to the rest...

@pavelfatin

This comment has been minimized.

pavelfatin commented Nov 25, 2018

Here are a few related things from the IntelliJ Scala plugin that could be useful:

  • SCL-13575 Type annotation settings: "Type is obvious" -> "Type is stable"
  • SCL-14339 "definition type is obvious" specification

Ideally, we should not require a type annotation when the type is stable, and should not display a type hint when the type is obvious.

@pavelfatin

This comment has been minimized.

pavelfatin commented Nov 25, 2018

As for the new syntax - I would rather prefer val baz = Baz(1, "two") because:

  1. Uniform access principle, for constructors.
  2. Possible to toggle between constructor and factory method.
  3. Consistency with case classes.
  4. Constructor is a function.
@kjsingh

This comment has been minimized.

kjsingh commented Nov 29, 2018

+1 for present syntax.

val baz = Baz(1, "two")

and

val baz = (1, "two") // as tuple

show clear intentions.

@sarveshseri

This comment has been minimized.

sarveshseri commented Dec 7, 2018

I personally don't find any excessive "boilerplate" even in val foo = new Foo(1, "two") and we already have val foo = Foo(1, "two)".

I personally think that existence of "alternative" ways to write almost everything in Scala is brings more problems compared to benefits.

Yes, reduction of boilerplate is often good but it should not come at the cost of increased ambiguity, inconsistency and complexity.

@propensive

This comment has been minimized.

propensive commented Dec 17, 2018

Thank you to everyone who commented on this. It will take me a while to find the time, but I plan to write a SIP to propose this change for Scala 3. I will try to take into account (by incorporating, or addressing) all the feedback in the proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment