-
Notifications
You must be signed in to change notification settings - Fork 214
Revisit unimplemented factory constructors and default values #4172
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
Comments
My opinion is to not change anything here other than possibly augmenting the definition of "potentially redirecting" to indicate that the presence (or absence) of a default value on an optional, non-nullable parameter will determine the type of the constructor (as in redirecting or not). It isn't intended to be a feature that an augmentation can alter this property of the constructor, this "potentially redirecting" definition is only there to describe how we handle the case where we can't know from the introductory declaration, not to try and add flexibility. Edit: If we don't allow providing the default value but no body today, we should allow that. |
I do want to introduce flexibility. If a user writes a constructor signature, and adds a macro to it, I do want to give the macro the flexibility to choose the implementation. That said, any number of details can lock the declaration onto being redirecting or not:
It really is just the plain signature that is ambiguous. |
One approach we could use would be to say that the default value can be specified in an otherwise pluripotent (sounds better than 'ambiguous' ;-) constructor declaration, and if it is an error to specify that default value in the resulting constructor definition then it will simply be ignored. We might want to maintain that it is an error if there are two conflicting default values in the same augmentation chain, or we could even allow that to enhance the overall flexibility. Example: class A {
A([int i = 0]);
factory A.foo([int i = 1]); // OK.
}
augment class A {
augment factory A.foo([int i]) = A; // OK, and the "inherited" default value is ignored.
}
class B {
B([int i = 0]);
factory B.foo([int i = 1]); // OK.
}
augment class B {
augment factory B.foo([int i]) { // OK, and the default value is "inherited".
return B(i);
}
}
class C {
C([int i = 0]);
factory C.foo([int i = 1]); // OK.
}
augment class C {
augment factory C.foo([int i = 2]) { // Error?
return C(i);
}
} Taking one step back, I think we have the following main players when it comes to constructor augmentation: graph LR
subgraph Factory
direction BT
fnr["Non-redirecting<br>factory A(int i) => B(i);"] --> fp["Pluripotent<br>factory A(int i);"]
fr["Redirecting<br>factory A(int i) = B;"] --> fp
end
subgraph Generative
direction BT
gnr["Non-redirecting<br>A(int i) {...}"] --> gp["Pluripotent<br>A(int i);"]
gr["Redirecting<br>A(int i) : this(i + 1);"] --> gp
end
This is already somewhat inhomogeneous because the pluripotent generative constructor is usable as such, but the pluripotent factory is a new invention (an 'unimplemented' factory), and it is an error unless there is an augmentation which will implement it (by redirection or by a body). The fact that some of these arrows can be blocked by parameter default values makes the situation one more bit inhomogeneous. |
Perhaps we should go a step further and simply say that it is not a supported feature to change the kind of constructor using augmentation? (It's a mess!) So we would accept an introductory declaration like If you wish to declare a redirecting constructor in an augmentation chain then it just needs to be introduced as such. If there is no I think this implies that there would not be any situations where a default value can be declared in the introductory declaration, and it becomes an error to have it in the augmenting declaration, or vice versa. This takes away a little bit of expressive power, but it also gives rise to a more consistent (and less confusing) model. graph LR
subgraph Factory
direction BT
fnr["Non-redirecting<br>factory A(int i) => B(i);"] --> fp["Pluripotent<br>factory A(int i);"]
fr["Redirecting<br>factory A(int i) = B;"]
end
subgraph Generative
direction BT
gnr["Non-redirecting<br>A(int i) {...}"] --> gp["Pluripotent<br>A(int i);"]
gr["Redirecting<br>A(int i) : this(i + 1);"]
end
|
We should in general have a principle that we don't allow you to change any which could be introspected on, because that leads to different introspection results in different phases. And, we probably do need to give you the ability to check if a constructor is redirecting or not, because it informs how you can augment the constructor. Therefore, I think we are left with really only two reasonable options:
|
Could be (Redirecting and generative are not mutually exclusive. We have almost all eight combinations of
Then it's not possible to augment a redirecting generative constructor, because it can only be redirecting if it has written a redirection, and we don't allow changing that redirection. I would like that flexibility, to be able to write a signature and let a macro worry about the implementation, without my signature being taken as an implementation in itself. Maybe allow |
Now that augmentations don't need to support every possible thing a macro could want to do, I think we have the opportunity to simplify a bit here. Here's a proposal:
In short:
We could allow mixing them in a future language version if really compelling. But, honestly, I would much rather go in the direction of enhanced constructors (#4246), potentially const statements (#4261), and function forwarding (#3444). I think there's a path there that would lead us to not needing redirecting constructors at all. Simplifying constructors would be really nice. What do you think? |
I really think we should allow an "API declaration" that declares what the API properties of a member, including a constructor, should be, and let something else define the implementation. Whether a constructor is So I want to be able to write @GenerateIt()
class A {
A(int id);
factory A.byName(String name);
} and not lock a code generator into I'd rather have the opportunity to allow the generator to decide the implementation strategy, and risk errors, I prefer the variant of of augmentation where you can only add implementation once (like #4203) because it avoids a lot of ordering issues, and the entire We can also allow multiple augmentations that modify implementation, they just have to agree. If they don't, it's a compile-time error, and you should stop having two code generators modify the same thing if they can't agree. If you end up with class A {
const A(int id);
A copyWith({int? id});
}
// @<generated id="GenerateIt" v="1.1.5" t="20250213111902" h="0x25af397bc9">
class A {
final int _id;
final String _trace;
const A(int id) : this._(id, "A.id($id)");
A._(this._id, this._trace);
A copyWith({int? id}) => A._(id ?? this._id, "$trace.copy(${id ?? ""})");
String toString() => _trace;
}
// @</generated>
// @<generated id="other" ... >
class A {
final int id;
const A(this.id);
int get hashCode => id.hashCode ^ 14f37;
bool operator ==(Object other) => other is A && id == other.id;
}
// @</generated> then we have a problem. There are two implementations of In this case we can layer these on top of each other and have a reasonable result:
It's not necessarily an issue to have a non-redirecting generative constructor on top of a redirecting (it works if your super-cosntructor is redirecting too, it just takes more steps to get to the actual initializing constructor.) It would be an issue if the generated parts were swapped, though, because a redirecting generative constructor Which really means that augmenting (generative) redirecting constructors is fine. We can do that easily. Maybe(!) we can allow one redirecting constuctor too, if we think of it more like a super-constructor-invocation. It's not easy. It might not be possible, but I really do want to try before saying that you can't augment or augment with redirecting constructors. (But I can see that they don't play well with augmentation, because they jump out of the augmentation chain.) |
I went ahead and scraped a pub corpus to see how common various kinds of constructors are:
From skimming the results, it looks like the majority (but not all) of factory redirecting and const factory redirecting constructors are coming from freezed generated code. That suggests to me redirecting constructors are useful for code generators and that that augmentations should play nice with them if possible. The initial issue here isn't about augmenting constructors adding redirection in general (which the proposal supports). It's about how that interacts with default values. Say you've got a introductory constructor with an optional non-nullable parameter. The declaration may be As it currently stands with the proposal:
That does not seem particularly flexible or elegant to me. I propose that we take the notion of "can only complete once" and do something similar for default values: The introductory and augmenting declarations can omit or provide default values, but a given parameter can only receive a default value from one declaration (introductory or augmenting). An augmenting constructor can choose to fill in a body or a redirection. Once that's all done, we see if the resulting declaration makes sense (i.e. has no default values if it's a redirecting factory, or does have default values otherwise). Further, we remove the compile-time error that prevents a redirecting factory constructor from providing default values. Yes, if you write them, they will be dropped on the floor, so it lets users do something confusing and meaningless. But it doesn't seem any more confusing and meaningless to me than: abstract class C {
foo([int x = 1]);
} We allow that and the world hasn't ended, so we can probably be a little looser in redirecting factory constructors too. Some consequences:
How does that sound? Thanks for catching this tricky corner case, Erik! |
@munificent wrote:
Very nice analysis! So we'd have the following relaxations:
Moreover, the semantic factory constructor which is defined by an introductory factory and zero or more augmenting factory declarations must specify exactly one default value for each parameter which is optional and whose type is potentially non-nullable, and it must specify at most one default value for each parameter which is optional and whose type is nullable. The tricky case is when a factory constructor has default values in the introductory declaration, and it is turned into a redirecting factory by an augmenting declaration. In this case we could allow it and ignore the default values (because they are not supported with redirecting factories). This is what the rule above implies. Alternatively, we would generalize redirecting factories to support default values that differ from the ones in the redirectee declaration (this implies that the redirecting factory would be a full-fledged function, not a notation which is fully eliminated at compile time; this is already true for the tear-off, anyway; see also #3427). I don't think there is a need to change the rules for generative constructor declarations, so they can just specify their default values in the introductory declaration, as before, which is also what we do with all other kinds of functions. We could allow "exactly one default value" to mean "one or more constant expressions whose values are identical" or "exactly one constant expression". I'd prefer the latter, because constant expressions with identical values may be easy to define and handle, but it doesn't generalize nicely to things like "you can specify one or more function bodies as long as they are equivalent" (that would be a canonical example of a Turing tarpit). |
If we only allow one augmenting declaration to provide implementation, then I think we should just consider default values (like initializing formals and We can loosen that to allow "documentation default values" which are not real, but exist only for documentation. We can then require a later actually-implementing augmenting declaration to declare the same default value, or redirect to something with the same default value, otherwise it's an error. Or we can leave it as something the analyzer can warn about. So:
I'd go with the first. If people really, really like to use default values for documentation, I'd go for the last. |
Hello there! As I read this, a few things come to mind:
facctory Example([int a = 0]) = Redirecting; Because if not, that makes this discussion pretty much useless for Freezed (as we want the And given that Freezed is the main user of redirecting factories today, that'd feel odd to me :P
typedef Example = void Function([int a = 0]);
... In terms of conflict resolution in cases of "What to do with the default / What if there's a default mismatch?" IMO the only real answer is to completely ignore default values as part of function comparison (with maybe a warning) and treat it purely as a documentation thing. One reason is: We can already override methods with default values, with a different default: abstract class A {
void fn([int value = 0]);
}
class B extends A {
// Legal
@override
void fn([int value = 42]) {}
} |
Let's see... Looking at individual constructor parameters:
And aggregating per constructor:
So default values aren't very common in general. It's redirecting constructors where this really matters, so just looking at those:
(Obviously, 0% of redirecting factory constructors have default values since the language prohibits that.) How common are redirecting constructors?
So the only kind of redirecting constructors where you could provide a default value are quite rare today. Out of all 233,346 constructors measured, only 77 were redirecting constructors with any default values. Overall, it seems like no, users don't really really like to use default values (for documentation or otherwise). |
I don't think that works out. Abstract methods can have default values, but we still want augmentations to be able to fill in bodies for them. I suppose we could say that an augmentation can only fill in a body for an abstract method if the method doesn't have any default values... but that feels pretty arbitrary. The default values are already merely documentary in an abstract method (for better or worse), so it seems strange to treat them as so meaningful that they prevent an augmentation from adding a body. I think maybe the easiest thing to do is to leave the proposal as it is. It means augmenting constructors are limited in the ways that Erik points out but... users can just add or remove the default values in the introductory declaration if the limitations implied by them are a problem. If you want an augmentation to be able to fill in your factory constructor with a redirecting body... then don't write any default values. The situation might be different in a world with macros where users might want to be pretty oblivious to what the macro is doing to their code. But with code generators, there's a pretty tight coupling, and I think it's reasonable for a code generator to say "hey, I'm going to add a redirecting body to your factory constructor so no default values for you." |
Fwiw there's a non-zero amount of redirecting factories with default values in the wild. factory Class({@Default(0) int a}) = Redirecting; |
@rrousselGit wrote:
Certainly. This would be a new feature which would require the redirecting factory to be an actual entity rather than syntactic sugar for invocations of the redirectee. We already need this for a tear-off, by the way: class A {
A();
factory A.redir([int i]) = B;
}
class B implements A {
B([int i = 1]);
}
void main() {
print("Redirecting constructor: ${A.redir.runtimeType}"); // '([int]) => A'.
print("Redirectee constructor : ${B.new.runtimeType}"); // '([int]) => B'.
} The fact that those two function objects have (and should have!) different types show that the constructor The reason why redirecting factories do not support default value declarations on parameters is probably that these constructors weren't supposed to exist, they were just meant to be syntactic sugar. However, they do exist today, because of the semantics of constructor tear-offs. It would simply be the natural next step in this evolution to introduce support for default values in the redirecting constructor declaration. We would need to allow the default values to be omitted, too (to avoid massive breakage), and the omitted default values would then be taken from the redirectee (preserving current behavior). This is consistent with the treatment of super parameters, so it would be in line with other parts of the language. I still want to use the same rules as elsewhere, so I'd recommend that it is an error if the resulting default value is a compile time error: abstract class A {
factory A.redir([int x]) = B; // Error.
}
class B implements A {
B([num x = 1.5]);
}
Alternatively, we could introduce a proper notion of forwarding and specify that the invocation of the redirectee will use the default values of the redirectee even in the case where this value would be a compile-time error if specified as the default value in the redirecting declaration. If we go this way then there would be a larger number of questions to resolve, but I would like to have support for proper forwarding in Dart, one way or the other. So the overall answer is: Yes, the redirecting constructor could declare default values, but it takes a couple of language design steps before we're there.
Adding default values to function types is a non-trivial step, too. We could make
Right, that's another reason why I wouldn't want to add default values as a full-fledged element in function types. Also, if it's treated as documentation then it wouldn't need to be in the language, and it might presumably be handled via metadata. |
Here's a proposal: #4393. |
And then they can't be augmented.I can live with that. Default values is implementation, not signature. We can also allow an abstract declaration with default values, but then an implementing augmentation shouldn't have to use the same default value. It can, it likely wants to, but nothing prevents implementing the abstract method by adding a mixin as a superclass, which has a different default value. Why bother preventing it for an augmentation. (I do think we should do the same thing for instance and static members and for constructors.) I think I'd go with the latter: you can write default values if you want to, but unless it's the implementing declaration, they're just comments, and have absolutely no effect on anything. |
Uh oh!
There was an error while loading. Please reload this page.
We need to introduce an extra rule about factory constructors that are subject to augmentation: They can omit default values.
With augmentations, a factory constructor can be unimplemented. It is possible to make a late choice about the nature of this constructor (it could be redirecting or it could be non-redirecting):
We may need to introduce an extra rule about these unimplemented factory constructors which says that it is allowed for an optional parameter to omit the default value clause, even in the case where the parameter type is potentially non-nullable.
For example:
The general rules about augmentations state that no default value can be specified by an augmentation, it must always be specified by the introductory declaration (if there is to be a default value for that parameter at all).
This implies that the declaration above would not allow
A.foo
to be augmented with a class body (such that it is a non-redirecting factory constructor): The parameter type cannot be modified by an augmentation, and an augmentation cannot provide a default value. In other words, the fact that there is no default value has eliminated the expressive power to make the decision about the factory being redirecting or non-redirecting, it's always forced to be redirecting.We could allow the unimplemented factory constructor to include the default value and then ignore it (because it would be an error to have it) when the constructor is augmented to be redirecting:
If we do not allow this then the choice to have a default value will force the constructor to be non-redirecting.
In other words, the ability to "decide late" whether a given factory is redirecting or not will disappear as soon as there is one or more optional parameters whose type is potentially non-nullable. Of course, we can then make the declarations inconsistent: If we have two such parameters and provide a default value for exactly one of them then no augmenting declarations can be written, because they will give rise to a compile-time error no matter what.
@dart-lang/language-team, WDYT? Are you willing to allow a default value to (1) occur in an augmenting declaration, and/or (2) be declared in the introductory declaration, but then erased in the final definition because tit would be an error?
The text was updated successfully, but these errors were encountered: