-
Notifications
You must be signed in to change notification settings - Fork 207
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
Generic and conditional constructors #4213
Comments
I like this idea :) This can go even further in the future with support for conditional conformance for methods. In such case is this syntax flecmxible enough to support that without need to reinvent that? |
(Summary derived at after having written the below: I think this can work. I just want more, but I think it'll be compatible.) I like the idea, and I'm pretty sure I've suggested something similar before, members that only exist in some instantiations, and constructors with limited return types. (Found them: #1899, #3181). I do find the syntax a little repetitive and unintuitive. So, again, I'll suggest "type patterns". Rather than having to specify type variables in one place, just to be able to infer them and use them in another place, you can specify a type variable inside something that today has to be a type term, and thereby achieving type destructuring. I've suggested Llist<final T> value = expr; as a syntax, but it doesn't work as well as I'd like. So my new suggestion is to let an unprefixed That would make it With that in place, we can use type patterns fx as the types of extensions extension Foo on List<<X>> {
X get center => this[length ~/ 2];
} That's not much different from today, but it suggests what to do with limited constructors and members: a pattern on the static type/this: class A2<X, Y> {
final X x;
final Y y;
A2(this.x, this.y);
A2<<X>, List<X>>.name(X x): this(x, [x]);
S get A<<S extends String>, <_>>.stringName => x;
}
void main() {
A2.name(1); // Has type `A2<int, List<int>>`.
A2.("Foo", 42).stringName;
} These work as both static type matchers, only allowing you to invoke the constructor or member of the receiver type matchers the type pattern, and as your destructors for type parameters, maybe function type parts, extracting the actual runtime type parameter's type as the runtime binding of the introduced your variable. (It also works in actual patterns, only for type arguments, I do want type patterns, and if we get those, they would fit in will with this feature, so I'd be wary of introducing a second and incompatible syntax for the same thing. However, it might not be incompatible. If you can put a type pattern before methods and constructors, the pattern can refer to type variables introduces by the method or (generic) constructor itself, as type values. That, plus generic corticoid, gives this proposal, and then we can extend that with your patterns latter, for an inline/pattern-based variable declaration rather than a separate variable declaration. (The same way I want to allow |
@lrhn wrote: Right, and we also have #2313, and the basic idea has been brought up in the language team several years earlier.
The type patterns proposal in #170 is quite broad, it is basically an outline of a whole family of mechanisms with something that relates to the phrase type patterns at the core. However, type patterns won't quite fit as a drop-in replacement of regular type parameters. First, all of the type pattern mechanisms that I can recall included the property that the pattern-declared type parameters couldn't be passed explicitly. For example, with an existential-open mechanism based on type patterns, we'd introduce the type parameter in the right-hand operand of a type test, and it would be bound to a value as part of the subtype check: void main() {
List<num> xs = <int>[1]; // We forgot the precise type argument.
num n = 1; // Precise type forgotten.
// Existential open: Allow us to re-discover the types.
if (xs is Iterable<final T>) { // Yields true and binds `T` to `int`.
if (n is T) xs.add(n); // Safe.
}
} In this case it certainly doesn't make sense to pass the value of Another example where type patterns came up is this comment in this issue. Here is a declaration that embodies the core of that issue: // The type `dynamic` comes from the original example, shouldn't matter here.
class BlocBuilder<TBloc extends Bloc<dynamic, var TState>> {...} In this case the point is that we declare It is again crucial that This differs from just declaring the type parameter separately: class BlocBuilder<TBloc extends Bloc<dynamic, TState>, TState> {...} We can do this today, and with the new 'inference-using-bounds' feature in Dart 3.7.0 we can also get a good and tight inferred value for So that's my first point: Type patterns aren't a good match for this task because they are inherently intended to be computed from other type arguments, they can't be passed explicitly. In particular, we can't do simple things like this: class A<X, Y> {
A<<X>, <Y>>(...); // Declare two type parameters using patterns.
...
}
void main() {
A<int, String>(); // Error!
A.new<int, String>(); // Error!
} The errors arise because the constructor doesn't accept any actual type arguments. This means that generic constructors can't be invoked with explicit type arguments, unless we somehow introduce some normal type parameters as well. Even with normal type parameters as well, we can't pass the pattern-declared ones explicitly, unless we come up with some mechanism to do so (perhaps those type parameters could be optional and named?). This implies that we certainly don't have a straightforward way to re-interpret all constructor declarations as generic constructors. Even the simplest cases where we "don't do anything" with the type parameters can't be expressed using type patterns, we'd have to support a way to introduce regular type parameters as well. They could be the type parameters of the class, but in that case we can't express the case where the constructor takes additional type parameters (like The next difficulty is that we can't control the ordering of type parameters that are introduced by type patterns, and we can't express type parameters that aren't used in other types. class A<X, Y> {
// Using this proposal.
A<X, Y>.new<Y, X extends Y, Z>(Z z);
// Using type patterns.
A<<X extends Y>, <Y>>(Z z); // `Z` is undefined?!
} Even if we assume that we can pass those type parameters explicitly, we don't have a way to specify any particular ordering. We could have a convention that says "the pattern-declared type parameters are passed in declaration order". We would then pass Next, pattern-declared type parameters are bound by decomposing other type arguments, which is different from finding actual type arguments by inference. In particular, we must choose a particular direction for the dependencies. class A<X, Y> {
// Using this proposal.
A<X, Y>.new<X, Y, Z>(Z z, List<Z> zs);
}
void main() {
A<String, double> a = A(1, <num>[]); // X:String, Y:double, Z:num.
a = A(1, []); // X:String, Y:double, Z:int, `[]` is `<int>[]`.
} Type inference binds For a type pattern based approach, we could try to introduce a notion of pattern-declared type parameters in the value parameter types: class A<X, Y> {
// Using type patterns? Is this readable.
A<<X>, <Y>>.name1(<Z> z, List<Z> zs);
A<<X>, <Y>>.name2(Z z, List<<Z>> zs);
}
void main() {
A<String, double> a = A.name2(1, <num>[]); // X:String, Y:double, Z:num.
a = A.name1(1, []); // X:String, Y:double, Z:int, `[]` is `<int>[]`.
} With Note that we must call All in all, I think type patterns are very powerful and I expect them to be very useful in many situations. But I don't think they'd work very well as drop-in replacements for regular type parameters. In fact, I think we should have generic constructors (a la this proposal, or whatever) using perfectly normal type parameters, and we should also have some of the features where type patterns are basically the perfect way to proceed (existential open, decomposition of structured types). I view this proposal as a way to explain what constructors of generic classes are doing, generalizing it to handle type arguments of the constructor itself as well as type arguments of the class: They are essentially just a different notation for generic static methods. This makes the current proposal very simple (and low risk), and it does support the extra expressive power that led to the use of words like 'conditional' and 'generic' together. |
That makes it a question of what the pattern destructures. There are actually two kinds of destructurings: Runtime type destructuring and type argument destructuring. It means that: void withBloc<T extends Bloc<var X>>(T value) { ... } and void withBlock(Block<var X> value) { ... } behave differently. I think both type destructurings are useful, with the former likely being slightly easier to implement than the latter. It doesn't require the same kind of existential open - not on runtime types, only on type argument types, which usually means that it's a statically known type. Or at least a type that could have been denoted at the call point. |
This is a proposal about a reinterpretation of constructors in Dart that allows them to have higher expressive power, similar to the additional power of constructors in the proposal about conditional instance members and constructors.
First note that the proposal has no effect on constructors of non-generic classes (and mixin classes, enums, extension types, etc, but I'll just say 'class' here for brevity).
In contrast, the interpretation of constructors of generic classes changes. The core idea is that a constructor does not have access to the type parameters of the enclosing class. Instead, the current notation is just an abbreviated form. When the underlying (non-abbreviated, full) form is used, every constructor must declare all the type parameters they need.
In the full notation, the name of the constructor must be of the form
<typeIdentifier> '.' <identifierOrNew>
. We can use this kind of name without changing the meaning of the declaration by using.new
when the original constructor name was of the form<typeIdentifier>
.In the full notation, in this case where it corresponds to the current form, the constructor declares the same type parameters with the same bounds as the class, up to alpha equivalence (that is, we can freely rename the type parameters as long as it is a consistent renaming). I've used new names in the example above in order to emphasize that the type parameters
X1 .. Xk
are not available in the constructor which uses the full notation, but the new type parameters play exactly the same role as the type parameters of the class do in the current notation. When it is result of this kind of desugaring, the full notation has the same semantics as the current notation.However, the full notation allows for more expressive power, because it is possible to specify that a given constructor can only create some generic instantiations of the given class. For example, we might have a very strictly confined constructor that only supports a fixed type:
The constructor
A1.name
can make the parameter optional, because it's guaranteed that it will never create an instance ofA1<T>
for any otherT
thanint
. This is safer than having a constructor that tries to use anint
as a default value forx
, but actually can't statically make it an error to try to create anA1<String>
.We could also have a constructor that imposes an extra constraint on a type argument:
This means that we can use the constructor
A2.name
to conveniently create a special kind ofA2
object where they
is a list that contains thex
(initially, at least), and we can declare the constructor such that this relationship between the type parameters and the instance variable values is enforced by declaring it explicitly.A typical example in the same vein would be a class where a constructor argument can be made optional in the case where the type arguments provide a guarantee:
For the static analysis, it is an error if it does not follow from the declarations of type parameters of a constructor that the resulting type arguments of the class will satisfy the bounds. For example:
The mechanism would be applicable to factory constructors as well as generative constructors, and it would be applicable to constructors that are redirecting as well as the ones that aren't.
Type inference on invocations of generic constructors would proceed as if they were generic static methods.
There is one question where the answer isn't obvious: Should it be allowed at the call site to provide the type arguments to the class, such that the invocation is similar to current constructor invocations?
I'd recommend that we are very careful when it comes to constructor invocations where the type arguments are passed to the class (as in the constructor invocations that we have today). In particular, it seems highly confusing that a generative constructor named
A6
would work in such a way thatA6<Object>()
evaluates to an instance ofA6<String>
.We could simply make it an error for an invocation of a generic constructor to receive the type arguments at the class (which is treated as a context type for an invocation of the corresponding static method). Alternatively, we could recommend that the analyzer emits a warning. The developer could then choose to make it
A6<String>()
in order to get rid of the warning.In any case, these considerations are relevant for explicitly provide type arguments, not for inferred ones.
For all those cases where no extra expressive power is needed, the current notation is available, with the same semantics. In particular, they will definitely continue to support the provision of type arguments to the class. We could (and probably should) treat constructors declared with the full notation the same if they are the result of a desugaring of the current notation (that is: declares same type parameters as the class, up to alpha equivalence, and passes all of them, and nothing else, to the class, in the same order).
@dart-lang/language-team, WDYT? This would allow us to obtain the expressive power that we've had in some proposals about static extensions, but using a mechanism that works in regular classes as well as static extensions (and perhaps even more places).
The text was updated successfully, but these errors were encountered: