_From @rhcarvalho on February 11, 2019 10:59_
This feature request intends to allow using constructors where functions with the same type/signature are expected.
Example code: https://dartpad.dartlang.org/9745b0f73157959a1c82a66ddf8fdba4
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.
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.
What I would like to write:
return Column(
// This won't compile:
children: ['Alice', 'Bob', 'Charlie'].map(Text).toList(),
);
What I have to write instead with Dart 2.1.0:
return Column(
// Need a dummy wrapper around the constructor:
children: ['Alice', 'Bob', 'Charlie'].map((name) => Text(name)).toList(),
);
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.
dart --version)Dart version 2.1.0 (build 2.1.0-dev.9.4 f9ebf21297)
_Copied from original issue: dart-lang/sdk#35901_
The expression "Text" would be ambiguous, normally it would mean a type object (Type), not a function.
Your request is equivalent to implementing contextual disambiguation:
var t = Text; // t is Type
Type t = Text; // t is Type
Function f = Text; // f is Function
I'm not sure there's a precedent in dart for this.
Text constructor as a function would probably be better denoted as Text.new to avoid confusion.
For named constructors, the problem does not arise: List.from can have only single interpretation (Function)
The idea was indeed discussed on multiple occasions over the years.
@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!
If you make the Type callable somehow, it solves this problem but creates another one instead.
E.g.
var listAsFunc = List; // listAsFunc has type Type, not Function, but OK, assume it's callable
//...
var list=listAsFunc(parameters); // calling Type as Callable
In this case, compiler has no idea (in compile time!) what these "parameters" are, because Type can as well relate to Map or Foo or anything else, with completely different parameters.
Much simpler approach is just use var listAsFunc = List.new, then the problem simply doesn't arise - compiler infers the signature of the function automatically.
Speaking of history - sure, there were not only proposals, but solutions (called constructor tear-offs).
The feature was discontinued (so to speak) for the reasons I don't remember well, but the syntax was a bit ugly, to tell you the truth :)
What felt surprising to me is that almost everything that is "callable" in Dart can be used as a function, except for constructors.
Not everything. There's also an issue of converting getters and setters into functions. But it might as well be considered a completely separate issue, unrelated to constructors (IMO). Note that I'm not a part of the dart team, just a humble historian :)
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.
So you are against the form Object.new - which would make it consistent with tear-offs of named constructors?
Compare
children: ['Alice', 'Bob', 'Charlie'].map(Text).toList()
children: ['Alice', 'Bob', 'Charlie'].map(Text.new).toList()
Which one is more readable? (at least to me, the first variant looks a bit enigmatic :)
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").
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.
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? :-))
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(...).
Text.newseems 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
constconstructor?
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.
It seems impossible to implement a named constructor called
newat 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
constconstructor?
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)
Dart doesn't have the concept of const functions, only const constructors. When you tear off the constructor, it becomes a regular function to which the notion of constness doesn't apply, so the use of Person.const would be misleading.
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:
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.
@andreashaese wrote:
It seems impossible to implement a named constructor called
newat 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 : there are 2 potential problems here:
class MyNameIsStillIrritatinglyLong {
someMethod() {
var c = new; // tear-off, looks strange a bit, no?
var myInstance= c();
}
}
Java 8 introduced the syntax ClassName::new, and by now, everyone got used to it already. So if dart settles on ClassName.new, this form would be familiar to users and won't raise a lot of controversy IMO
Of course, in this case constructor invocation like ClassName.new(params) becomes possible automatically - it's simply equivalent to ClassName(parameters). Is it much of a concern? If not, the syntax is 1) simple 2)intuitive 3)familiar. What not to love here? :)
@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. ;-)
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.
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".
But then, in tear-off, you will have to write ClassName.new.named too. That's possible, of course. Maybe not a bad idea?
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();
}
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.
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
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 :-)
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:
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.
Is there anything fundamentally wrong with the last line? Why do we need
List.newinstead of justList?
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
@eernstg : there's always a strong temptation to kill several birds with one stone, but in this case, I'm afraid, there's only one bird available :) There's a reason why java doesn't allow referring to "this class" other than by full class name. And my speculation is that the rationale goes approximately like this.
There's a fundamental difference between "this instance" (denoted by "this") and "this class". "This instance" is always uniquely defined. But if we introduce "this class", it can be ambiguous: it may refer to what java calls "this.getClass()" and dart calls "this.runtimeType", or it may refer to the class from lexical context. I think it can lead to subtle bugs.
Also, the problem of introducing a short alias for "this class" doesn't end here: it would then be logical to provide a shorter notation for static methods, too. Which is not addressed by "new" at all.
Maybe we can decouple these problems? Allow ClassName.new as an alias for default constructor, and call it a day? (Factory constructors have natural names already, so ClassName.named is not problematic).
@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.
One more option is to make Type a parametrized type, like in java:
var t = Text; // static type of t is: Type<Text>;
var texts = myList.map(t); // compiler can statically know about t's association with Text
Not sure this is a sufficient condition though. And whether it can be taken advantage of in other contexts.
Further, if t is Type\
(Not sure it all makes any sense - please take it with a grain of salt :)
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
someFunctioncould 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
Xfor whichX(arg)makes sense, then.map(X)should also be valid for anyX.
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.maphas a clear signature, it takes a function that itself takes one argument and returns a value that will be part of a newIterable. There is never a case where I intend to pass an instance of aTypein 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.
Should this be aligned with getter/setter tear-off syntax?
@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:FooandFoo.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).
@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.newas 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".
@eernstg : sorry, I misunderstood! You meant constructor declaration, not invocation inside the class.
I'll try to summarize it all, maybe we are in full agreement now.
class MyNameIsStillIrritatinglyLong {
new() { ... constructor body ...} // means the same as MyNameIsStillIrritatinglyLong() {...}
new named() { ... constructor body } // note the space between `new` and `named`
void someMethod() {
var inst = new(); // ERROR
}
}
// tear-offs
var l = list.map(Text.new); // OK
var l = list.map(Text); // ERROR
var l = list.map(Text.named); // OK
var l = list.map(Text.new.named); // ERROR
var tearoff = Text.new; // OK
var tearoff = Text.named; // OK
var x = Text.new("foo"); // ERROR or WARNING?
I think it's not bad. Default constructor declaration using new(){...} rhymes with tearoff syntax, so tear-off looks less like an ad-hoc feature - it has a sibling now. Probably, that's what you meant, right? :)
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 ['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?
@tatumizer wrote:
we are in full agreement now
Exactly, including "it's exactly the sibling thing"! ;-)
Some loose ends:
Should this be aligned with getter/setter tear-off syntax?
Getters and setters are easy to closurize, e.g. ()=>obj.myGetter, similarly for setters. Whatever new syntax is introduced, it won't be much shorter, but might be more cryptic IMO. The reason the same doesn't work for constructors is that constructors tend to have lots of parameters, In contrast, getter has always 0 and setter has only 1.
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?
Not
int getterAsTearOff() => this.value;
But
Function getterAsTearOff() => ()=> this.getter;
"Tear-off" means: convert the thing into a normal function
You are not supposed to create a function getterAsTearOff in the class (no point)
Example: "length" is a getter in String
Then
["bar", "baz"].map((s)=>s.length) gives you the list of lengths.
Whatever other syntax you could come up with, won't be simpler or easier to understand.
E.g.
["bar", "baz"].map(it.length#); // is it really better? note Kotlin-style "it" - dart doesn't have it
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).
I just thought unified syntax for tear-offs was the goal.
I was just curious if this is still the case.
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.
List<T>.new? (Feels way too simple to not be a stupid suggestion)
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).
@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.
}
YES!!! We are having a philosophical debate! Love it!
Or must you write
D.new<int>(42, id)for no (locally) obvious reason?
I think I'd prefer to not allow generic unnamed constructors.
In addition to syntactic reasons (no word to attach \<..> to), there's a semantic reason.
"new" doesn't carry much of a meaning to attach a generic to. The construct just won't make sense for a reader.
For example, if you have FancyList\
If you have a constructor FancyList.from and invoke it as FancyList\
Unfortunately, a (rather strong) argument can be made against declaring a constructor using "new". Right now, the syntax of constructor declaration matches that of constructor invocation. If we declare a default constructor as new() {...} then the user might expect that at least inside the same class, it can be also invoked as new(parameters). If you later introduce "This" to refer to the class in a lexical context (which I think is not a bad idea), it would create a third form of invocation as This(parameters), This.named(parameters) etc.
Ideally, 3 things should match syntactically: 1) tear-off 2) constructor declaration 3) constructor invocation. With T.new, we might be creating more problems than we solve.
To minimize the damage, we can use the only word that distinguishes default constructor among other constructors. This word is (surprise!): "default". So maybe we can settle on T.default, which can be used anywhere where T can be used - including in the declarations? Even if allowed only for tear-offs, this will be just a small wrinkle in the language, which might be tolerable.
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
C(params...) and C.named(params...) would work (of course)C.new(params...) might work, but probably not:"not even supporting instance creation (
ClassName.new(42)might as well stay asClassName(42)"
new(params...)wouldn't work from inside the classThis(params...)andThis.named(params...)wouldn't work because Dart doesn't inherit constructors
However, it's not ambiguous
I didn't mean to say it's ambiguous - just inconsistent and likely confusing.
Consider
class Foo {
new() {...} // default constructor, synonymous with Foo() {...}
someMethod() {
var foo = new(); // this form cannot be supported - it's an error. Try to explain to user ...
// ... why new() is an error - just 1 line above we defined it exactly as new()
var foo1 = Foo.new(); // an error again, we decided to outlaw this form
}
}
As for "This" - it's a different story. "This" can mean only "the class in lexical scope". It might, or might not, be relevant to this discussion.
My whole point was that Foo.default might be a better choice than Foo.new, because it can work in all 3 contexts without causing too much controversy. We don't have to allow it in one place and disallow in another - it's just a synonym for Foo everywhere, except where we mean Foo as a type as opposed to tear-off, and vice versa.
Plus, you can attach type parameter to Foo.default\
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.
Would default() not have the same issues as new()?
It would have issues, but not "the same issues". Less issues. :)
"default" is consistent with the principle of simplest explanation, aka Sutton's law
In case some overly picky user pretends he doesn't understand why Foo() means the same as Foo.default(), all we have to do is to state the obvious: default means "default" by default.
Which can be further abbreviated to just "b/c default". That's it. With "new", this doesn't work - it requires quite a bit of scaffolding, with "new" declaration syntax, rules of usage ("new" is OK in one place, banned in another etc.), just to make it less of a kludge, but a picky user will easily see through it :-)
What if we just allowed
Foo.new()?
As a synonym for Foo() everywhere? Because "new" does not map to the idea of "default" in our head, but maps to the idea of invocation of the constructor (not necessarily a default one). The appeal of "new" here, IMO, is just that it's a shorter word - but this argument is not a very strong one.
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:
CC() and C.named() needs to be retainedOf course invocation syntax C() and C.named() must be retained. But you can optionally call C() as C.default() if you feel so inclined.
I've actually used it to represent neutral or preset instances (e.g.
NetworkProvider.default).
Are you sure? "default" is listed as a reserved word (no matter in what context). You shouldn't be able to create such constructor (in fact, I double-checked it in the latest dartpad - got an error).
As to why "new" was proposed - probably, for no other reason than "new" being a short word somehow related to constructors. Can't speculate though. I was among those who proposed it, by the way. It was just the first thing that came to mind - shorter words are closer to the surface :)
"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.
Won't it be exciting if we could refer to default constructor neither as "new" nor as "default", but by "constructor with empty name"? Because that's what it is - the constructor with empty name!
Good news: due to back ticks introduced by #271, we will be able to write it as List.``
Which exactly matches the concept of "constructor with an empty name"!
In declarations, we mostly won't need to specify empty name explicitly unless we want to add type parameters:
class C<X,Y> {
C.``<Z>() {...}
}
var constructor = String.``; // tearoff
var constructor = C<int, String>.``<int>; // tearoff
@andreashaese : no, my suggestion was to allow C.default() and a complete synonym for C(), and C.default with no parentheses when we want to tear it off, with no other changes whatsoever - so that all constructors are considered named, it's just a default one has the name default by default.
But now I think C.`` would be a better choice
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?
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.
@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()
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.
Similar to operator tearoff (see https://github.com/dart-lang/language/issues/376#issuecomment-554179156) we can support the forms
var constructor= constructor Foo;
var constructor= constructor Foo.named;
It's less cryptic than the alternatives IMO.
Any updates on this? It's been quite a while...
No updates, sorry. We've been very busy on null safety.
Most helpful comment
@andreashaese wrote:
Constructor names are generally of the form
C.norC, whereCis the name of the enclosing class, and the latter one is sometimes described as nameless, default, etc. We could allow fornewas an alternative syntax:Of course, it could be helpful to standardize on this such that a search for
newwill always find the "nameless" constructor, and there could be other arguments thatnewshould not be used any more at all, etc. But it would fit well with the use of terms likeText.newfor a tear-off. ;-)