dart-lang / dart-lang/language
Should tearing off a call method be null-aware?
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
Description
[Edit: Refer to the operation as "tearing off a `call` method".]
Thanks to @johnniwinther for bringing up this ambiguity. Consider the following program:
```dart
class A { void call() {} }
void main() {
var b = true;
A? a = b ? A() : null; // Ensure we don't promote `a` to `A` by initialization.
Function? f = a; // Error.
}
```
The error message from the analyzer is 'A value of type `A?` can't be assigned to a variable of type `Function?`', and it shows that tearing off a `call` method is simply not considered when the context type is nullable.
Compare this to the following legacy program:
```dart
// @dart=2.9
class A { void call() {} }
void main() {
var b = true;
A a = b ? A() : null;
Function f = a; // Accepted.
}
```
The existing behavior in legacy programs works as if `a` is subject to a transformation to `a?.call` rather than `a.call`, and this smoothly allows the null to exist and propagate.
The rule which is applied with null safety enabled is more strict, it essentially transforms `a` into `a.call` and then proceeds to report an error if `a` has a potentially nullable type.
So we may wish to specify that the transformation associated with tearing off `call` methods goes from `a` to `a?.call` when the type of `a` is potentially nullable, and to `a.call` otherwise.
This may seem benign and convenient, but we should also consider the relationship to union types:
If we make this choice then we are essentially saying that if a receiver has a type which is a union type and the context is a union type (maybe: the same union type, or somehow similar), then a mechanism like generic function instantiation can make a choice among the union type operands and then proceed:
```dart
class A1 { void call() {} }
class A2 {}
void main() {
var b = true;
A1 a1 = A1();
A2 a2 = A2();
A1|A2 a12 = b ? a1 : a2;
Function|A2 f = a12; // Apply generic function instantiation to one operand of `|'?
}
```
This example highlights how much magic we are applying to the `A|Null`/`Function|Null` unions that are spelled `A?` and `Function?` in the first code snippet. On the other hand, we do have special casing for `Null` in many ways already, e.g., null-aware member access using `?.`.
Contributor guide
Research direction
Start with the nullable and legacy Dart examples in the issue, then read the 21-comment discussion to understand the unresolved behavior and its relationship to union types. Done means reaching and documenting a decision for how tearing off a call method behaves with potentially nullable receivers, including the consequences for the shown examples.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100