Skip to content
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

Shorter constructor declaration syntax. #3788

Open
lrhn opened this issue May 8, 2024 · 10 comments
Open

Shorter constructor declaration syntax. #3788

lrhn opened this issue May 8, 2024 · 10 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@lrhn
Copy link
Member

lrhn commented May 8, 2024

A constructor declaration repeats the class name.

That's not really necessary. A constructor needs some syntax to define it as a constructor, but repeating the (potentially quite long) class name is noisy and verbose.

The declaration syntax mimicks how the constructor is called, so Foo(int x) is called as Foo(42), but that's not an argument we use for instance or static members. (And it wasn't true originally, not until Dart made new optional.)

So really it's because "Java and C# did it", the two languages Dart was designed to be familiar to users of. And both probably took it from C++, which did it because then they didn't need to introduce another keyword.

We can do better.

Proposal

Instead of writing the class name when declaring a constructor, use the word new, or for a const constructor, use const instead of the name and the const prefix.

The affected grammar rules are:

<constantConstructorSignature> ::= 
      'const' <constructorName> <formalParameterList>
    | <shortConstroctorNameConst> <formalParameterList>  ;; new

<constructorSignature> ::= 
       <constructorName> <formalParameterList>
    | <shortConstructorNameNonConst> <formalParameterList>  ;; new

<factoryConstructorSignature> ::= 
      'const'? 'factory' <constructorName> <formalParameterList> |
    |'const' <shortFactoryConstructorNameConst> <formalParameterList>  ;; new
    |<shortFactoryConstructorNameNonConst> <formalParameterList>  ;; new

<redirectingFactoryConstructorSignature> ::=
      <factoryConstructorSignature> '=' <constructorDesignation> ;; modified
   
<constructorName> ::= <typeIdentifier> ('.' (<identifier> | 'new'))?  

<shortFactoryConstructorNameConst> ::= 'factory' ('.' <identifier>)?
<shortFactoryConstructorNameNonConst> ::= 'factory' '.' (<identifier> | 'new')
<shortConstructorNameNonConst> ::= 'new' ('.' <identifier>)?    ;; new
<shortConstructorNameConst> ::= 'const' ('.'<identifier>)?    ;; new

The result of these rules is to allow the following declarations:

  const(this.x);
  const.name(this.x);
  new(this.x);
  new.name(this.x)
  factory.new(int x) ...
  factory.name(int x) ...
  const factory(int x) ...
  const factory.name(int x) ...

Basically, for a generative constructor, write new instead of the class name, or const instead of the name and a leading const.
For factory constructors use the factory instead of the class name, except for the non-constant unnamed constructor, which needs to be factory.new.

It needs that because factory is currently not a reserved word, and factory(int x) => 42; is currently a valid method declaration. There are probably other workarounds, possibly making factory at least a contextual keyword, but this is short and doesn't require much new syntax. (Suggestions welcome. We do risk conflicts with const factory(int x) => ... in the future if we ever start allowing "constant methods" or class-level expressions or other weird stuff.)
Making factory a reserved word, or evne just a contextually reserved word at the start of a class-level declaration, is breaking.

Notice that this grammar does not allow combinations like const.new(...) and new.new(...). Because that looks ridiculous.

A declaration containing a const is a constant constructor declaration, a declaration containing factory is a factory constructor declaration, and the part after the signature still determines whether it's a redirecting constructor or not.

In every case, the currently allowed constructor signature can be recreated by inserting the class name into the declaration at the correct place, or by replacing a leading new. That makes it easy to understand, it's like a "class name inference" that infers the name of the constructor.

Examples

A declaration of

abstract final class UnmodifiableByteBufferView implements ByteBuffer {
  external factory UnmodifiableByteBufferView(ByteBuffer data);
}

becomes

abstract final class UnmodifiableByteBufferView implements ByteBuffer {
  external factory(ByteBuffer data);
}

And with more constructors:

final class NativeFloat64x2List /*...*/ {
  final Float64List _storage;

  new(int length) : _storage = NativeFloat64List(length * 2);

  new._externalStorage(this._storage);

  new._slowFromList(List<Float64x2> list) : _storage = NativeFloat64List(list.length * 2) {
    // ...
  }

  factory.fromList(List<Float64x2> list) { 
    // ...
  }
 
  // ...
}

Summary

This change allows omitting the class name from some constructor declarations, in two cases inserting the keyword new instead to have something to recognize the constructor by.

I am not absolutely sure that makes code more readable.
The capitalized word of a current constructor stands out.
But standing out and being verbose may be correlated.

Maybe this is too little, and we should have a completely different constructor syntax, instead of trying to tweak the existing (C++ legacy) syntax.

Or maybe we don't need any change, what we have "works" even if it's occasionally annoying to write, because it is actually easy to read, and familiar to almost everybody.

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label May 8, 2024
@lucavenir
Copy link

maybe I'm asking a silly one, but how does this interact with #2364?

@modulovalue
Copy link

Currently, some ways of declaring constructors reuse method declaration syntax. Consider:

class ??? {
  Foo() {}
}

If the class is called Foo, then Foo is a constructor declaration. If the class is not called Foo, then Foo is a method declaration.

I think it would be great if constructors and methods were syntactically distinct so that we can definitively say whether a constructor is a constructor and a method a method without one having to consider context further up the parse tree.

It seems like this proposal would be able to make that happen since const/new/factory aren't valid declaration names and the dot syntax is unambiguous with method declaration syntax.

@tatumizer
Copy link

tatumizer commented May 9, 2024

Please consider an alternative:

final class NativeFloat64x2List {
  final Float64List _storage;
  constructor new(int length) : _storage = NativeFloat64List(length * 2);
  constructor _externalStorage(this._storage);
  constructor _slowFromList(List<Float64x2> list) : _storage = NativeFloat64List(list.length * 2) {/* ... */}
  factory fromList(List<Float64x2> list) {/*...*/ }
}

WHY?
The point of this syntax is to allow a compiler to automatically generate a "static interface", with the following definition:

static interface NativeFloat64x2ListStaticInterface {
  constructor new(int length);
  constructor _externalStorage(Float64List storage); // private ones won't be here,
  constructor _slowFromList(List<Float64x2> list);   // but imagine they were included - for illustration
  factory fromList(List<Float64x2> list);
}

If NativeFloat64x2 had a static method - say, static int foo(int x), this method would become part of static interface, too, with omitted "static" (it can be made optional: within a static interface, everything is either a constructor, or a factory, or a static method)

static interface NativeFloat64x2ListStaticInterface {
  constructor new(int length);
  // etc. - as before
  int foo(int x);
}

What does it buy us?
Now we can define static interface Copyable<T> (see #356) and implement it:

static interface Copyable<T> {
  T copy(T t);
}
class User implements static Copyable<User> {
  final String name;
  User(this.name);
  static User copy(User u)=>User(u.name);
}

When we say implements static Copyable<User>, we mean that the automatically generated interface UserStaticInterface is guaranteed to comply with (extend) static interface Copyable<User>. Without reference to UserStaticInterface, it would be difficult to explain what it means.
Here's what a generated interface UserStaticInterface looks like:

static interface UserStaticInterface {
  constructor new(this.name);
  User copy(User u)=>User(u.name);
}

If User had type parameters, they would become type parameters of UserStaticInterface, too.
Does this make sense? (Not sure)
(Of course, static interface could be generated based on different naming conventions, but I think this one is more familiar and less cryptic)

@Wdestroier
Copy link

There are probably other workarounds, possibly making factory at least a contextual keyword, but this is short and doesn't require much new syntax.

Would be great to make factory at least a contextual keyword imho, because the factory.new(int x) ... declaration feels asymmetric with const factory(int x) ....

@tatumizer
Copy link

Putting undue emphasis on "factory" is unfair to generative constructors. They deserve more respect! :-)

Let's view the issue from another angle. What change would be the easiest to explain?
I can't think of an explanation simpler than this: "Replace class name with the word constructor". That's it. No "new". No name mangling. If the name of the constructor is empty, it remains empty.
Of course, factory constructor wording is redundant - we can allow (optionally) omitting constructor for factories.

final class NativeFloat64x2List {
  final Float64List _storage;
  constructor (int length) : _storage = NativeFloat64List(length * 2); // no "new"
  constructor _externalStorage(this._storage);
  constructor _slowFromList(List<Float64x2> list) : _storage = NativeFloat64List(list.length * 2) {/* ... */}
  factory fromList(List<Float64x2> list) {/*...*/ }
}

It works well with const, too (the change doesn't affect it).

@lrhn
Copy link
Member Author

lrhn commented May 11, 2024

My main complaint about constructor, what was also what I said ... quite a lot of years ago when it was first suggested, is that it's long.
There are class names that it's longer than, so it doesn't really make things much shorter. (And I'm bound to mistype it).

What if it was ctr instead? (Annoying, I'll have to rename my counters.)
Then it's still a new word, where we already have a three letter word associated with constructors.

So try the above with new instead:

final class NativeFloat64x2List {
  final Float64List _storage;
  new(int length) : _storage = NativeFloat64List(length * 2); // no "new"
  new _externalStorage(this._storage);
  new _slowFromList(List<Float64x2> list) : _storage = NativeFloat64List(list.length * 2) {/* ... */}
  factory fromList(List<Float64x2> list) {/*...*/ }
}

That's basically what I proposed, just without a . after new.
And fixing that you can have a method named factory today.

So why is that . there?
Because this doesn't work as well with const. Writing const new(x) looks off. It's out cigarette or is it not. (Well, you can call or work both, so maybe it works. But the new feels redundant.
Using just

  const name(args);

Works too. May step on a later constant functions feature.
So add the . to make this clearly a constructor declaration.

@tatumizer
Copy link

tatumizer commented May 11, 2024

"constructor", as long a word as it is, is still shorter than a typical class name. Real class names in flutter (and elsewhere) tend to be rather long because they (understandably) have to concatenate several words, like "AlwaysScrollableScrollPhysics". Replacing a name like this with a shorter "constructor" is still a considerable gain.

"constructor" has one thing going for it: it's very visible among other declarations. Today, the constructors are visible due to the repetition of the class name. Class names follow a very recognizable syntactic pattern, which makes them "stand out". "new" is less visible IMO.

According to Wikipedia, the standard abbreviation for "constructor" is "ctor", but there's no example of the language introducing a "ctor" keyword. The term "constructor" is used in javascript and kotlin - look up "secondary constructor". (Some languages indeed use the name "new", others follow a traditional C++/java format).

Since the constructors are not declared very often, using a bit longer, but more explicit, keyword seems appropriate. You can always set up IDE so that "ctor" will be expanded into "constructor". Elsewhere, "Grammarly" plugin will fix a wrong spelling.

I thought the issue is somehow entangled with the concept of static interfaces (#356), where the word "constructor" would look more readable to me than the alternatives, though this is subjective. (I don't know whether you have plans to include constructors in the static interface, but this would be highly desirable: the ability to say T.myConstructor(...) where T- type parameter, will add a lot of power to the language.)

When the feature is considered in isolation, everyone will vote for a shorter syntax, but here, the feature is interrelated with a number of other features that have to be included in the context.

EDIT: speaking of "const", I find these two declarations inconsistent with each other:

const.name(this.x);
const factory.name(int x) ...

One const has a dot next to it, another doesn't, and you will have to explain to everyone why.
Adding a dot in the second declaration would give const.factory.name(int x), which is weird: here we introduce the whole new sub-language with never-before-seen syntax just to avoid the word "constructor"? Anyone reading the declaration will
have "constructor" ringing in their head anyway, just with more effort.

The rules of migration are complicated (what has to be replaced by what, what has to be dropped, insert dot here, but not there :-). I tried to formulate all the rules, came up with a decision tree, which I won't be able to internalize :-)

Another issue. Suppose the class has a number of static methods (foo, bar) etc.
In the static interface, "static' becomes optional, so we have

static  interface XInterface {
  new(String);  // it was const(...) in the class! we have to drop "constness" by replacing "const" with "new"
  new.name(int);
  factory.new(int);.
  factory.name(int x)
  foo(int); 
  bar(int);
}

My point is that the constructors should be featured more prominently, or else it's hard to tell them from static methods.

Thinking more about it, maybe ctor is not a bad idea? After all, dart has int, bool, num, var, typedef, in other languages we find pub, mut, fn, impl (to name a few), and no one protests. On the other hand, sooner or later dart will have to introduce longer keywords like immutable (or unmodifiable), for which there's no common abbreviation, and then constructor won't feel out of place.

@lucavenir
Copy link

lucavenir commented May 11, 2024

My two cents: I wouldn't like a new keyword. Instead, the already defined new fits fine with the rest of the language (e.g. constructor tear-offs); I'm familiar with it. It's explicit, it's easy to understand. new const is also fine imho.

@charafau
Copy link

Not sure if I like this. Makes more exceptions and doesn't bring any value

@TekExplorer
Copy link

TekExplorer commented Jul 26, 2024

I actually really don't like this proposal.

I get it, but it reads too weirdly, and costs us readability. There is too much difference with how we normally do it.

Treating const and factory as functions feels... Really bad. That's not what they are, but it's what the semantics imply.

Worse, factory.new and const.new implies that they're objects which is even worse.

Why don't we just... Use .?

Just straight up remove the type name, and do something like this:

class Foo {
  final int x;
  .(this.x);
  // (this.x); // thoughts?
  // .new(this.x); // explicit
  const .(this.x);
  factory .(int x) {...}
  const factory .(int x) = SubFoo;
  
  const .zero() : x = 0;
  factory .parse(String source) {...}
}

It retains the existing syntax, and makes it pretty obvious that it's distinct from normal methods.

All of the benifit, none of the... Weirdness.

I would have no f'n clue what the hell a factory.new or a const() is supposed to be

Far too much cognitive overhead with the proposal as it is.

Consider this alternative?

In other words, remove the type identifier (and add a dot for the unnamed one, or otherwise allow for us to use new as a "named" constructor for this)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

7 participants