Destructuring nullable type in refutable pattern should promote type to non-null
Yesterday there was a discussion on the Dart community discord on doing something like this:
class Platform {
Object? core;
}
late final Platform? _corePlatform;
void main() {
// Have both the _platform_ and the _core_ bound to a name
// & checked for being non-null
if (_corePlatform case Platform(:final Object core)) {
print("${_corePlatform.core} $core");
}
}
The example program doesn't type check, because _corePlatform is not promoted from Platform? to Platform, even though Platform(:final Object core)) implies this to be true.
Doing this solves the problem:
if (_corePlatform case final Platform platform && Platform(:final Object core)) {
print("${platform.core} $core");
}
but it would be nice if it wasn't necessary.
The shortest way to do it today is:
class Platform {
Object? core;
Object? other;
}
late final Platform? _corePlatform;
void main() {
if (_corePlatform case Platform(:final Object core) && final _corePlatform) {
print("${_corePlatform.other} $core");
}
}
Or we can simply not use patterns if we find it too much convoluted:
class Platform {
Object? core;
Object? other;
}
late final Platform? _corePlatform;
void main() {
final corePlatform = _corePlatform;
final core = corePlatform?.core;
if (corePlatform != null && core != null) {
print("${corePlatform.other} $core");
}
}
But I agree, I don't know why the analyzer can't naturally introduce a local _corePlatform with the promoted type, as long as it is local (it is not in the previous case).
Something like (in this case it would be final is the original _corePlatform was final and non-final otherwise):
class Platform {
Object? core;
Object? other;
}
void main() {
final Platform? _corePlatform;
// ... other logic
if (_corePlatform case Platform(:final Object core)) {
print("${_corePlatform.other} $core");
}
}
TL;DR: You can't promote a top-level variable at all.
A case Something(...) does promote if the matched expression can be promoted.
the analyzer can't naturally introduce a local
_corePlatform
That's not how promotion works. Either the compiler can prove, beyond a shred of doubt, that the value you just read and checked will be the same value you read and use later, so it can use the promotion of the first check at the later use, or it can't (or doesn't know how to) prove that. Introducing a local variable would only be sound if it can be proven that it won't change the value, and then you could promote the variable directly.
The standard argument for "why not promote this final variable" is that APIs are not defined in terms of being a final variable, but about being a getter. If it's not const, then there is no promise that the getter will keep being a final variable. If you can promote a final variable, but not a general getter, then it'd be a breaking change to change a final variable to a getter. That would break getter/variable symmetry and we don't want that.
In this particular case, the variable is private. Dart allows promoting local instance variables inside the same library, with some clever logic to ensure that the variable is definitely a final variable whose value won't change between being checked and being used, and not, fx, overridden by a getter or falling back on noSuchMethod in some cases. It's only allowed inside the same library, because if you change a variable to a getter, you immediately get all the resulting failed promotion errors. It won't break someone else's code later.
That promotion does not apply to local static/top-level variables. There is no good reason for that, other than it not having been done. They should be easier than instance variables since they can't be overridden. (Not sure it'd work with late final too, but it probably can. After all, you can only ever successfully read one value from it, it's just that it may throw instead.)
Moving the variable to be an instance variable gives:
class Platform {
Object? core;
}
class _PlatformHolder {
late final Platform _corePlatform;
}
final _PlatformHolder _platform = _PlatformHolder();
void main() {
if (_platform._corePlatform case Platform(:final Object core)) {
print("${_platform._corePlatform.core} $core");
}
}
That compiles. The final _PlatformHolder _platform is trusted to be stable, the _platform._corePlatform can be promoted because it's an instance member.
(Which is evidence that not promoting the static/top-level final variables directly is not for safety, it's just not done. Yet! @stereotype441)
Thank you for your lengthy response 😄
Can you maybe explain a bit more why it makes a difference if it's for example overridable or a getter?
Is it possible to construct a counter example for how promoting _corePlatform could be incorrect here?
if (_corePlatform case Platform(:final Object core)) {
print("${_corePlatform.core} $core");
}
class Platform {
Platform([this.core]);
Object? core;
}
abstract class _APlatformHolder {
late final Platform? _corePlatform;
}
class _PlatformHolder extends _APlatformHolder {
_PlatformHolder([this.__p]);
Platform? __p;
@override
Platform? get _corePlatform {
print(__p);
final r = __p;
__p = null;
return r;
}
}
final _APlatformHolder _platform = _PlatformHolder(Platform("core"));
void main() {
if (_platform._corePlatform case Platform(:final Object core)) {
print("inside");
print("${_platform._corePlatform?.core} $core");
}
}
Okay, I see...
If it's not const, then there is no promise that the getter will keep being a final variable.
So this would be permittable, it's just not implemented?:
class Platform {
const Platform([this.core]);
final Object? core;
}
const Platform? _corePlatform = Platform();
void main() {
// Have both the _platform_ and the _core_ bound to a name
// & checked for being non-null
if (_corePlatform case Platform(:final Object core)) {
print("${_corePlatform.core} $core");
}
}