-
Notifications
You must be signed in to change notification settings - Fork 208
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
[extension types] Let T
be assignable to a fully transparent extension type with representation type T
#3614
Comments
Does it have to be a non-extension type? It should work just as well adding extension types on extension types. Does it have to be the same type? (Because I really don't like it when things that depend on two types being the same.) It's not restricted to immediate superinterfaces, which is good. Maybe:
Then This only defines assignability, which is effectively an (infalliable) implicit casting coercion. We should probably consider if/how this coercion interacts with other coercions. It's assignability, not subtyping, so it's only skin-deep. var list = [1];
List<int> intList = list; this will not: var lists = [[1],[2]];
List<List<int>> intLists = lists; The latter will see an assignment from Assignability is not subtyping, because it doesn't work on nested types. var lists = [[1],[2]];
List<List<int>> intLists = lists as List<List<int>>; so are we giving people half a feature here? If the only use-cases are something like returning a But if we have to touch all those places anyway, maybe just doing the special casing there is enough:
(We can introduce a predicate for that, " |
Great questions!
No. However, it is my impression that the variant with the non-extension type is conceptually simple and well justified (that particular kind of extension type really doesn't hide the type of the representation object). With an extension type whose representation type is yet another extension type, it's much less obvious to me that we can find a similarly natural and convincing conceptual perspective. In any case, it would be very easy to generalize this feature such that it allows some extra assignability relationships later on, and the change would be non-breaking.
That is indeed kind of peculiar. However, I wanted to allow the notion of being "fully transparent" as narrow. The language team did not support adding keywords to make this kind of distinction, so we have to use something else, and this notion of being fully transparent does seem rather natural to me. In particular, consider the following example: extension type E(int _) implements num {} If If
Right, I did not want to make it a subtyping property. In particular, we could have a subtyping relationship to another extension type that isn't fully transparent, which would cause some anomalies: extension type E1(int _) {}
extension type E2(int _) implements int, E1 {}
void main() {
E1 e1 = E1(1);
E2 e2 = E2(1);
int i = 1;
e2 = i; // OK, `int <: E2`, not in the proposal, we're just exploring that option.
e1 = e2; // OK, `E2 <: E1`.
e1 = i; // Compile-time error?
} However, it's a very good point that we need to handle composite cases in order to deliver a sufficiently convenient behavior. I did think about that, but forgot to dive into it. We would use a rule which is structural, such that
With the structural rule mentioned above, it would work. I think it should work, too. I adjusted the proposal to include this criterion.
Embellishing the return type by means of this mechanism is likely to be an important use case, but there are many other situations where assignability is used. For example, passing actual arguments to functions, initializing or assigning to variables (any kind), specifying elements in collection literals, and more. |
I think this feature should be optional with implicit constructors. extension type Height(double _) implements double {}
extension type Weight(double _) implements double {}
double calcBmi(Height height, Weight weight) => weight / ( height * height);
void main() {
var height = Height(1.64);
var weight = Weight(54);
var bmi = calcBmi(height, weight);
print(bmi); // 20.077334919690664
bmi = calcBmi(1.64, 54.0); // compile-time error
bmi = calcBmi(weight, height); // compile-time error
} |
You could indeed use implicit constructors to achieve a similar effect. However, implicit constructors as proposed here is a different mechanism: more general in some ways and less general in other ways. It is more general because the operation that produces the desired type of object from a given expression of a different type can inject arbitrary computations, and because it can enable a conversion from any type to any other type. For example: // Hypothetical code: assumes that implicit constructors have been added to Dart.
// Suppose we want to transform `int`s into `String`s, implicitly.
static extension on String {
implicit factory String.fromInt(int i) => i.toString();
}
void main() {
String s = 42; // OK, becomes `String s = String.fromInt(42);`.
} A somewhat similar mechanism has been provided in Scala for several years (see https://docs.scala-lang.org/scala3/reference/contextual/conversions.html). The experience from the Scala community is that it may be difficult to keep track of implicit conversions and hence they should be made explicit in some way (somewhere in the same file, at least). Following that insight, it is required in the Dart proposal that each individual imported implicit constructor which is capable of being used implicitly must explicitly be In particular, an implicit coercion from the representation type However, implicit constructor invocations are less general than the mechanism proposed here in another sense: The mechanism proposed here allows structural lifting of the assignability (which is possible exactly because there's no need to perform any actual computations, we're just giving some objects a new type). // Hypothetical code: Assumes the feature proposed in this issue.
extension type Height(double _) implements double {}
extension type Weight(double _) implements double {}
double calcBmi(Height height, Weight weight) => weight / ( height * height);
void main() {
Map<Height, Weight> map;
map = {1.64: 54.0, 1.84: 77.8, 1.86: 92.2};
for (var MapEntry(key: h, value: w) in map.entries) {
print('BMI: ${calcBmi(h, w)}');
}
} For the assignment to This should illustrate why it is useful to have both proposals on the table, and possibly even adding both of them to the language. |
I meant, bmi = calcBmi(1.64, 54.0); // compile-time error should be errror as it is so far. |
Right, that's a delicate point. I proposed using a keyword to indicate properties like assignability (so we'd have this kind of assignability with an extension type if and only if its declaration starts with the keyword We might reconsider using a keyword, but in the meantime I'm trying to see if we can use the superinterface hierarchy to indicate openness. This happens to work quite well for an extension type If you don't want assignability (in both directions), but you do want to have all the extension type Height(double it) {
export it;
}
void main() {
Height h = 181.0; // Error, `double` not assignable to `Height`.
double d = h; // Error, `Height` not assignable to `double`.
h.floor(); // OK, all `double` members have forwarders in `Height`.
} The point is that we want to reuse members, but we don't want to create the subtype relationship, and |
When this proposal will be accepted, please implement #2506 together. |
With structural assignability, and using the erasure as context type, why not just make it a proper subtype?
Here (Everything after the "if" is the definition I'd use for an extension type being transparent.) That would give us We'd have all the positive effects, with much less effort. What would the negatives be? The extension type becomes an equivalent, but different, type to the supertype/representation type. Should the extension type be a type of interest for the supertype? That is, do we get assignment promotion? Probably not. Nor vice versa. And the extension type still has a higher depth than its declared supertype, so they won't be crowding each other in UP calculations. Is having a cycle on the supertype graph a problem? Is it a problem that a type implements a subtype of itself? Can't say for sure, I'll leave that to the type theorists, but the implements hierarchy does not have cycles, because we disallow that explicitly. We collapse some chains of implementing types into equivalence classes for subtyping, but the only thing we actually change, the extension type, must be implementing specific interfaces that are now also subtypes of it. It won't be different instantiations of those interfaces, so there should be no conflict. |
If we specify that the representation type is assignable to the extension type when the latter is 'fully transparent' as defined above then we will allow an expression of the representation type to be passed where the extension type is required (including higher order cases like If we introduce a full subtype relationship then we will also allow unlimited assignment from an arbitrary different fully transparent extension type with the same representation type. We should at least be aware of that difference, and I suspect that it is too permissive. If you want that then perhaps just write all those methods as plain extension methods, rather than declaring extension types in the first place. |
I agree with this. The implicit assignability is precisely what I wouldn't want for something like the BMI example. The // Weight in kg.
extension type const Weight._(double _) implements double {
const Weigth.kg(double kg) : this._(kg);
const Weigth.g(double g) : this._(g / 1000);
}
// Height in m.
extension type const Height._(double _) implements double {
const Height.cm(double cm) : this._(cm / 100);
const Height.m(double cm) : this._(m);
}
double bmi(Weigth weight, Height height) => weigth / (height * height);
void main() {
var height = Height(1.74);
var weight = Weight(82.0);
print("BMI: ${bmi(height, weight).toStringAsFixed(2)}");
} Notice the mistake? I swapped the So no implicit transparency, because we'd just need an explicit way to opt out of it anyway. Adding Maybe |
This issue is a proposal to add an assignability relationship. Informally, it goes as follows: Assume that
E
is an extension type with representation typeR
which is a non-extension type that is also a superinterface ofE
. In this case the typeR
and subtypes thereof are assignable to the typeE
. For example:The conceptual justification for this assignability rule is that
FancyString
"announces to clients" that it is an extension type that has a non-extension representation type which is related toString
by havingimplements String
(this could be directly or indirectly), and the superinterface and the representation type are exactly the same type, not just subtype-related. In other words, the representation type ofFancyString
is no secret at all.In this case we say that the extension type is a fully transparent extension type.
We need to make sure that the assignability is applicable to composite cases: Assume again that
E
is a fully transparent extension type with representation typeR
. WhenR
is assignable toE
we also wantList<R>
to be assignable toList<E>
, andR Function(E)
should be assignable toE Function(R)
. (Thanks to @lrhn for making this point at a time where I hadn't thought through how we'd deal with structural types!) The crucial point is that there are no run-time actions associated with this kind of assignability, which means that a function of typeR Function(E)
will behave exactly like a function of typeE Function(R)
at run time, and hence we can allow one to occur where the other is expected. Regular subtyping allowsE Function(R)
to occur whereR Function(E)
is expected, and this new kind of assignability allows the converse.We introduce the covariant fully transparent erasure and the contravariant fully transparent erasure of a type, the former also being known as the fully transparent erasure. Both of these functions replace an extension type by the corresponding representation type, recursively and on all subterms. However, the covariant erasure only does this on types that occur in a covariant position and the contravariant erasure only does this on types that occur in a contravariant position, and the covariant erasure invokes the contravariant erasure on subterms in a contravariant position and vice versa.
For example,
FancyString
erases (covariantly) toString
,FancyString Function(FancyString)
erases toString Function(FancyString)
,FancyString
erases contravariantly toFancyString
, andFancyString Function(FancyString)
erases contravariantly toFancyString Function(String)
.With that terminology in place, we can restate the proposal:
A type
T
is assignable to a typeS
if the contravariant fully transparent erasure ofT
is a subtype of the covariant fully transparent erasure ofS
.The rule could have used "is assignable to" rather than "is a subtype of", but this is a possible future enhancement, and we prefer to avoid the complexity of having multiple transformations on the same expression.
We have in several other ways used the fact that an extension type
E
implements a certain non-extension typeI
as a signal thatE
is associated withI
(for instance "E
is-anI
" in the sense thatE
is a subtype ofI
, andE
has all the non-redeclared members of the interface ofI
). This proposal takes that perspective one step further and introduces assignability from the representation type to the extension type (which is the opposite direction of what we already have, based on the subtype relationship).Consider the well-known example that demonstrates the potential for run-time type errors based on dynamically checked covariance:
If we wish to adapt the treatment of the type
List
such that it is considered to be invariant then we can introduce an extension type with a phantom type argument:However, the initialization
List<int> xs = [1];
will only succeed if this proposal is adopted. Otherwise it's a compile-time error because the typeList<int>
from 'invariant_list.dart' isn't a supertype of the typeList<int>
from 'dart:core'. In that case we'd have to useList<int> xs = List([1]);
, which implies that theList
type from 'invariant_list.dart' is no more a drop-in replacement for theList
type from 'dart:core'.During inference, a fully transparent extension type
E
with representation typeR
would give rise toR
as the context type. Note that an expression with context typeR
could have typeE
or a subtype thereof (which is of course assignable toE
, and also a subtype of the context type), or it could have typeR
or a subtype thereof (which is now also assignable toE
).Note that this proposal basically subsumes the proposal in #3607: We would just include an extra rule to say that a fully transparent extension type
E
with representation typeR
can be the return type of anasync
/async*
/sync*
function if and only ifR
can be said return type; in this case, the future value type / element type of the function is computed fromR
.@dart-lang/language-team, WDYT?
[Edit: Feb 14: Generalized assignability to be structural.]
The text was updated successfully, but these errors were encountered: