dart-lang / language

Design of the Dart language
Other
2.67k stars 205 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.

Levi-Lesches commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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

kevmoo commented 2 years ago

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

leafpetersen commented 2 years ago

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