Enable a transistor-like behavior of a boolean value
Introduction
One particular kind of expression comes up frequently, and there have been some discussions about how it could be expressed more concisely (I can't find a dedicated issue, though). The expression is condition ? expression : null.
The behavior of this expression is "transistor-like" in the sense that it uses the boolean value of condition to turn on or off the evaluation of the expression, yielding the value of the expression if it was evaluated, and null otherwise.
This issue is a proposal that we should make : and the last expression optional in the grammar rule about conditional expressions, and specify that : null is implied when that part is omitted.
Existing approaches
It is crucial that the expression is not evaluated in the case where the boolean value is false: That value will not be used anyway (so we don't want to waste resources on computing it), and it may even give rise to run-time failures if it is evaluated (e.g., r.hasFoo ? r.foo : null where r.foo might throw when r.hasFoo is false).
We could consider reversing the order of the operands (a bit like the reverse conditional statements in some languages a la doThis() if somethingIsTrue();), and use an extension method:
extension<X> on X {
X? operator ^(bool b) => b ? this : null;
}
void main() {
final x = 'Hello!';
print(x ^ true); // Prints 'Hello!'.
print(x ^ false); // Prints null.
}
The operator ^ is basically only defined for numbers and booleans themselves, so it might be OK to use it for this purpose on all other types. However, this is not good enough because it will evaluate the left operand, no matter what.
We could also use an operator like ~ on bool to turn it into null when we want the value (that is, true should be mapped to null), and then we'd use ~b ?? e. However, if we do this then there is no good value to map false to: It should be null, because that's the value that the whole expression ~b ?? e should have when the boolean is false, but if we do that then we'll just evaluate e and yield that value again.
extension on bool {
bool? operator ~() => this ? null : false /* what else?! */;
}
void main() {
print(~true ?? 'Hello!'); // Prints 'Hello!', as it should.
print(~false ?? 'Hello!'); // Prints 'false'. Should have been 'null'.
}
Proposal
We change the grammar such that the two last elements in the conditional expression are optional:
<conditionalExpression> ::=
<ifNullExpression>
('?' <expressionWithoutCascade> (':' <expressionWithoutCascade>)?)?
This should not create difficulties during parsing because <conditionalExpression> only occurs in the right-hand side of grammar rules of top level constructs: <expression>, <expressionWithoutCascade>, <initializerExpression>, and <cascade> (where it's followed by ?.. or ..). In other words, if we don't see a colon then the conditional expression definitely omits the last part, and if we do se a colon then we have already decided that we will commit to parsing a conditional expression in cases where an expression can be followed by a colon (that is, in various map literal related constructs).
A conditional expression b ? e where ':' <expressionWithoutCascade> has been omitted is treated as b ? e : null.
For example:
void main() {
print(true ? 'Hello!'); // Prints 'Hello!'.
print(false ? 'Hello!'); // Prints null.
// A typical example, assuming that the undefined names have suitable declarations.
// "Check if we have a video, then use its length, otherwise bail out with a null."
var length = myProtoAsset.hasVideo() ? protoAsset.video.length;
}
I fear parsing this will make ? even more overloaded than today. At least a e1 ? e2 : e3 can only mean two things, and only in a map literal. A e1 ? (e2) or e1 ? [e2] will become ambiguous in every context.
I'd suggest "if expressions" instead (#3374, probably others), so you can use if (expr) expr (or null) or if (expr) expr else expr, and never use ?/: again!!!!! Or something.
(Edit: Also https://github.com/dart-lang/language/issues/2306, finally found it. Title does not have the best keywords.)
(I've made extension functions like:
extension on bool {
T? call<T>(T value) => this ? value : null;
String operator[](Object? value) => this ? "$value": ""; // For use in interpolations: `"foo ${ifBar[bar]}".
}
But I really want interpolation elements instead.)
https://dart-review.googlesource.com/c/sdk/+/451560 implements this proposal, and it only fails on tests that are specifically expecting a syntax error at <ifNullExpression> ? <expressionWithoutCascade> without ':' <expressionWithoutCascade>. This is exactly the new thing that we're allowing, as expected.
The ambiguous cases e1 ? (e2) and e1 ? [e2] are actually only the latter, because the former used to be a syntax error (we'd use e1?.call(e2)). But the second one is still parsed as a null-aware invocation of operator []:
extension on bool {
int call(int other) => 100;
int operator [](int other) => 100;
}
void main() {
print(true ? (2)); // '2': was an error, is now a conditional.
print(true ? [2]); // '100': was a null-aware operator `[]`, still is.
print(false ? (2)); // 'null', again a short conditional.
print(false ? [2]); // '100': still operator `[]` defined above.
}
So it's a non-breaking change.
Could someone write true ? [2] expecting it to resolve to [2], or false ? [2] expecting null? Would the syntax be parsed differently depending on the presence or absence of an extension defining bool.operator[]?
Would the syntax be parsed differently depending on the presence or absence of an extension defining
bool.operator[]?
Likely not. Parsing is one of the first things that happen in the compiler, and type inference is over if the last, which is also where instance and extension members are resolved.
Making parsing depend on types, when type inference is something you do on parsed expressions, would give the compiler nowhere to start.
Agreeing on @lrhn's comment about parsing being performed without access to type information, I don't think there's much to worry about for these examples from @Jetz72:
Could someone write
true ? [2]expecting it to resolve to[2], orfalse ? [2]expecting null?
If a developer writes something like b ? [2] and intends this to be a short conditional then they are very likely to get some error messages: First, b would have the type bool, and it's a warning to use the null-aware index operator on a non-nullable receiver. Moreover, bool is quite unlikely to have an operator [], so there's another error. Such an operator could potentially be declared by an extension, but it is not obvious which purpose an operator [] on the type bool would have, and I doubt it would ever occur. Finally, if bool does indeed have an operator [] in a given context then the type of the expression would be the return type of that operator, and that's again rather unlikely to be assignable to the same kind of target that List<int>? would be assignable to. A developer would need to be very unlucky to get the unexpected semantics silently.
If the type of b is dynamic then all these type based safeguards are gone, but that's true in general when the type dynamic occurs in an expression. But you could argue that the ambiguity is slightly more dangerous for the dynamic case than it is for non-dynamic code.
By the way, it's worth noting that the singleton list literal is the only construct that will parse as an invocation of operator [], if you have zero or more than one element then it will again parse as the short conditional expression.
[Edit: e ? [... is actually committed as being a null-aware invocation of operator [], so the forms like b ? [] and b ? [e1, e2] etc. are reported as errors, rather than being parsed as short conditional expressions. This might be OK, to avoid the ambiguity; otherwise, https://dart-review.googlesource.com/c/sdk/+/451560 would need a bit more work.]
With that in mind, I do think the ability to use e1 ? e2 as a short conditional expression meaning e1 ? e2 : null is reasonable.
it is not obvious which purpose an operator [] on the type bool would have, and I doubt it would ever occur.
That particular usage would not be compatible with a null-aware operator, though, it's there to avoid a "null" being put into the interpolation.
Still, it's not just about getting the unintended syntax silently, it's about getting the undersired syntax when you wanted the current syntax. Which is possible, even if it's not that likely.
I definitely would not want b?[] and b?[e1, e2] to be "list if b is true, null otherwise", but b?[e1] to be a null-aware index operator.
(And I think the syntaxes are too damn close.)
I definitely would not want
b?[]andb?[e1, e2]to be "list ifbis true,nullotherwise", butb?[e1]to be a null-aware index operator.
Agreed, and it would also prevent a future generalization whereby operator [] would support an arbitrary formal parameter list. This means that the current behavior of https://dart-review.googlesource.com/c/sdk/+/451560 is also the behavior we'd want.
the syntaxes are too damn close
It is definitely an argument against this feature that there is such an ambiguity. However, we do have other ambiguities with associated disambiguation rules, so that doesn't generally stop us. Also, I think it's going to be useful: I searched some internal code and found 48,496 occurrences of conditional expressions. 10,678 of those end in : null and 5,228 have ? null :. In other words, it looks like about a third of all conditional expressions could be abbreviated by using this feature. Sounds worthwhile to me.
I analyzed a big corpus of pub code to see how many existing ?: expressions have null in one of the branches. I tried to exclude generated files from the analysis because those tend to use conditional expressions heavily in idiosyncratic ways. I also counted what kind of surrounding expression they occurred in.
Here's the results:
-- Conditional (151613 total) --
98047 ( 64.669%): No null branch ============================
35578 ( 23.466%): Null else branch ==========
17988 ( 11.864%): Null then branch =====
-- Parent (53566 total) --
32544 ( 60.755%): ArgumentList ===================
7280 ( 13.591%): ExpressionFunctionBody =====
4828 ( 9.013%): VariableDeclaration ===
3421 ( 6.387%): AssignmentExpression ==
3101 ( 5.789%): ReturnStatement ==
927 ( 1.731%): ParenthesizedExpression =
431 ( 0.805%): ConstructorFieldInitializer =
419 ( 0.782%): ConditionalExpression =
368 ( 0.687%): MapLiteralEntry =
121 ( 0.226%): ExpressionStatement =
61 ( 0.114%): ListLiteral =
37 ( 0.069%): RecordLiteral =
11 ( 0.021%): SwitchExpressionCase =
11 ( 0.021%): InterpolationExpression =
4 ( 0.007%): CascadeExpression =
2 ( 0.004%): SetOrMapLiteral =
Took 59.121s to scrape 28295841 lines in 165151 files. (32 files could not be parsed.)
It looks to me like null is pretty common in branches: around a third of ?; expressions. It seems that null appears fairly often in both then and else branches, though the else branch is ~2/3 of them.
Of conditional expressions with a null in one branch, it looks most of them are in argument lists. That's pretty much what I expected. A lot of Flutter widget code looks like:
TextStyle(
color:
item.error != null ? context.theme.colorScheme.error : null,
)
BoxDecoration(
color: hovered ? effectiveHoveredBackgroundColor : null,
borderRadius: effectiveRadius,
)
Container(
padding: widget.buttonPadding.resolve(Directionality.of(context)),
height: widget.isDense ? _denseButtonHeight : null,
child: child,
)
Or even:
DropdownButtonFormField<String>(
style: textStyle,
autofocus: autofocus,
focusNode: _focusNode,
value: _value,
dropdownColor: bgcolor,
enableFeedback: widget.control.attrBool("enableFeedback"),
elevation: widget.control.attrInt("elevation", 8)!,
padding: parseEdgeInsets(widget.control, "padding"),
itemHeight: widget.control.attrDouble("itemHeight"),
menuMaxHeight: widget.control.attrDouble("maxMenuHeight"),
iconEnabledColor: selectIconEnabledColor,
iconDisabledColor: selectIconDisabledColor,
iconSize: widget.control.attrDouble("selectIconSize", 24.0)!,
borderRadius: borderRadius,
alignment: alignment ?? AlignmentDirectional.centerStart,
isExpanded: widget.control.attrBool("optionsFillHorizontally", true)!,
icon: selectIconCtrl.isNotEmpty
? createControl(widget.control, selectIconCtrl.first.id, disabled)
: selectIconStr != null
? Icon(selectIconStr)
: null,
hint: hintCtrl.isNotEmpty
? createControl(widget.control, hintCtrl.first.id, disabled)
: null,
disabledHint: disabledHintCtrl.isNotEmpty
? createControl(widget.control, disabledHintCtrl.first.id, disabled)
: null,
decoration: buildInputDecoration(context, widget.control,
prefix:
prefixControls.isNotEmpty ? prefixControls.first.control : null,
prefixIcon: prefixIconControls.isNotEmpty
? prefixIconControls.first.control
: null,
suffix:
suffixControls.isNotEmpty ? suffixControls.first.control : null,
suffixIcon: suffixIconControls.isNotEmpty
? suffixIconControls.first.control
: null,
counter: counterControls.isNotEmpty
? counterControls.first.control
: null,
icon: iconControls.isNotEmpty ? iconControls.first.control : null,
error: errorCtrl.isNotEmpty ? errorCtrl.first.control : null,
helper: helperCtrl.isNotEmpty ? helperCtrl.first.control : null,
label: labelCtrl.isNotEmpty ? labelCtrl.first.control : null,
customSuffix: null,
focused: _focused,
disabled: disabled,
adaptive: widget.parentAdaptive),
onTap: !disabled
? () {
widget.backend.triggerControlEvent(widget.control.id, "click");
}
: null,
onChanged: disabled
? null
: (String? value) {
debugPrint("Dropdown selected value: $value");
_value = value!;
widget.backend
.updateControlState(widget.control.id, {"value": value});
widget.backend
.triggerControlEvent(widget.control.id, "change", value);
},
items: items,
)
I would still really like to support if, for, and ... in argument lists. If we get that, then in argument lists, we wouldn't need ? without :. You could just use if instead:
BoxDecoration(
if (hovered) color: effectiveHoveredBackgroundColor,
borderRadius: effectiveRadius,
)
Personally, I find that easier to read that ?. I agree with Lasse that the ? operator is getting really heavily overloaded. Even if it isn't technically ambiguous to the parser, it starts to get confusing for the human reader.
Allowing if in argument lists would help for most of these examples, but wouldn't cover use cases outside of argument lists like:
return classDeclaration == null
? null
: "${classDeclaration.name}_$outerName";
final borderSpacing = borderSpacingExpression != null
? tryParseCssLength(borderSpacingExpression)
: null;
value = widget.initialDate != null
? SingleCalendarValue(widget.initialDate!)
: null;
LiveInputRecording? fromJsonMap(Map<String, dynamic>? json) =>
json != null ? LiveInputRecording.fromJson(json) : null;
Honestly, I wish Dart was an expression oriented language and all of those could use if expressions. I don't know how feasible it is to get there from here.
Either, hopefully this data is a little helpful.
Allowing if expressions should be easy.
An if statement would take precedence in expression statements, an if element would take precedence in element contexts, but otherwise we'd just allow if (<expression>) <expression> and if (<expression>) <expression> else <expression> as expressions. The meaning of the else-less expression would be the same as else null.
Precedence meds to be addressed. Could be that of its context, or the same as <conditionalExpression>. (#2306)
Can think of it as a generalization of elements.
Collection elements represents value* because list are sequences. That includes value?. For a collection, no value means no entry.
Argument elements can have an empty else branch, that means passing no argument. They represent argument?.
Interpolation elements could be value*, an empty element would insert nothing in the string, aka the neutral element for string concatenation: the empty string.
Expression elements would then be value, no quantifier because expressions must have precisely one value, a missing result would be the expression default value: null.
That's very interesting information, @munificent, thanks!
It seems that
nullappears fairly often in both then andelsebranches, though theelsebranch is ~2/3 of them.
If the question is whether or not the given occurrence of a conditional expression is a candidate for using the short form that I'm proposing then both branches would be relevant: The ones that have null in the else branch can immediately be abbreviated simply by deleting : and null, and the ones that have it in the "then" branch would need to use the negated boolean expression. This can be achieved by adding !, but it may often be achieved in other ways (for example, expr == null can be turned into expr != null and e.isEmpty can be turned into e.isNotEmpty).
So I think it's fair to say that a bit more than 1 in 3 conditional expressions are candidates to use this feature.
It's an extra bonus that we might standardize these conditional expressions such that nobody needs to spend brain cycles on reading b ? null : e vs. b ? e : null, because we'd effectively always use the latter and omit the : null part.
Also, about 61% occur in an argument list and 39% elsewhere, which means that even in the case where there are other ways to handle a similar situation with an actual argument list, it's still pretty common.
I would still really like to support
if,for, and...in argument lists. If we get that, then in argument lists, we wouldn't need?without:. You could just useifinstead:BoxDecoration( if (hovered) color: effectiveHoveredBackgroundColor, borderRadius: effectiveRadius, )
That's a different semantics: The proposal here is to have a conditional expression that "defaults to null" implicitly, but the actual argument terms you mention are supposed to pass or not pass the argument entirely (at least, that's the kind of proposal I've heard in this context). This is only the same thing if the parameter is optional and it has default value null.
It's worth noting that the current examples have the exact same semantics as what I'm proposing here, but the mechanism that allows passing/not-passing an actual argument has a different semantics and could only be used when it has been checked that the corresponding formal parameter is indeed optional, and it has the default value null.
Honestly, I wish Dart was an expression oriented language and all of those could use
ifexpressions.
Right, that would surely be more readable. However, if we're talking about if expressions (rather than the special forms that actually amount to passing or not passing an actual argument) then it's just a different syntax for exactly the same thing as b ? e1 : e2, and in that case if (b) e1 could have the same meaning as b ? e1 in this proposal, and it would presumably be just as useful.
By the way, if we support passing/not-passing an actual argument using something like if (b) e then this syntax will block the ability to use if expressions as actual arguments, so we'd still need to use b ? e in order to get the semantics which is proposed here, even in the case where if (b) e is an expression with the same meaning as b ? e (when it occurs in other locations than an actual argument list).
The ones that have null in the else branch can immediately be abbreviated simply by deleting : and null, and the ones that have it in the "then" branch would need to use the negated boolean expression.
Yup, good point.
That's a different semantics: The proposal here is to have a conditional expression that "defaults to null" implicitly, but the actual argument terms you mention are supposed to pass or not pass the argument entirely (at least, that's the kind of proposal I've heard in this context).
Correct, but I think in practice, the behavior is identical and the semantics users actually want are "don't pass the argument". Since there's no way to express that today, they approximate it by passing null which in most APIs does the same thing.
then it's just a different syntax for exactly the same thing as
b ? e1 : e2, and in that caseif (b) e1could have the same meaning asb ? e1in this proposal, and it would presumably be just as useful.
Well, yes. But this entire feature is syntactic sugar, so the aesthetics of the syntax matters. :) I find if (b) e1 much easier to read than b ? e1.
so the aesthetics of the syntax matters
Right, but I wasn't saying "don't introduce if expressions", I was saying "if we do introduce if expressions then there's a proposal that corresponds exactly to the proposal I've made here: include if expressions without the else part". I'd support that, too! ;-)