language icon indicating copy to clipboard operation
language copied to clipboard

Request: calling a function only when a parameter is not null.

Open MichaelRFairhurst opened this issue 6 years ago • 15 comments

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 :)

MichaelRFairhurst avatar May 17 '19 17:05 MichaelRFairhurst

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(_))

MichaelRFairhurst avatar May 17 '19 17:05 MichaelRFairhurst

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(_))

MichaelRFairhurst avatar May 17 '19 17:05 MichaelRFairhurst

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 Option type, 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).

MarvinHannott avatar May 19 '19 05:05 MarvinHannott

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 the foo invocation, and foo(??bar(??baz)) is a different thing which also depends on the bar return 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).

lrhn avatar May 20 '19 06:05 lrhn

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.

MichaelRFairhurst avatar May 20 '19 17:05 MichaelRFairhurst

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.

MichaelRFairhurst avatar May 20 '19 17:05 MichaelRFairhurst

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 avatar May 20 '19 23:05 leafpetersen

@leafpetersen

that is fantastic!

MichaelRFairhurst avatar May 21 '19 17:05 MichaelRFairhurst

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.

jamesderlin avatar Jun 16 '19 06:06 jamesderlin

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);
}

lrhn avatar Jun 17 '19 08:06 lrhn

@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'.

Wikiwix avatar Oct 31 '24 15:10 Wikiwix

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'.

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),
  };
}

jakemac53 avatar Oct 31 '24 15:10 jakemac53

Related to https://github.com/dart-lang/language/issues/190

FMorschel avatar Dec 04 '24 19:12 FMorschel

Pipe operator would help here: toPrint? |> print

ghost avatar Dec 06 '24 03:12 ghost

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;

mabdelaal86 avatar Sep 23 '25 19:09 mabdelaal86