-
Notifications
You must be signed in to change notification settings - Fork 205
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
Comments
@tatumizer thanks for the comment. I see the ambiguity in the unnamed constructor case. 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 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! |
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 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 It won't be done that way, though. We probably have to constrain the tear-off to type literals, so |
We can make 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 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 |
I'd love this feature! I also agree that |
|
It seems impossible to implement a named constructor called I'm obviously not working on the Dart compiler, so my ideas are naïve at best. But could Dart not synthesize a struct Person {
let name: String
}
let alice = Person(name: "Alice")
let people = ["Alice", "Bob"].map(Person.init) Notice also how |
My hope is that we'd get used to it quickly and it would stop seeming weird 😄
It isn't allowed.
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 |
Ah, I hadn't considered that - I guess it's a reserved word. Feels a little less weird now.
IIRC, you can normally call a |
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 |
Oops.. it was late.. for some reason I had something like |
@andreashaese wrote:
Constructor names are generally of the form // Today.
class MyNameIsIrritatinglyLong {
MyNameIsIrritatinglyLong();
}
// Possible alternative.
class MyNameIsStillIrritatinglyLong {
new();
} Of course, it could be helpful to standardize on this such that a search for |
@vsmenon wrote:
We don't have to allow the bare
Dunno. ;-) |
Edit: You beat me to it. Shorthands of 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 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. |
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 |
class MyNameIsStillIrritatinglyLong {
new();
new named();
} |
Thanks for the great discussion! To summarize what I understand so far:
The last comments in the discussion above focused on how default constructors are treated. Named constructorsThey 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 constructorsThere was some concern about contextual ambiguity. @eernstg's suggested making the default constructor be optionally called
If I understand correctly, that would mean that there would be two valid ways to refer to the default constructor: 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 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 From a user's perspective, I intuitively think that if I can write Factory constructorsAs far as I could tell and test, at call sites, factory constructors necessarily fall into either named or unnamed/default. As an example, the 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. |
See #216 (comment)
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 |
@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 I don't come from a Java background, for my human eyes when I have a The language now permits As it stands today, the complexity lies in considering what is 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
|
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 Here is the first concrete example I hit in a quick code search: https://docs.flutter.io/flutter/widgets/BuildContext/inheritFromWidgetOfExactType.html
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
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
|
Should this be aligned with getter/setter tear-off syntax? |
@rhcarvalho wrote:
What I meant was a bit different: Constructor declarations would be allowed to use But constructor references would not use The only connection between the tear-off of the form
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 |
@tatumizer wrote:
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 If we were to put a broader and more semantic angle on this then we might expect to be able to use Btw, I do want to be able to denote the class of
I'd recommend that we consider allowing So, for the tear-off related feature, |
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:
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 |
@tatumizer wrote:
Exactly, including "it's exactly the sibling thing"! ;-) |
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 Edit: I probably misunderstood you (assumed a proposed tear-off syntax of |
I probably used poor names in my example, but 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 |
I just thought unified syntax for tear-offs was the goal. |
Other things to worry about is generics. If you do a tear-off of List<T> Function<T>() listCreator = List.new; will not work. The constructor is not generic, the We could allow constructor tear-off to treat class generics as function generics, so the And we do want constructors to be generic, independently of the class being generic. class List<E> {
...
List.mapped<S>(E value(S element), Iterable<S> sourceElements) : this() {
for (var element in sourceElements) this.add(value(element));
}
} The We don't have a good syntax for making the unnamed constructor generic, though. Maybe you just can't. |
|
Any updates on this? It's been quite a while... |
No updates, sorry. We've been very busy on null safety. |
@munificent, @leafpetersen : please see this PR for another use case : flutter/gallery#423. It would make this quite a bit less boilerplaty |
I'm excited to see this implemented with |
Looking forward to this feature! 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 |
But it turns out I still cannot make every argument using the default value if the value is omitted. Someone finds the best practice to use the default value is not to use them at all - 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! |
@tatumizer Definitely a historian view ;) I agree with you. The default values problem should be considered, it's a major difference between But it's a little off-topic, we could discuss more on this issue: |
I wrote up a draft proposal for this, listing the "simplest" approach and some of its issued and workaround for those issues. Admin comment: this is now https://github.com/dart-lang/language/blob/master/accepted/future-releases/constructor-tearoffs/feature-specification.md |
With the limitations described, I would be in favor of using the EDIT: After participating in #1564 I've switched my opinion to |
After discussion on #1564, I've update the constructor tear-off proposal to use |
@tatumizer A good example is 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, |
@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) |
Maybe you misunderstood the example. In 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
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. |
@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 – is this done? Just waiting for release to close? |
Released in the last beta, shipping in the upcoming 2.15 stable. |
Support tearing-off and passing constructors similar to current support for functions. Example:
https://github.com/dart-lang/language/blob/master/accepted/2.15/constructor-tearoffs/feature-specification.md
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):
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:
What I have to write instead with Dart 2.1.0:
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.
The text was updated successfully, but these errors were encountered: