-
Notifications
You must be signed in to change notification settings - Fork 213
Constructor tear-off of forwarding factory constructors is under-defined. #3427
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
So how do we understand the nature of a redirecting factory constructor? Note that it is quite similar to an abstract instance member declaration in several ways: abstract class A {
A._();
factory A([int x]) = B;
void foo([int x]);
}
class B extends A {
B([num x = 3.25]): super._();
void foo([num x = 3.25]) {}
}
So we should probably have specified constructor tear-offs to use the ultimate redirectee when a redirecting factory is torn off, and given the tear-off expression the function type of the redirecting factory. In the example, this means that the tear-off expression Compared to the approach used today, this would preserve the static type of tear-off expressions, and it would yield a run-time value whose type is a subtype thereof. In other words, this would be a non-breaking change (except that This removes the need to do anything magic/inconsistent about this kind of tear-off, because there is no need for a forwarding function with special powers wrt default values, and there is no need for new concepts like functions that are forwarding an actual argument list (which is not something that we can write in Dart). I don't see a need to introduce any special magic for generic function instantiation: If a generic function has one or more default values then they must be type correct for every possible actual type argument list, and hence there's no need to invoke the special power of "passing on the actual argument list". In the case where we would introduce a more powerful kind of default values such as non-constant expressions in the scope of the enclosing function declaration, we would presumably not be able (or willing) to prevent side effects. This would make the ability to "pass on the actual argument list" observable, which makes it more of a liability to sneak it in at this time. |
Tearing off the (transitive) target is a sound solution. It's not without its own leakage, though. An alternative is to say that synthetic methods are allowed to have a default value which isn't assignable to the parameter type. (If we do introduce non-constant default values, then forwarding the actual arguments list is probably precisely what we'd want, rather than having to specify copying of an expression outside of its scope.) |
Actually, this shouldn't matter because the declaration of language/specification/dartLangSpec.tex Line 4618 in c214fd9
However, this error apparently hasn't been implemented. So we need to consider whether we want to implement it (a breaking change, perhaps with some actual breakage), or we want to change the specification such that it isn't an error anyway. In the latter case we must of course specify what to do with that ill-defined default value. The language specification says that a redirecting factory specifies a call to another constructor, not a constructor in itself, and it says that the invocation of a redirecting factory gives rise to an invocation of the redirectee with the exact same actual argument list. All these elements match up perfectly with the perspective that there is no entity corresponding to a redirecting factory, it is just like an abstract member signature, and it's all just a compile-time device that allows us to access the ultimate redirectee using a different static type and a different name. At run time we get the redirectee, no ifs and buts.
That's a very good point! It doesn't quite match up with the language specification, but it seems to be in line with the constructor tear-off feature specification. However, if we want to support the specified semantics and turn a redirecting factory constructor into a real thing then we need to specify that the torn-off redirecting factory constructor is a function that has optional parameters as declared, and it forwards every call with exactly the same actual arguments. No default values needed, just a magic ability to determine whether or not an optional argument was passed, and then a body that is able to call the redirectee passing an argument to every possible subset of the optional parameters. This is a capability that we might want to provide in some other situations as well (in order to obtain a faithful forwarding function), so we probably want to turn that concept into a proper language mechanism. It would not necessarily be a feature which is accessible to developers (perhaps we don't specify any concrete syntax for it, at least not at this time), but the mechanism must be well defined. |
So it is. Since we have had code violating that restriction on the SDK platform libraries, it's probably at least a little dangerous to enable it now. And mostly unnecessary, we haven't had any problems with it before constructor tearoffs.
That's one approach, and I'm all for having it available - it can solve other similar issues too, but for this particular issue, it's sufficient to allow that particular function to have a default value which is not a subtype of its parameter type. No matter how it is done, it will be detectable to the user that the function behaves differently when you don't pass an argument, to how it behaves for any allowed argument. They just can't see how it's done, which is a great place to be for implementation freedom. (That is: we should specify how things behave, extensionally, not how they should be implemented.) |
Note that forwarding functions would allow us to get the desired semantics (and not make the situation described in this issue a compile-time error). |
Just a note that we've seen confusion due to this
what was intended was that So making the mismatch an error would have helped in this case. Another case where it has shown up is when the concrete constructor is generated. In that case the exact constructor type is not very visible, so it's easy to typo it --> also probably good for it to be an error if there is a mismatch. Thanks. |
About the ability of a redirecting factory constructor to declare formal parameters with more strict types than the redirectee: class FooImpl implements Foo {
FooImpl([num x = 1.5]);
}
abstract class Foo {
factory Foo([int x]) = FooImpl; // The default value would be a type error here.
}
void main() {
Foo(); // Creates a `FooImpl(1.5)`, but we can't pass that argument explicitly.
} There is an error which is currently specified, but not implemented, and the spec might be changed to make it a non-error. However, that's only about the situation where the parameter is optional in the redirectee, and it has a default value whose type isn't a subtype of the type of the corresponding parameter of the redirecting factory. So we'd actually need a check (a lint?) that flags all cases where the formal parameter types in the redirecting constructor are proper subtypes of the ones in the redirectee, not just the cases where we have the default value with those extra properties mentioned above. |
This came up again recently. I think we should implement the compile-time error as specified, and at least assess the actual breakage. When It is a compile-time error if a formal parameter of $k'$ has a default value
whose type is not a subtype of the type annotation on the corresponding
formal parameter in $k$. For example, the following is an error (because the default value for the parameter class A {
factory A([int i]) = _B;
}
class _B implements A {
_B([int? i]);
}
void main() => A(); One reason why we may not want to bless the current behavior is that it allows us to emulate the feature where a function bool receivedAnArgument1<X>([X? x]) {
final receivedAnArgument = x != null;
print('Working hard, and X is $X');
return receivedAnArgument;
}
class _Secret {
const _Secret();
}
const _secret = _Secret();
extension type receivedAnArgument2<X>._(bool _b) implements bool {
factory receivedAnArgument2([X x]) = receivedAnArgument2._widen;
receivedAnArgument2._widen([Object? x = _secret]) : _b = _helper<X>(x);
static bool _helper<X>(Object? x) {
final receivedAnArgument = x != _secret;
x as X;
print('Working hard, and X is $X');
return receivedAnArgument;
}
}
void main() {
print('--- 1');
// 1 works for non-nullable types.
print(receivedAnArgument1(1)); // 'true'.
print(receivedAnArgument1()); // 'false'.
// 1 does not work for a nullable type.
print(receivedAnArgument1(null)); // 'false', should be `true`.
// But 1 eliminates null safety at call sites.
int? i = null;
receivedAnArgument1<int>(i); // Sure, no problem!
print('--- 2');
// 2 works.
print(receivedAnArgument2(1)); // 'true'.
print(receivedAnArgument2()); // 'false'.
print(receivedAnArgument2(null)); // 'true'.
// receivedAnArgument2<int>(null); // Compile-time error.
}
As I mentioned, I'd recommend that we implement the type-incorrect implicitly propagated default value as an error, as specified. If we add support to the language for determining whether or not an optional parameter has actually been passed, then we can always eliminate the error and generalize the semantics of redirecting factories in a way that uses this feature. |
And I still think we should specify the forwarding of the argument list, and let implementations do that. It's occasionally useful, it's sound, and it matches implementations. There is no need to define a desugaring, especially not one introducing more restrictions than we actually need. Invoking (non-dynamically) a redircting factory constructor with an argument list has the same runtime behavior as invoking its target constructor with the same argument list. Invoking a torn-off redirecting factory constructor function with an argument list should have the same behavior as invoking target constructor with the same argument list. (Or as invoking the torn-off target constructor function with the same argument list.) And that applies to instantiated generic-class constructor tear-offs too, with the actual mapping of type arguments to the target constructor type. abstract class C<T extends Object> {
T get value;
factory C([T x]) = D<T>;
}
class D<T extends Object> implements C<T> {
final T value;
D._(this.value);
factory D([T? x = null]) => x == null ? const N() : D<T>._(x);
}
class N implements D<Never> {
Never get value => throw StateError("No value");
const N();
}
void main() {
C<int> c1 = C<int>();
C<int> c2 = C<int>(42);
c1.value; // throws
} This is all sound and well, and works today. Changing that would be breaking, with no actual benefit to anyone. (I think I have had more uses for this with extension types than with classes, but that's a valid use too.) |
It almost works: void main() {
var c = C<int?>(null);
print(D<int?>._(null)); // Yes, we can create an object like this.
c.value; // Throws.
} So So perhaps it should have been I think this illustrates that if we want to equip Dart with a mechanism that includes a proper forwarding semantics then we should introduce forwarding as a language feature. Providing support for proper forwarding was the starting point for #3444. Another way to achieve the same thing would be to introduce several more primitive operations which are able to work together to provide the true forwarding semantics. If we have language support for forwarding then it will be well-defined and well tested, and developers can use it wherever they need it. We can then use it for redirecting factories, and it won't be a bug-like exception. |
It should have been And I don't consider this a "bug-like exception". The original forwarding factory constructor design intended to forward the entire argument list. |
The constructor tear-off specifiction defines the behavior of a tear-off as "equivalent to" tearing off a corresponding static function, which has the same type parameters as the class, the same parameter list as the constructor, and a return type which is the class-type instantiated with those type arguments.
That definition doesn't work for all redirecting factory constructors with optional parameters, because those cannot have default values, which makes the corresponding static function also not have default values, even if the optional parameter's type is non-nullable. That is, the "corresponding static function" is not a valid Dart function.
(And this is, again, why we shouldn't define things in terms of desugaring.)
The current approach cannot be saved by saying that we should use the default value of the (transitive) redirect target's parameter, which must also be optional and theregore surely have one. That parameter can have a wider type, and a default value which isn't valid for the redirecting factory's parameter.
Example:
There is no valid desugaring which can introduce a Dart function for
C.new
which behaves like callingC.new
should(that is, calling that function works just like calling
C.new
with the same arguments would).I suggest we rewrite the places in the specification where we introduce a wrapper which copies default values, into forwarding argument lists.
Constructor tear-off:
(or something to that effect.)
Basically accept that not all function signatures have default values, even where user-declared functions must have them.
The same principle should be applied to generic function instantiation, implicit or explicit. It introduces a new function with a signature which has no type arguments and where the parameter and return types replaces the type parameters with the instantiating type arguments. Calling that function invokes the original function with those type arguments and the same argument list. Again, without any attempt to copy default values, because there are functions which do not have default values - even if it's only redirecting factory constructor tear-offs, for now.
The text was updated successfully, but these errors were encountered: