language
language copied to clipboard
Should tearing off a call method be null-aware?
[Edit: Refer to the operation as "tearing off a call method".]
Thanks to @johnniwinther for bringing up this ambiguity. Consider the following program:
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=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:
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 ?..
The general question is: Whenever we do something special for static type T and a value, should we do the same thing for T? and a non-null value? (So, work the same as before below the union?)
It would be nice if we do. It might not be possible in all cases.
- If we assign an expression of a callable object type to
Function, then we do a tear-off, implicitly adding thecalltoFunction x = e.call;. - If we assign an expression of a (non-nullable) callable object type to
Function?, then we should probably do the tear-off anyway. It's clearly what is intended, it's valid, and not doing the tear-off is invalid, and it really is justFunction? x = e.call;. - If we assign an expression of a nullable callable object type to
Function?, then ... it would be nice if we did a null-aware tear-off (Function? x = e?.call;).
Instantiated tear-off is similar, and should probably work the same, whatever we end up doing. There is no syntax for it, though. (I guess we can use foo?.call<T1, T2> as the desugaring.)
We fixed double constants to work so double? x = 0; treats the 0 literal to a double literal.
(I don't remember any other implicit context-type based conversions).
I'd be fine with having these implicit conversions generalize to general union types. Assume that the context type of an expression is a union type.
- If an expression is directly assignable to the union type, then no conversion is needed.
- If not, and there is an available conversion (like adding
.callto a non-nullable expression or?.callto a nullable expression) which allows the expression to be assignable to the context type, then use that. - If there is more than one compatible conversion, then it's a compile-time error due to ambiguity.
I think this is already specified here. Feel free to re-open if I've missed something.
It is specified there for non-nullable expressions and nullable/non-nullable context types, and only for callable object tear-off and double literals.
That does not cover nullable expressions and context types where the tear-off can still be done using ?.call.
So, do we want that?
It wouldn't hurt for nullability alone, but it doesn't generalize to FutureOr<CallableObjectClass>.
Also, should we allow FutureOr<Function> to tear off callable objects and FutureOr<double> to convert literals to double, as long as the callable object isn't directly assignable to FutureOr<Function>, to complete the union type behavior? (Like the list I made above: If directly assignable, do nothing. If a conversion applies and would make assignment valid, use that. If none or multiple conversions apply, still compile-time error. That would treat our implicit conversions as local, type-based rewrites, which is what they are, but not try to lock down the types which allow the rewrites ahead of time (which means we have to revise them when we change the type system).
It is specified there for non-nullable expressions and nullable/non-nullable context types, and only for callable object tear-off and double literals.
That does not cover nullable expressions and context types where the tear-off can still be done using
?.call.
I don't understand. From the text:
The implicit conversion of integer literals to double literals is performed when the context type is double or double?.
The implicit tear-off conversion which converts uses of instances of classes with call methods to the tear-off of their .call method is performed when the context type is a function type, or the nullable version of a function type.
This covers the case where the context type is nullable or non-nullable, and and the expression is non-nullable, and:
Implicit tear-off conversion is not performed on objects of nullable type, regardless of the context type. For example:
This covers the nullable expression case. I guess technically we should cover the potentially nullable case? Is that what you're worried about?
where the tear-off can still be done using
?.call.
Isn't this the case that is covered by the text I quoted above? What am I missing?
Also, should we allow
FutureOr<Function>to tear off callable objects andFutureOr<double>to convert literals todouble, as long as the callable object isn't directly assignable toFutureOr<Function>, to complete the union type behavior?
My immediate reaction is no, but I'm open to being convinced otherwise.
ACK. The case is covered by saying that the call method is not torn off.
The question is then whether that is the behavior we want, or do we want to tear off the call method of a nullable callable object type with a nullable function type context using ?.call?
That's a possible enhancement on the currently specified behavior.
And then we can further enhance the behavior by allowing other union type contexts to also prompt tear-off.
That's not as trivial because FutureOr<Function>? f = futureOrCallableObjectOrNull; doesn't have a simple rewrite. We can tear off from a callableObject or a callableObjectOrNull expression, but an expression with type FutureOr<CallalbeObject>? has no easy rewrite, which might be reason enough to not do anything for it.
I think FutureOr<Function>? x = callableObject; and FutureOr<Function>? x = callableObjectOrNull; might still be worth it, for the consistency across union types, but it's also unlikely to be valuable in practice.
(And if one is against implicit conversions, which I also am sometimes, adding more of them would be a non-goal, it's about reining in what we have instead).
For reference, this is the issue where discussed this previously.
Considering the issue #401 where this was previously discussed and the nnbd spec rules, I think there are two new elements here:
-
I, at least, wasn't aware of the fact that legacy programs use
?.callconversion (such that null is mapped to null, rather than attempting to tear.calloff of null and throwing). -
Implementations (just tried
dart,dart2js,dartanalyzer) perform the.callconversion also when the context type isFutureOr<T>whereTisFunctionor a function type. I believe this is not specified.
// @dart = 2.9
import 'dart:async';
class A { void call() {} }
void main() {
var b = false;
A a = b ? A() : null;
Function f = a; // No dynamic error, so we're using `a?.call`, not `a.call`.
print(f); // 'null'.
FutureOr<Function> f2 = a; // Allowed.
}
I'm still rather sceptical about the amount of magic that we'll have if we generalize the mechanism to allow arbitrary union types (at least the two we can express ;-) as the context type, but it is on the other hand a breaking change to stop supporting the FutureOr case.
For double conversion, I think this is already implicitly specified:
If \code{double} is assignable to $T$ and \code{int} is not assignable to $T$,
then the static type of $l$ is \code{double};
That is, this covers the FutureOr<double> and FutureOr<double?> and FutureOr<double>? cases, no?
I can't find any specification of the call method implicit tear off at all, so this should probably be added to the nnbd spec. Perhaps a similar formulation would correctly capture the current behavior?
More relevant background here.
Hi,
while migrating my flutter_command package I fell vicitim of this oddity.
So far Commands, which are callable classes could be assigned to VoidCallbacks like event handlers in widgets without problems.
Not this doesn't work anymore because all the flutter event handlers are nullable, which distroys a lot of the elegance and the apeal of flutter_commands. I actually reduces the power of callable classes significantly.
/// Calls the wrapped handler function with an optional input parameter
void execute([TParam? param]);
/// This makes Command a callable class, so instead of `myCommand.execute()`
/// you can write `myCommand()`
void call([TParam? param]) => execute(param);
With the optional parameter it was possible to assign this function to VoidCallbacks as well as to ValueCallbacks.
PLEASE! Make this work again.
@escamoteur
Make what, precisely, work again?
I can't find a definition of ValueCallbacks. The one for VoidCallback is:
typedef VoidCallback = void Function();
A class with a call method declared as void call([whatever]) => ... should be assignable to VoidCallback and VoidCallback?.
If you have a value which has type TheClass?, then you don't get implicit tear-off because there is nothing to tear off null.
You must do something to make the type non-nullable.
TheCallabackClass? c = ...;
VoidFunction f_bad = c; // Doesn't work.
VoidFunction f1 = c!; // Throws if null.
VoidFunction? f2 = c?.call; // Explicit tear-off.
VoidFunction? f3 = c is null ? null : c.call; // I really wish we could just use `c` in the last expression
A class with a call method declared as void call([whatever]) => ... should be assignable to VoidCallback and VoidCallback?.
That's exactly what isn't possible anymore. you can't assing it to a VoidCallback?
@escamoteur I can.
@lrhn Ah, I think I know now what happened.
before, this here would work:
final handler = isActive ? myCallableClass : null;
onTapped: handler // where onTapped is of type VoidCallback?
which is a pretty standard pattern do enable/disable controls in Flutter. this now no longer works. this will hit a lot of Flutter users that use callable classes in such a context
Unfortunately the Error that the linter shows in such a case isn't really helpful either.
Here a repro: https://dartpad.dev/db16677e8d7b985481869cc5d9f0a4bd?null_safety=true
the problem that @escamoteur is having as I understand is the following line:
//A? to VoidCallback? does not work
VoidCallback? v3 = aNull;
if A is a callable class, then assigning A to VoidCallback works, but assigning A? to VoidCallback? does not.
@lrhn @escamoteur means exactly this which you said before:
I really wish we could just use
cin the last expression
Looking at the error that is thrown at compile time: Error: Can't tear off method 'call' from a potentially null value. VoidCallback? v5 = aNull; seems to me that it is converted by the compiler to VoidCallback? v5 = aNull.call instead of VoidCallback? v5 = aNull?.call
So I believe the answer to the title of this issue Should tearing off a call method be null-aware is yes it should add the ? where needed so we can indeed assign a nullable instance of a callable class to a nullable function parameter
@Kavantix Thanks for putting it in such precise words
@leafpetersen has this topic ever been revisited since last year?
@escamoteur No, we've not revisited this. It seems like something that we might want to look into at some point (evaluate how breaking changing it would be, and if reasonable, and we think it's a good change, schedule it), but it's not currently at the top of the priority queue.
might I bring this up again? I know there are currently some other discussions around callable classes like here https://github.com/dart-lang/language/issues/2399 Or was that already handled by https://github.com/dart-lang/sdk/commit/7ab89309fb413b5e17c635b11ae375e61a13a411 ?
I still think callable classes are a great feature of Dart.