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

Allow passing constructors as functions #216

Closed
vsmenon opened this issue Feb 11, 2019 · 55 comments
Closed

Allow passing constructors as functions #216

vsmenon opened this issue Feb 11, 2019 · 55 comments

Comments

@vsmenon
Copy link
Member

vsmenon commented Feb 11, 2019

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
Copy link

@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) dart-lang/sdk#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
Copy link
Member

lrhn commented Feb 12, 2019

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
Copy link
Member

eernstg commented Feb 12, 2019

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
Copy link

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
Copy link

DanTup commented Feb 12, 2019

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
Copy link

andreashaese commented Feb 12, 2019

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
Copy link
Member

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
Copy link

DanTup commented Feb 12, 2019

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
Copy link

dcov commented Feb 13, 2019

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 😄

@DanTup
Copy link

DanTup commented Feb 13, 2019

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
Copy link
Member

eernstg commented Feb 13, 2019

@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
Copy link
Member

eernstg commented Feb 13, 2019

@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
Copy link

andreashaese commented Feb 13, 2019

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
Copy link
Member

eernstg commented Feb 13, 2019

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
Copy link

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
Copy link

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
Copy link
Member

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

See #216 (comment)

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
Copy link

@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
Copy link
Member

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
Copy link

zoechi commented Feb 14, 2019

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

@eernstg
Copy link
Member

eernstg commented Feb 14, 2019

@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
Copy link
Member

eernstg commented Feb 14, 2019

@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
Copy link

andreashaese commented Feb 14, 2019

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
Copy link
Member

eernstg commented Feb 14, 2019

@tatumizer wrote:

we are in full agreement now

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

@andreashaese
Copy link

andreashaese commented Feb 14, 2019

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
Copy link

andreashaese commented Feb 14, 2019

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
Copy link

zoechi commented Feb 15, 2019

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

@lrhn
Copy link
Member

lrhn commented Feb 15, 2019

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
Copy link

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

robert-ancell added a commit to robert-ancell/x11.dart that referenced this issue Sep 7, 2020
This requires converting the reply factory constructors to static methods since
Dart doesn't support passing constructors as functions yet:
dart-lang/language#216
@Levi-Lesches
Copy link

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

@munificent
Copy link
Member

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

@ferhatb
Copy link

ferhatb commented Feb 5, 2021

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

@Levi-Lesches
Copy link

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

@rockingdice
Copy link

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
Copy link

rockingdice commented Mar 3, 2021

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
Copy link

rockingdice commented Mar 4, 2021

@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
Copy link
Member

lrhn commented Mar 24, 2021

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
Copy link

Levi-Lesches commented Mar 24, 2021

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.

@mit-mit mit-mit changed the title Proposal: allow passing constructors as functions Allow passing constructors as functions Apr 7, 2021
@lrhn
Copy link
Member

lrhn commented Apr 13, 2021

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
Copy link

Levi-Lesches commented Apr 14, 2021

@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.

@Levi-Lesches
Copy link

Levi-Lesches commented Apr 14, 2021

When tearing off the constructor of a generic class, the function tear-off is always instantiated so the resulting function is not generic. This works the same way as instantiated tear-off of any other function, except that it is not an option to not instantiate when tearing off. If type inference has no constraints on the type arguments, they will be filled in by instantiate to bounds.

@lrhn Just so I understand:

class Foo<T> {
  T field;
  Foo(this.field);
  Foo.named<E>(List<E> list, this.field);
}

var a = Foo.new;                   //  Foo<dynamic> Function(dynamic)
var b = Foo.named;                 //  Foo<dynamic> Function(List<dynamic>, dynamic)

var c = Foo<int>.new;              // error?
var d = Foo.named<int>;            // error?

If so, how about this instead:

var a = Foo.new;                   // Foo<dynamic> Function(dynamic)
var b = Foo.named;                 // Foo<dynamic> Function(List<dynamic>, dynamic)

var c = Foo<int>.new;              // Foo<int> Function(int)
var d = Foo<int>.named;            // Foo<int> Function(List<dynamic>, int)
var e = Foo<int>.named<String>;    // Foo<int> Function(List<String>, int)

@Levi-Lesches
Copy link

Levi-Lesches commented Apr 14, 2021

Maybe you misunderstood the example. In Map<K, V>, K is for the keys and V is for the values. The E in Map.fromIterable<E> would mean that instead of passing in an Iterable, you pass an Iterable<E>. Currently, there is no E and thus it's automatically Iterable<dynamic>. In other words, even if the user passes in a List<int>, the functions don't know that and expect a dynamic parameter:

final Map<String, int> map = Map.fromIterable(
  [0, 1, 2],  // clearly an Iterable<int>, but...
  // ERROR: String Function(int) cannot be assigned to String Function(dynamic)
  key: (int index) => index.toString(), 
  // ERROR: int Function(int) cannot be assigned to int Function(dynamic)
  value: (int index) => index + 1
);

Changing both index parameters to dynamic fixes the issue, but it's weird (and unsafe) that Dart forces you to use dynamic instead of actual types here. As I said in my earlier comment, we can fix this by simply introducing an E type argument:

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

So now the example becomes:

final Map<String, int> map = Map.fromIterable<int>(
  [0, 1, 2],  // Iterable<int>
  key: (int index) => index.toString(),  // String Function(int)
  value: (int index) => index + 1  // int Function(int)
);

Based on this, point of generic constructors is to sync up the types of the parameters, just like regular functions can.

@lrhn
Copy link
Member

lrhn commented Apr 14, 2021

@Levi-Lesches Yes. Whether to allow the explicitly specified type arguments in tear-offs is an open question. I started out with a proposal without it, to keep the proposal small, but if it can be included, then I'm all for it (#123).

@mit-mit mit-mit added this to Being spec'ed in Language funnel Apr 20, 2021
@mit-mit mit-mit moved this from Being spec'ed to Ready for implementation in Language funnel Jun 16, 2021
@mit-mit mit-mit moved this from Ready for implementation to Being implemented in Language funnel Aug 5, 2021
@kevmoo
Copy link
Member

kevmoo commented Nov 18, 2021

@mit-mit – is this done? Just waiting for release to close?

@leafpetersen
Copy link
Member

Released in the last beta, shipping in the upcoming 2.15 stable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
Development

No branches or pull requests

17 participants