Request: calling a function only when a parameter is not null.
It's awkward to call a function when a parameter is not null, such as:
String toPrint;
if (toPrint != null) {
print(toPrint);
}
This is significantly more work than calling a method when the target is not null:
Printer printer;
printer?.print(toPrint);
So naturally we developers have gotten privileged and want both :)
I wish Dart's null were the Option type, and we had a single-parameter-closure shorthand.
Then you could just write
toPrint.exists(=> print(_))
This could also be easily solved by macros, and the release of macros could include a macro for this.
Rust style macro (I think):
exists!(toPrint, print(_))
This could also be easily solved by macros, and the release of macros could include a macro for this.
Rust style macro (I think):
exists!(toPrint, print(_))
But Rust can do macro expansion and analysis at compile time. That is not a good approach for a language running on a virtual machine. Also, Dart's appeal stems from its simplicity. Complex meta programming is the opposite of simplistic. Only a hand full of Rust gurus even touch macros.
I wish Dart's null were the
Optiontype, and we had a single-parameter-closure shorthand.Then you could just write
toPrint.exists(=> print(_))
And that gets unwieldy pretty quickly. I have written my Rust. While it might be sensible to have Options and Results in a systems programming language, on a higher level they are clutter. And in the real world you only have to perform few null checks.
But to your original problem: With the null aware operator you could just wirte toPrint ?? print(toPrint).
Sadly toPrint ?? print(toPrint) does the opposite of what was asked for - it calls printonly when toPrint is null.
A shorter null guard, like the one requested in #361, would make the existing test + call shorter: toPrint !! print(toPrint). It will even work well with typing, the static type of e1 !! e2 would be the static type of e2 made nullable.
Another option is to make an operator which only applies to arguments, and which short-circuit the surrounding call if the value is null, say: o.foo(bar, ??baz, qux). (It's prefix rather than infix, so it's not the normal null-guard).
This would then evaluate o.foo, evaluate bar, evaluate baz, check that value, and if it is null, then the function invocation evaluates to null.
I don't like that, for a number of reasons:
- It only works for function parameters (not operator operands).
- It only goes one deep, so
foo(bar(??baz))would not be able to short-circuit thefooinvocation, andfoo(??bar(??baz))is a different thing which also depends on thebarreturn value. - It's non-local. I can't see that
foo(..........., ??bar........)might be null unless I notice the embedded??.
So, I'd prefer a proper delimited null-escape. Say, something like (? foo(bar(?:baz))). Here the ?: evaluates its operand, and if null, it short-circuits to the closest enclosing (?...) expressin and makes that evaluate to null. (Might need named braces and null-breaks too, so you can skip further out than the closest enclosing (?....).)
The syntax is obviously bad. Perhaps the (?...) delimiter can be postifx (easier to write, harder to read).
Consider foo(bar(baz?!))!? The ?! takes an expression that may be null, then makes it locally non-null by shortcircuiting the null to the !? suffix which takes something and makes it nullable. (Syntax doesn't needs to be reserved because foo!?.bar() will otherwise be valid).
But Rust can do macro expansion and analysis at compile time. That is not a good approach for a language running on a virtual machine.
Dart already has compile-time constants. I don't think that proves that macros wouldn't have problems, but, I think many people would have made the same arguments against const and that const is actually very beneficial to dart (people constantly want it expanded to do more things!)
Also, Dart's appeal stems from its simplicity....Only a hand full of Rust gurus even touch macros.
I think ideally we have a handful of gurus making simple macros. But you're right, once we release something, its hard to know how it will be abused.
Any solution we have here that's syntax based will likely get really soupy really quick (even !!).
Named operations are much better as the number of operations increases. If we want to do names, a la
ifNotNull(foo, expr(_))`
then we either have to add macros or built-ins like assert (which is basically a macro built into dart).
One more thought, if we add pattern matching, then it might be best to do (no idea on proposed syntaxes)
toPrint =>{ !null => print(toPrint) }
This would be soupy but at least lean on existing concepts.
toPrint.exists(=> print(_))
Just a quick plug that extension methods will allow you to write this (well, except for the implicit parameter lambda):
extension Exists<S, T> on T? {
S? exists(S Function(T) f) => (this == null) ? null : f(this);
}
void test() {
toPrint.exists(print); // calls print on toPrint iff toPrint is non-null
}
@leafpetersen
that is fantastic!
Even without extensions, I think you could do:
R applyIfExists<R, T>(R Function(T) f, T arg) => (arg == null) ? null : f(arg);
which I personally think is more readable/familiar than toPrint.exists(print), which inverts typical order.
Another extension method option is to extend a unary function:
extension Exists<T, R> on R Function(T) {
R? ifArg(T? argument) => argument == null ? null : this(argument);
}
void test(Object? toPrint) {
print.ifArg(toPrint);
}
@leafpetersen
extension Exists<S, T> on T? { S? exists(S Function(T) f) => (this == null) ? null : f(this); }
In Dart 3.5.3 as well as in the Beta and Main channels this leads to the following compile time error:
The argument type 'T?' can't be assigned to the parameter type 'T'.
In Dart
3.5.3as well as in the Beta and Main channels this leads to the following compile time error:The argument type 'T?' can't be assigned to the parameter type 'T'.
this can't be promoted so you would need to either use this! or introduce a new variable using for instance a switch expression:
extension Exists<S, T> on T? {
S? exists(S Function(T) f) => switch(this) {
null => null,
T self => f(self),
};
}
Related to https://github.com/dart-lang/language/issues/190
Pipe operator would help here: toPrint? |> print
I also have a similar use case. I want to apply a transformation if the value is not null. I created an extension function as many suggested in this thread, but in a slightly different way:
extension NotNullExtension<T> on T {
R convert<R>(R Function(T e) fun) => fun(this);
}
And I call it with the null safety operator ?. as follows:
int value = readStrData()?.convert(double.tryParse)?.ceil().remainder(5) ?? 0;