dart-lang / dart-lang/language

Should tearing off a call method be null-aware?

Open
#1,333 21 comments 0 reactions 0 assignees View on GitHub
nnbd question
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.