-
Notifications
You must be signed in to change notification settings - Fork 213
Type promotion for this
#1397
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
Please, I struggle with this every time I have to implement a sum type. |
@mateusfccp, is it correct to assume that you need |
Sorry, @eernstg, I couldn't exactly understand what you meant. Usually when I make a sumtype I have to make a T fold<T>(...) {
final _this = this;
if (_this is A) {
// ... do something with promoted _this
} else if (_this is B) {
// ... do something with promoted _this
}
} Alternatively, I use |
@mateusfccp, the simplest approach to promotion of |
I absolutely love this: class A {
void m() {
if (this is B) {
B b = this; // OK because `this` has type `B`.
}
}
}
class B {} But I think this is very much a bridge too far: class A {
void m() {
if (this is B) {
b = 1; // "b" is now in scope because of the promoted implicit `this`.
}
}
}
class B extends A {
int b;
} I don't think type promotion should interact with the scoping of bare identifiers like this. Say you have a program like: var name = "top level";
class A {
m() {
print(name);
}
}
class B extends A {
var name = "in B";
}
main() {
A().m();
} This prints "top level". If we used the promoted m() {
if (this is B) print(name);
} Now In other words, I think it makes sense to promote the identifier |
@munificent wrote:
to let a plain identifier
We actually wouldn't ever re-interpret an identifier like So we maintain the sanity property that we have today: But it would of course still be possible to use an explicit m() {
if (this is B) print(this.name); // Prints "in B".
} |
(Actually, doing One of the places where I most often want to promote |
FYI, I'm considering adding support for |
@stereotype441, what's your take on the indirect consequences of promoting One consequence is that However, we also have rules like this one that give rise to indirect consequences:
In this rule The important part is that this rule introduces class A {
m() {
if (this is B) print(name);
}
}
class B extends A {
var name = "in B";
} The reason why this is allowed is not that we're introduced some funky scope rules, it is simply that (1) Another interesting situation arises because a promotion of I also mentioned the consequences for type variables: In a |
I'd be fine with an unqualified name being looked up in the surrounding class, not the static type of It means that if the surrounding class has no member, but the promoted For extension declarations, I'd do the same. An unqualified identifier which resolves to a surrounding instance member declaration, is involved on the same extension as the current invocation. That means the same extension, same type arguments and the same receiver. It does not use
Here we promote E<T>(this).max(...) and not to Again I think that's fine, and we should just keep it |
That is not quite true, as I mentioned here. But it is true that it did not matter before.
Arguably, we aren't resolving plain identifiers (or operators) that end up denoting instance members that aren't declared in a lexically enclosing scope. We are detecting that those identifiers/operators are not in scope, and then we transform the construct to make it an explicit invocation on If we wish to stop specifying that Surely it's doable, but it's not a no-op. |
@eernstg asked:
This one seems fine to me.
IMHO this would be reasonable.
I'm ok with promotion of
I would love for @lrhn said:
That doesn't match my memory of what's currently specified. My understanding is that an unqualified name is first looked up in the lexical scope. Then:
For example: var x3 = 'top level x3';
class B {
get x3 => 'B.x3';
get x4 => 'B.x4';
}
class C extends B {
get x1 => 'C.x1';
get x2 => 'C.x2';
f() {
var x1 = 'local variable x1';
print(x1); // prints 'local variable x1', due to lexical lookup
print(x2); // lexical lookup finds prints 'C.x2'; this is virtually dispatched, so it prints 'D.x2'
print(x3); // prints 'top level x3', due to lexical lookup
print(x4); // lexical lookup fails, hence equivalent to `print(this.x4);`, hence prints 'B.x4'
}
}
class D extends C {
get x2 => 'D.x2';
}
main() {
D().f();
} So when you say "I'd be fine with an unqualified name being looked up in the surrounding class, not the static type of
Agreed that this Speaking for myself, I want to make implicit and explicit
|
That would be "no changes", with |
Ack, I was misremembering what we do for an identifier that it's not in the lexical scope. I thought we first checked whether the surrounding class has a member with that name (not class declaration, so including inherited members), before deciding whether to turn it into In which case keeping that behavior is probably safest. And an identifier resolving to an extension member of the current extension has a potentially different meaning than prefixing with Maybe we should just say that if an extension is applicable to an invocation inside itself, it's always most specific (based on location). You can always use an explicit invocation if you mean something else. (Maybe extensions defined in the same library should always be more specific.) |
@mkustermann mentioned the situation where Moreover, as @lrhn mentioned here, If Also note that we already have a similar treatment of the representation variable in an extension type when its name is private (which is relevant because the representation variable in an extension type E(num _n) {
void foo() {
if (_n is int) {
_n.isEven; // OK, `_n` has type `int` here.
}
}
} |
To summarize, also for my own benefit: Classes and extension types behave mostly the same:
The issues are pretty much the same. Extensions have one case where they're unaffected by The biggest issue, IMO, is that it just doesn't work for generics. That's a pretty big caveat too. We can't promote a generic class C<T> {
T? get value => null;
void foo() {
var self = this;
if (self is C<num>) {
num? v = self.value; // Not allowed. Not promoted to `C<num>` or `C<T&Num>`.
}
C<Object?> self2 = this;
if (self is C<num>) {
num? v = self2.value; // OK, did promote.
}
}
} Most of the other issues are not new. They happen just the same if promoting variables. |
That's a great summary! @lrhn wrote:
Certainly, that's the main reason why I created this issue in the first place. It's interesting. ;-) It's worth noting that we can already write code where a type variable is promoted, but it takes a couple of steps: [Edit: Now returning a value whose type depends on // ignore_for_file: non_constant_identifier_names, unused_local_variable
extension _AIntExtension<X extends int> on A<X> {
List<X> fooAInt(String s) {
print('Receiver is $s: Stuff which is only applicable when `X <: int`.');
A<int> v1 = this; // OK.
A<X> v2 = this; // OK.
int v3 = x; // OK.
return [x];
}
}
class A<X> {
X x;
A(this.x);
List<X> foo(String s) {
late List<X> result;
print('Receiver is $s: Stuff that works for all values of `X`.');
if (this is A<int>) {
// We need a dynamic invocation to make the type variable bounds
// check occur at run-time. NB: It is guaranteed to succeed, but the
// type checker doesn't understand that.
void typeVariableCaster(List<Y> Function<Y extends int>(A<Y> self) g) {
result = (g as dynamic)<X>(this);
}
typeVariableCaster(
<Z extends int>(A<Z> self) => _AIntExtension<Z>(self).fooAInt(s));
} else {
result = [];
}
return result;
}
}
String typeName(Object? o) => o.runtimeType.toString();
void main() {
A<int> a1 = A(42);
A<String> a2 = A('Hello!');
print(a1.foo(typeName(a1)));
print(a2.foo(typeName(a2)));
} |
That's a clever way to get the current binding of a type variable bound to a type variable with a more restrictive upper-bound, but it's not what I'd call type variable promotion. It's introducing a second type variable, which is not assignable to the first. Actual type variable promotion, as I use the term, would mean something like: class C<X extends num> {
void foo(X value) {
if (this is C<int>) { // Assume this promotes `X` from `extends num` to `extends int`.
int v2 = value; // `X` is now a subtype of `int`!
}
} That's what |
So true, I was just confirming that we can achieve the desired typing situation where In particular, we have to say I've adjusted the example such that I'm sure we can find other reasons why this emulation isn't entirely as powerful as a real type parameter promotion, but that's just one more argument why we might consider having the real thing. Conversely, you could also say that the emulation technique is worth having in mind, if it is needed once in a blue Moon. ;-) |
The reserved word
this
, when defined, denotes the current instance of the enclosing class, and hence it is known that ifthis is T
yields true then it is sound to promote the static type ofthis
as ifthis
had been a local variable, and similarly for all other situations where the local variable would be promoted.The promotion of
this
implies thatthis
gets a different static type (when it is written explicitly, and when it is an implicit part of an instance member access).However, it is also possible to learn more about type arguments:
For simplicity, we could make the choice to support promotion of
this
with no changes to the treatment of the type parameters of the enclosing class, or with support as an option that we may pursue in the future.Presumably, it is a nearly non-breaking change to add support for promotion of type parameters, because this only adds more information about said type parameters, it doesn't invalidate existing information. (The change is not completely non-breaking because any change of a static type may give rise to inference of type arguments with different values, and they are reified so the change is observable.)
The text was updated successfully, but these errors were encountered: