dart-lang / language

Design of the Dart language
Other
2.65k stars 202 forks source link

Allow passing constructors as functions #216

Closed vsmenon closed 2 years ago

vsmenon commented 5 years ago

Support tearing-off and passing constructors similar to current support for functions. Example:

class Text {
  String label;
  Text(this.label);
}

void main() {
  // Tear-off the `Text` constructor and pass it to the `map` function.
  List<Text> l = ['foo', 'bar', 'baz'].map(Text.new).toList();

  // Print all text labels.
  for (var t in l) {
    print(t.label);
  }
}

Original issue filed by @rhcarvalho on February 11, 2019 10:59 as dart-lang/sdk#35901

This feature request intends to allow using constructors where functions with the same type/signature are expected.

Example code: https://dartpad.dartlang.org/9745b0f73157959a1c82a66ddf8fdba4

Background

As of Dart 2, Effective Dart suggests not using the new keyword and removing it from existing code. Doing that makes named constructors and static methods look the same at call sites.

The Language Tour says (emphasis mine):

Constructors Declare a constructor by creating a function with the same name as its class [...]

Thus suggesting that a constructor is a function. But it turns out it is not really a function, as it cannot be used in all contexts where a function can.

Why it would be useful

Dart built in types, in particular collection types, have several methods that take functions as arguments, for instance .forEach and .map, two concise and expressive ways to perform computations.

In an program I'm working on, I got a bit surprised by not being able to create new instances using Iterable.map + a named constructor. And then I realized that others have arrived at the same conclusion at least 4 years back on StackOverflow, but I could not find a matching issue.

Example Flutter code

What I would like to write:

  return Column(
    // This won't compile:
    children: [
      Heading('Children:),
      ...['Alice', 'Bob', 'Charlie'].map(Text.new),
    ]
  );

What I have to write instead with Dart 2.1.0:

  return Column(
    // Need a dummy wrapper around the constructor:
    children: [
      Heading('Children:),
      ...['Alice', 'Bob', 'Charlie'].map((name) => Text(name)),
    ]
  );

Note that the feature request is not specific to Flutter code, but applies to any Dart code as per the more generic example in https://dartpad.dartlang.org/9745b0f73157959a1c82a66ddf8fdba4.

rhcarvalho commented 5 years ago

@tatumizer thanks for the comment. I see the ambiguity in the unnamed constructor case. It seems to me that a similar disambiguation happens for example with Callable Classes:

class Greeter {
  final String name;

  Greeter(this.name);

  void call(String who) {
    print('${name} says: Hello ${who}!');
  }
}

void main() {
  var t1 = Greeter('Callable class 1');
  print(t1.runtimeType); // Greeter

  Function t2 = Greeter('Callable class 2');
  print(t2.runtimeType); // (String) => void

  ['Alice', 'Bob'].forEach(t1);
  // Callable class 1 says: Hello Alice!
  // Callable class 1 says: Hello Bob!

  ['John', 'Mary'].forEach(t2);
  // Callable class 2 says: Hello John!
  // Callable class 2 says: Hello Mary!
}

I originally run into this when I wanted to pass in a named constructor to List.map. What felt surprising to me is that almost everything that is "callable" in Dart can be used as a function, except for constructors.

If the proposal was limited to consider only named constructors, I feel it would create a usability problem, by introducing more exceptional behavior instead of generalizing a concept.

I found now this similar issue (but AFAIU different) https://github.com/dart-lang/sdk/issues/10659, confirming other people have run into this in the past. Apparently using constructors as functions was a thing at some point and then got removed.

Looking forward to hearing more feedback. Thanks!

lrhn commented 5 years ago

There is a precedent in Dart for changing the meaning of expressions based on context type. We do it for double literals, generic function instantiated tear-off and callable objects.

Usually we only change the behavior if the existing behavior is a compile-time error (like assigning a Type to a function type).

The way to tear off the unnamed constructor is the only non-trivial part of constructor tear-offs. Using the context type to enforce the conversion is very similar to how we convert callable objects to functions, as if we are treating Type objects as having a call method.

It won't be done that way, though. We probably have to constrain the tear-off to type literals, so Function f = Object; works, but Function f = (Object); does not, because that's just an arbitrary expression with type Type. We need the static link to the class to know whether there is a constructor to tear off.

eernstg commented 5 years ago

We can make Text work, but the larger amount of mental work that we pile up (for every developer, all the time) because the source code is ambiguous (and you don't know what it means before you have computed a lot of types, maybe in your head), the more we are asking developers to waste their time doing disambiguation.

I think it's very easy to underestimate this kind of work, and it's probably worthwhile to spend some syntax on making the disambiguation explicit. This makes Text.new more attractive than Text + compiler smarts.

Comparing with int-to-double conversion, I think it's fair to say that the difference is smaller with the numbers (there's a big difference between the semantics of getting a type reification and tearing off a constructor, but evaluating 1 to an integer or to a double sort of "means the same thing"), so choosing between a Type and a function object requires a deeper kind of double-thinking. Similarly, comparing with generic function instantiation we also have the same kind of entity (without the conversion f is a generic function, with the conversion it is a non-generic function; but it is, arguably, still "the same function").

andreashaese commented 5 years ago

I'd love this feature! I also agree that Text.new is more obviously a constructor than Text in the described context, especially in concert with named initializers. It's a bit unfortunate that new doesn't have an actual implementation in the type (so you can't search for it by name), but that could be mitigated if IDEs treats new as a symbolic link to the unnamed initializer.

DanTup commented 5 years ago

Text.new seems a slightly weird to me, it looks like a defined member (and what happens if there is one with that name? or if it's a const constructor? :-))

andreashaese commented 5 years ago

It seems impossible to implement a named constructor called new at the moment, so I guess that would not interfere with existing code. Excuse my ignorance, what would change if it was a const constructor?

I'm obviously not working on the Dart compiler, so my ideas are naïve at best. But could Dart not synthesize a new constructor during compilation, that mirrors the accessibility and signature of the provided default constructor and forwards any parameters to the default one? In Swift, when you define a type (Struct) and don't provide an initializer, the compiler does this:

struct Person {
    let name: String
}

let alice = Person(name: "Alice")
let people = ["Alice", "Bob"].map(Person.init)

Notice also how Person(...) and Person.init(...) are similar to Text(...) and Text.new(...).

natebosch commented 5 years ago

Text.new seems a slightly weird to me

My hope is that we'd get used to it quickly and it would stop seeming weird 😄

what happens if there is one with that name?

It isn't allowed. new is a reserved word and so it cannot be the name of a constructor, static, or instance member.

or if it's a const constructor?

Shouldn't be a problem. All const constructors also work as non-const constructors. If you can reasonable tear-off the unnamed constructor, referring to it as ClassName.new shouldn't have any impact other than disambiguation against the Type instance.

DanTup commented 5 years ago

It seems impossible to implement a named constructor called new at the moment

Ah, I hadn't considered that - I guess it's a reserved word. Feels a little less weird now.

Excuse my ignorance, what would change if it was a const constructor?

IIRC, you can normally call a const constructor with either const or new and it'll do different things (eg. if you call using const twice with the same args, you'll get the same instance, but you won't if you call using new). It's not clear which behaviour you'd get here if it was a const constructor and you used Person.new? Maybe it could be handled by supporting Person.const in addition to Person.new. (I'm just thinking out loud, I also don't work on compilers/language design)

dcov commented 5 years ago

What about doing:

data.map(new Text);

It's not the best looking but it gets the point across that it's a constructor and not a function, while also allowing for named constructors:

data.map(new Text.rich);

It also gives a reason to use new :smile:

DanTup commented 5 years ago

When you tear off the constructor, it becomes a regular function to which the notion of constness doesn't apply

Oops.. it was late.. for some reason I had something like const ["x", "y", "z"].map(Class.const) in my head, but of course that wouldn't be very constant going through map.

eernstg commented 5 years ago

@andreashaese wrote:

It seems impossible to implement a named constructor called new at the moment

Constructor names are generally of the form C.n or C, where C is the name of the enclosing class, and the latter one is sometimes described as nameless, default, etc. We could allow for new as an alternative syntax:

// Today.
class MyNameIsIrritatinglyLong {
  MyNameIsIrritatinglyLong();
}

// Possible alternative.
class MyNameIsStillIrritatinglyLong {
  new();
}

Of course, it could be helpful to standardize on this such that a search for new will always find the "nameless" constructor, and there could be other arguments that new should not be used any more at all, etc. But it would fit well with the use of terms like Text.new for a tear-off. ;-)

eernstg commented 5 years ago

@vsmenon wrote:

shorthand tear-off within the class will be possible, too (by implication!)

We don't have to allow the bare new for a tear-off inside the class. The fact that it is possible to have a named constructor C.foo and an instance member foo already serves as a hint that we might want to use C.foo rather than foo for constructor tear-offs, also inside C. So we'd just have C.foo and C.new everywhere for tear-offs, which doesn't seem all that inconsistent.

What not to love here? :)

Dunno. ;-)

andreashaese commented 5 years ago

Edit: You beat me to it.

Shorthands of new could be disallowed. In Swift, you're forced to prefix an initializer with its type:

struct Test {
    init() {}

    static func staticTest() {
//        let initializer = init // error
        let initializer = self.init
        let instance = initializer()
    }

    func memberTest() {
//        let initializer = init // error
        let initializer = type(of: self).init // or Test.init
        let instance = initializer()
    }
}

Another solution could be to standardize default constructors to be named C.new as in

class MyNameIsStillIrritatinglyLong {
  MyNameIsStillIrritatinglyLong.new();
}

but I actually find it appealing that I don't have to type the name of the class in the constructor.

eernstg commented 5 years ago

appealing that I don't have to type the name of the class in the constructor

But then we might want this:

class MyNameIsStillIrritatinglyLong {
  new();
  new.named();
}

It might not work in all details, but the short spec would be "In a constructor declaration signature, the name of the class can be replaced by new".

andreashaese commented 5 years ago

new. looks a bit namespace-like indeed. I don't think we would want to change the spelling of named constructors on call sites (ClassName.named(...), otherwise we'd break lots of existing code), and tear-offs shouldn't look different in my opinion. The spec is convincingly easy, though. How about omitting the dot?

class MyNameIsStillIrritatinglyLong {
  new();
  new named();
}
rhcarvalho commented 5 years ago

Thanks for the great discussion!

To summarize what I understand so far:

  1. People are generally positive about the proposal, as it would help smoothing out a rough edge and allow for more concise code following the idiom "DON’T create a lambda when a tear-off will do".
  2. There are two types of constructor to consider: a. Named constructor b. Default constructor Extra: see note about Factory Constructors below, as I have originally not considered them.

The last comments in the discussion above focused on how default constructors are treated.

Named constructors

They seem to carry no controversies, as there is no contextual ambiguity. As a reference, if this proposal is implemented the snippet below should compile in a future version of the language:

  final teas = [
    ['green', 'black'],
    ['chamomile', 'earl grey'],
  ];
  print(teas.map((x) => Set.from(x)));  // ({green, black}, {chamomile, earl grey})
  print(teas.map(Set.from));  // Compilation error in Dart 2.1.0

Default constructors

There was some concern about contextual ambiguity. There are other cases in the language that similar ambiguity is solved.

@eernstg's suggested making the default constructor be optionally called new, so that instead of .map(Foo) one would write the unambiguous .map(Foo.new).

"In a constructor declaration signature, the name of the class can be replaced by new"

If I understand correctly, that would mean that there would be two valid ways to refer to the default constructor: Foo and Foo.new. Both can be used to create an instance, but only the latter can be used as a function (tear-off).

var p = Person('Alice'); // ok, must stay in the language for backwards compatibility
var q = Person.new('Bob'); // ok in "future Dart"

['Mary', 'John'].map(Person.new); // ok in "future Dart"
['Mary', 'John'].map(Person); // ERROR

Did I understand the idea correctly?

I feel introducing more ways of referring to the default constructor would create more variability in code bases, making code harder to read, as everybody will need to learn to read both forms.

While new code that uses new to declare the default constructor would be clear when Person.new is used as a function, old and new code that declare the default constructor using the class name would look confusing.

Consider the snippet:

  final sizes = [1, 2, 3];
  print(sizes.map((x) => List(x)));  // ([null], [null, null], [null, null, null])
  print(sizes.map(List));  // Compilation error in Dart 2.1.0

Is there anything fundamentally wrong with the last line? Why do we need List.new instead of just List?

From a user's perspective, I intuitively think that if I can write X(), then .map(X) should also be valid for any X. I concede that not everyone's intuition works in the same way :-)

Factory constructors

As far as I could tell and test, at call sites, factory constructors necessarily fall into either named or unnamed/default. As an example, the dart:core Set class has a default and several named constructors that are factory constructors:

sdk/lib/core/set.dart

abstract class Set<E> extends EfficientLengthIterable<E> {
  factory Set() = LinkedHashSet<E>;
  factory Set.identity() = LinkedHashSet<E>.identity;
  // ...
}

Thus we can probably concentrate the discussion on the named and default constructors, and factory constructors will follow.

natebosch commented 5 years ago

Is there anything fundamentally wrong with the last line? Why do we need List.new instead of just List?

See https://github.com/dart-lang/language/issues/216#issuecomment-462817333

List is already a valid expression which evaluates to an instance of Type. Making that expression mean different things in different contexts adds ambiguity. The compiler can likely resolve this, it does something similar for callable objects, but it means that a human reader needs to disambiguate as well. Very similar code snippets will have entirely different meaning:

var t = List; // t is a Type
Function f = List; // f is a tearoff of the constructor
someFunction(List); // The argument may be a Type or tearoff depending on the definition of someFunction which isn't visible here
rhcarvalho commented 5 years ago

@natebosch, thank you! Being new to the language myself, I think I may still be missing some context.

I am trying to imagine how common it is to deal with variables of type Type, perhaps it has its uses for reflection. What a typical someFunction could do with a Type argument?

I don't come from a Java background, for my human eyes when I have a X for which X(arg) makes sense, then .map(X) should also be valid for any X. If I have to write instead .map(X.new) (or anything other than just .map(X) for that matter), it feels cognitively heavier to me. I need to think what's special about X that I need to learn and memorize some new syntax for it. And then if I follow through and go see the definition of X and don't see any reference to new (a field? a method? a getter? something defined in a parent class?), then it gets even more confusing.

The language now permits .map(X) for some X where X(arg) is valid, and the style guide suggests that the simpler form should be preferred over wrapping with a lambda, as in .map((arg) => X(arg)). In terms of clarity to the reader, the lambda makes the intention explicit at the call site.

As it stands today, the complexity lies in considering what is X and if it can be used like that or if it requires wrapping.

In Dart I have not seen the types of arguments declared on the call sites, only on the function signatures. Why would it be different for X.new, why do we need to disambiguate at the call site that I am passing a constructor tear-off / function and not a type?

List.map has a clear signature, it takes a function that itself takes one argument and returns a value that will be part of a new Iterable. There is never a case where I intend to pass an instance of a Type in that context.

natebosch commented 5 years ago

I am trying to imagine how common it is to deal with variables of type Type, perhaps it has its uses for reflection. What a typical someFunction could do with a Type argument?

I don't know how common it is, but disallowing it or changing the syntax required would be breaking which is not worthwhile to be able to omit the .new which I think we'll get used to quickly.

Here is the first concrete example I hit in a quick code search: https://docs.flutter.io/flutter/widgets/BuildContext/inheritFromWidgetOfExactType.html

for my human eyes when I have a X for which X(arg) makes sense, then .map(X) should also be valid for any X.

If we didn't have existing code that it would break then the discussion would be more interesting, as is I don't think it's worth considering breaking changes for this.

The counterpoint around consistency is that someMethod(List) has consistency with if (variable is List) where in both cases List refers to the type, and not a tearoff of the constructor.

why do we need to disambiguate at the call site that I am passing a constructor tear-off / function and not a type?

Because there is existing code using that syntax and meaning for it to be a type. As mentioned in multiple comments above we could use the context of the function being called to disambiguate, but that makes it harder for human readers to know what is happening - that is we could use the definition of someFunction to know that we need a tearoff, but that means the human reader needs this same information which may be non-local.

List.map has a clear signature, it takes a function that itself takes one argument and returns a value that will be part of a new Iterable. There is never a case where I intend to pass an instance of a Type in that context.

map() calls are going to be obvious to readers, other calls may not be. It also means that certain refactoring patters that look safe may not be. For instance refactoring values.map(Something); to var construct = Something; values.map(construct); could break, because the context of how Something is use changed from a place requiring a Function to a var.

zoechi commented 5 years ago

Should this be aligned with getter/setter tear-off syntax?

eernstg commented 5 years ago

@rhcarvalho wrote:

"In a constructor declaration signature, the name of the class can be replaced by new"

If I understand correctly, that would mean that there would be two valid ways to refer to the default constructor: Foo and Foo.new.

What I meant was a bit different: Constructor declarations would be allowed to use new in the location where we currently use the class name (so the "nameless" constructor that used to be declared as MyClass(); could now be declared as new();, and MyClass.name(); could be declared as new.name();). This is just a tiny abbreviation that seems natural and convenient, and it maintains the connection between the word new and the concept of creating instances (which is otherwise a bit weaker to day than it used to be, in Dart at least, because new can be omitted).

But constructor references would not use new as part of the name (so the instance creation MyClass() would still be MyClass(), and so would MyClass.name()), except for this single situation: A tear-off of a nameless constructor would have the suffix new (so the two example tear-offs would be written as MyClass.new and MyClass.name).

The only connection between the tear-off of the form MyClass.new and the constructor declarations using new rather than the class name is the fact that they both contain the token new, and the point is simply that these two features might work together to make each other feel more natural. ;-)

Person.new('Bob'); // ok in "future Dart"

We could do that, but I'm not convinced that it's very useful. It's new, and longer, so we'd need a good reason for adding it (and given that we couldn't declare that constructor using Person.new(..);, it doesn't seem very natural to me).

eernstg commented 5 years ago

@tatumizer wrote:

There's a fundamental difference between "this instance" (denoted by "this") and "this class".

I can see where you are going, but it shouldn't be necessary to worry about that here. I did not make any proposals that are intended to make any difference for that distinction. I just suggested that we could use new as an abbreviation of the name of the enclosing class in the signature of a constructor declaration, but that occurrence of the class name is simply a flag that says "this is a constructor", so in that sense it is just a tiny bit of syntactic sugar.

If we were to put a broader and more semantic angle on this then we might expect to be able to use new to denote "something enclosing" (the enclosing class or instance) in some other context. For instance, new could be allowed as an expression in the class, denoting the current class or something like that. But I did not have any intention to go into these broader interpretations, it's just a tiny convenience feature.

Btw, I do want to be able to denote the class of this as well, calling it This (most likely), but that's another topic. Surely we'll go there again, somewhere else. ;-)

Allow ClassName.new as an alias for default constructor

I'd recommend that we consider allowing ClassName.new as a new form of expression that evaluates to a tear-off of the nameless constructor of ClassName, not even supporting instance creation (ClassName.new(42) might as well stay as ClassName(42)).

So, for the tear-off related feature, new is not the name (or part of the name) of a constructor, it's just a flag on an expression that disambiguates it: "This expression will yield a function object which is a tear-off of the nameless constructor", and it's just because ClassName as an expression already means "this will yield a reification of the type ClassName".

andreashaese commented 5 years ago

Love it!!

Would backward compatibility be retained by simultaneously allowing the "old" syntax? If so, would existing code (especially packages) need to be updated if I want to use its constructors as tear-offs, or could it somehow be made compatible automatically?

Edit: Maybe I can attempt to answer the question myself. This proposal consists of two separate parts:

  1. Allowing tear-offs of constructors, at all. This adds new functionality to Dart.
  2. Introduce the new syntax, which makes things easier to understand. This doesn't affect Dart's expressiveness.

Since 1) is purely additive and doesn't depend on 2), I assume it could be done in such a way that existing code doesn't need to be changed: I don't need to wait for Flutter libraries to be ported to use ['Alice'].map(Text.new). 2) is designed such that it can live next to the existing syntax (as long as you don't implement a constructor twice), so while it's recommended to use it, there is no technical pressure to update existing code it in a timely fashion. Does that make sense?

eernstg commented 5 years ago

@tatumizer wrote:

we are in full agreement now

Exactly, including "it's exactly the sibling thing"! ;-)

andreashaese commented 5 years ago

I'm not sure if that's as trivial as it seems:

import 'dart:async';

void main() {
  var test = Test(42);
  foo1(test.getter);
  foo2(test.getterAsTearOff); // <-- simulates tear-off that would potentially also just read "getter"
  test.value = 45;
}

class Test {
  int value;
  Test(this.value);
  int get getter => this.value;
  int getterAsTearOff() => this.value; // <-- simulates the same getter, but that you can tear off
}

void foo1(int a) => Future.delayed(Duration(milliseconds: 200), () => print(a));
void foo2(int Function() f) => Future.delayed(Duration(milliseconds: 200), () => print(f()));

This program prints 42, 45. Depending on the definition of the receiving function, you're either forwarding the tear-off of the getter for the function to evaluate whenever it wants, or evaluate the getter before function invocation. That's similar to the above discussion about cognitive load.

Edit: I probably misunderstood you (assumed a proposed tear-off syntax of obj.getter). What would you suggest?

andreashaese commented 5 years ago

I probably used poor names in my example, but foo2 takes the getter as a normal function. Anyway, I see now, I thought @zoechi was referring to tearing off getters and setters of one specific object instance.

Edit: In Swift you can curry instance methods (not getters though) to achieve a somewhat similar effect:

struct Test {
    init(_ value: Int) { self.value = value }
    private var value: Int
    func add(_ other: Int) -> Int { return value + other } // instance method
    var valueGetter: Int { return value } // computed property a.k.a. getter
}

let t = Test(40)
t.valueGetter // 40
t.add(2) // 42

// Curried:
Test.add(t)(3) // 43

[Test(1), Test(2), Test(3)]
    .map(Test.add) // this is now an array of functions (Int) -> Int
    .map { f in f(2) } // apply the function with some parameter, yield array of Int
    .forEach { i in print(i) } // prints 3, 4, 5

// Test.valueGetter(t) // error, currying only works with methods

Maybe Dart could do something like ["bar", "baz"].map(String.length).

zoechi commented 5 years ago

I just thought unified syntax for tear-offs was the goal. I was just curious if this is still the case.

lrhn commented 5 years ago

Other things to worry about is generics.

If you do a tear-off of List.generate, type inference will fill in the type arguments to List for you. You will not get a generic function, so:

List<T> Function<T>() listCreator = List.new;

will not work. The constructor is not generic, the List class declaration is (a raw List is not a class, List<T> is a class for every T, and if you just write List, then type inference will fill in the type argument for you to create the actual class). There is no constructor to extract without having a class that has all its type arguments. So, that might be confusing.

We could allow constructor tear-off to treat class generics as function generics, so the List.new tear-off becomes equivalent to <E>([int length]) => List<E>(length) rather than ([int length]) => List<dynamic>(length). We don't want to do that, though, because it would block us from having actual generic constructors.

And we do want constructors to be generic, independently of the class being generic. That would allow something like:

class List<E> {
   ...
   List.mapped<S>(E value(S element), Iterable<S> sourceElements) : this() { 
     for (var element in sourceElements) this.add(value(element));
   }
}

The S type parameter is not part of the class, it just enforces a relation between the two arguments, just as if the constructor was a normal generic function. Tearing off that constructor could give a generic function.

We don't have a good syntax for making the unnamed constructor generic, though. Maybe you just can't.

andreashaese commented 5 years ago

List<T>.new? (Feels way too simple to not be a stupid suggestion)

lrhn commented 5 years ago

Not sure what List<T>.new is a suggestion for (maybe a sign that I'm listing too many problems :smile:).

If it's for allowing an instantiated tear-off of an unnamed constructor, then sure (but then Function f = List<int>; would also work, and we might want to enable that syntax anyway, q.v. #123).

If we want to make the unnamed constructor generic, then List<T>.new<S>() is not a stupid suggestion, but it's not perfect ergonomically. It requires you to write new, because List<T><S>() is not something we'd want to allow. (You can omit type parameters, so which one is omitted in List<X>?).

You can call unnamed constructors without the new when they are not generic, or when you omit the type argument of both that and the class. Assume a class with a generic unnamed constructor:

class C<T> {
   final T field;
   C<S>(S value, T convert(S value)) : field = convert(value);
}

You can call it as C(42, (int x) => "$x"), but if you want to provide either type argument, you must write C<String>.new(42, (int x) => "$x") or C.new<int>(42, (x) => "$x"). That's annoying and confusing (adding a missing type parameter means you have to rewrite more of the expression).

Even more confusing, if the class is not generic, like:

class D {
  final int field;
  D<S>(S value, int convert(S value)) : field = convert(value);
}

Can you then write C<int>(42, id)? That would be a pitfall for people working with both generic and non-generic classe. Or must you write D.new<int>(42, id) for no (locally) obvious reason? That's also a usability issue.

I think I'd prefer to not allow generic unnamed constructors. The way the Dart syntax is designed, you need a name to hang a type arguments off, and for an unnamed constructor there isn't one. Introducing new as a name for the unnamed constructor, but only in some cases, and not cases that are predictable to users, is not really an improvement.

(There is also the option of having unspecifiable type parameters, type parameters that can only be filled in by type inference, and which are basically only there to enforce constraints between parameters. I'm not sure I like that idea, having something inferred, but with no way to override the inference, is confusing and error prone as well. It would dodge the lack of syntax for writing the arguments, and it might be useful for other situations as well ... there have been requests for generic operators, which is again a place where you can't write the type argument).

eernstg commented 5 years ago

@lrhn wrote:

So, that might be confusing [that is: to work with generic constructors]

We can always come up with difficulties associated with additional features that we may add to the language at some point. ;-)

However, we only had proposals for generic named constructors so far, because it seemed too much of a pointless exercise to handle those issues (and you also mention this restriction). I don't think there's anything here which is more confusing than all the similar issues that we'll have anyway if we start having two type parameter lists on certain constructors.

class C<X> {
  C();
  C.name<Y>(Y y);
}

main() {
  C<num> Function() f1 = C; // OK, infers class type argument `int`.
  C<num> Function(String) f2 = C.name; // OK, `X` is `int` and `Y` is `String`.

  // Generic function instantiation would happen like it does with functions today.
  f1 = C<int>.new; // Could be allowed.
  f2 = C<num>.name; // Could be allowed.

  // Or explicitly, assuming that we add support for this.
  f2 = C.name<String>; // OK.

  // And we'd then have a generic function as the basic result of the tear-off
  // which makes the others work.
  C<int> Function<Y>(Y) f3 = C.name; // OK.
}
andreashaese commented 5 years ago

For newcomers, there is indeed a certain disconnect between constructor naming new and invocation C() (as you know, intentionally so.)

However, it's not ambiguous: Given a class C

andreashaese commented 5 years ago

Would default() not have the same issues as new()? What if we just allowed Foo.new()? I don't think people will be overly confused by new vs. Foo(), but at the same time I don't think there's a technical reason not to allow it. Also, Foo<S>.new<T>() would be possible.

andreashaese commented 5 years ago

I can see where you're coming from. However, my counter argument would be that C.default isn't unambiguous, either: in some of my projects, I've actually used it to represent neutral or preset instances (e.g. NetworkProvider.default).

I think new was proposed for two reasons: 1) people here are familiar with the old syntax new C(), so it was a natural (certainly biased) choice 2) more importantly, new wouldn't break existing code because it's a reserved word (see my anecdote)

Anyway, whatever the naming is, I'm afraid it will always have this slight consistency issue due to these orthogonal requirements:

andreashaese commented 5 years ago

"default" is listed as a reserved word

Oh, I stand corrected! Must have been defaultInstance or similar then. My point is, that was my interpretation of default in that context. Then again I don't have a strong opinion here.

So your suggestion would be as follows?

class C {
  default() {...};
  new named() {...};
}

If so: what I like about new is that it has essentially become a flag for constructors. Something that I personally value very highly. Of course we could do new default() {...}, but that's indeed a bit wordy.

Levi-Lesches commented 5 years ago

Hey so there's a lot of talk on how to determine whether the code is referring to a Type, default constructor, or const constructor. But what about named constructors? Or factories? That would be easy, right? Or should I just start a new issue so we could keep these two problems separate?

munificent commented 5 years ago

But what about named constructors? Or factories? That would be easy, right?

Right, those are easy. But when we design a way to tear off constructors, we want that one feature to cover all constructors, which means handling both the easy and hard cases.

Or should I just start a new issue so we could keep these two problems separate?

I think one issue is fine. I don't think we'd want to only allow tearing off named constructors, so it's probably not helpful to track it as a separate issue.

slightfoot commented 4 years ago

@munificent regarding the concern for naming why not go back to using the hash/pound symbol? as before. See https://github.com/dart-lang/sdk/issues/10659#issuecomment-149480639

children: ['Alice', 'Bob', 'Charlie'].map(Text#).toList()
children: ['Alice', 'Bob', 'Charlie'].map(Text.named#).toList()
munificent commented 4 years ago

That's weird because you don't need to use # to tear off any other members, so it would be weird to have to use it for constructors. Of course, you could require that for all members, like Dart used to, but that was deeply confusing for users because it didn't match their expectations from other languages and was noticeably more verbose.

Levi-Lesches commented 3 years ago

Any updates on this? It's been quite a while...

munificent commented 3 years ago

No updates, sorry. We've been very busy on null safety.

ferhatb commented 3 years ago

@munificent, @leafpetersen : please see this PR for another use case : https://github.com/flutter/gallery/pull/423. It would make this quite a bit less boilerplaty

Levi-Lesches commented 3 years ago

I'm excited to see this implemented with Foo.new or Foo.default

rockingdice commented 3 years ago

Looking forward to this feature! My scenario is making the widgets dynamically by scripts. So I probably would do this:

Container container = Function.apply(Container.ctor, [], {Symbol("alignment") : Alignment.bottomCenter});

So the named arguments could be generated from a script. And the code to create the Container is simplier than:

Container buildContainer({
  Key? key,
  alignment,
  padding,
  color,
  decoration,
  foregroundDecoration,
  double? width,
  double? height,
  BoxConstraints? constraints,
  margin,
  transform,
  transformAlignment,
  child,
  clipBehavior = Clip.none,
}) {
  return Container(
      key: key,
      alignment: alignment,
      padding: padding,
      color: color,
      decoration: decoration,
      foregroundDecoration: foregroundDecoration,
      width: width,
      height: height,
      constraints: constraints,
      margin: margin,
      transform: transform,
      transformAlignment: transformAlignment,
      child: child,
      clipBehavior: clipBehavior);
}
Container container = Function.apply(buildContainer, [], {Symbol("alignment") : Alignment.bottomCenter});

I have to wrap the constructor into a function, then using the Function.apply to make it happen.

rockingdice commented 3 years ago

Looking forward to this feature! My scenario is making the widgets dynamically by scripts. So I probably would do this:

Container container = Function.apply(Container.ctor, [], {Symbol("alignment") : Alignment.bottomCenter});

So the named arguments could be generated from a script. And the code to create the Container is simplier than:

Container buildContainer({
  Key? key,
  alignment,
  padding,
  color,
  decoration,
  foregroundDecoration,
  double? width,
  double? height,
  BoxConstraints? constraints,
  margin,
  transform,
  transformAlignment,
  child,
  clipBehavior = Clip.none,
}) {
  return Container(
      key: key,
      alignment: alignment,
      padding: padding,
      color: color,
      decoration: decoration,
      foregroundDecoration: foregroundDecoration,
      width: width,
      height: height,
      constraints: constraints,
      margin: margin,
      transform: transform,
      transformAlignment: transformAlignment,
      child: child,
      clipBehavior: clipBehavior);
}
Container container = Function.apply(buildContainer, [], {Symbol("alignment") : Alignment.bottomCenter});

I have to wrap the constructor into a function, then using the Function.apply to make it happen.

But it turns out I still cannot make every argument using the default value if the value is omitted. A Function.apply call on the constructor directly is necessary.

Someone finds the best practice to use the default value is not to use them at all - https://stackoverflow.com/questions/14612914/how-to-optionally-pass-an-optional-parameter

I hope we could do more about the default value, not abandon them. But that's another issue.

Still expecting the Foo.new to come real!

rockingdice commented 3 years ago

@tatumizer Definitely a historian view ;) Thanks for your information. There's a plan B for the Function.apply call. I used it first because I thought I could avoid the default values problem, but it turns out I couldn't. So whether to use it does not matter at all.

I agree with you. The default values problem should be considered, it's a major difference between null and undefined or whatever it names. At least we should get a undefined for argument usage. The caller should have the ability to decide to use the default values or not dynamically.

But it's a little off-topic, we could discuss more on this issue: https://github.com/dart-lang/sdk/issues/33918

lrhn commented 3 years ago

I wrote up a draft proposal for this, listing the "simplest" approach and some of its issued and workaround for those issues. https://github.com/dart-lang/language/blob/master/working/0216%20-%20constructor%20tearoffs/proposal.md

Admin comment: this is now https://github.com/dart-lang/language/blob/master/accepted/future-releases/constructor-tearoffs/feature-specification.md

Levi-Lesches commented 3 years ago

With the limitations described, I would be in favor of using the new Symbol alternative. I also find it's intuitive, and sometimes being explicit can save a lot of pain, both to the compiler and Dart devs.

EDIT: After participating in #1564 I've switched my opinion to Foo.new, mainly since it's similar to named constructors.

lrhn commented 3 years ago

After discussion on #1564, I've update the constructor tear-off proposal to use C.new as tear-off syntax (and a general alternative to naming the unnamed constructor).

Levi-Lesches commented 3 years ago

@tatumizer A good example is Map.fromIterable (it's in the proposal):

Map.fromIterable(Iterable iterable, K key(element), V value(element));  // current
Map.fromIterable<E>(Iterable<E> iterable, K key(E element), V value(E element));  // new

In general, it can be used whenever you have type information about the parameters of the constructor, but is not needed for the object itself. In the Map example, iterable is discarded so E doesn't need to be saved to Map.