Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: operator !! (opposite of ??) #361

Open
MichaelRFairhurst opened this issue May 17, 2019 · 18 comments
Open

Proposal: operator !! (opposite of ??) #361

MichaelRFairhurst opened this issue May 17, 2019 · 18 comments
Labels
feature Proposed language feature that solves one or more problems null-aware-expressions Issues proposing new expressions to manage null

Comments

@MichaelRFairhurst
Copy link

MichaelRFairhurst commented May 17, 2019

Proposed solution to #360.

Where a ?? b is sugar for a != null ? a : b, there may be value in adding !! defined as a == null ? null : b.

Example: calling a function when the parameter is not null.

String? toPrint;
toPrint !! print(toPrint);

Example: awaiting a future when its not null

Future? fut;
fut !! await fut;

Example: asserting an object is fully initialized

assert(name !! age !! email !! password != null)

Similar to #219 except I think it has a few different advantages:

  • extremely simple and consistent with an existing well-used and understood dart concept
  • binary operator so left-to-right flow is easier to follow
  • wouldn't interact with any other syntax (prefix ? is was proposed to interact with maps, lists, optional parameters, etc.)

Disadvantages:

  • using ! and not throwing an exception on null is confusing. Maybe ?! or !??
  • Limited usefulness
  • repeat lhs required to take advantage of promotion
@MichaelRFairhurst MichaelRFairhurst added the feature Proposed language feature that solves one or more problems label May 17, 2019
@lrhn
Copy link
Member

lrhn commented May 20, 2019

With this definition, x! would be x ?? throw, and x?.foo() would be x !! x.foo().
Arguably, we have swapped the operators when we defined ?? (but then, we didn't have the suffix ! back then).

The operation, by itself, seems reasonable.
It may not be intuitive what x !! y means, and the places where it will be used, I don't think it looks very good.
The static typing works. If e1 has static type T1 and e2 has static type T2, then e1 !! e2 has static type T2?. That is a little confusing because it shows that the value of e1 is ignored except for the null-check.
That is also the biggest issue: you have to repeat the expression, which means you only get promotion for local variables. You can't do this.foo !! requiresNotNull(this.foo) because it won't promote. We should be able to promote var x = ...; x !! requiresNotnull(x);.

If you could write the !! on the expression, and that makes it break out of the context, then you only need to write it once: (? requiresNotNull(this.foo!!)) could evalute this.foo, then if it is null it shortcircuits the current expression and makes the surrounding (?...) evaluate to null.

@MichaelRFairhurst
Copy link
Author

That is also the biggest issue: you have to repeat the expression, which means you only get promotion for local variables. You can't do this.foo !! requiresNotNull(this.foo) because it won't promote.

Great catch Lasse...

@mathieudevos
Copy link

As per #706 I would like to suggest a slight change: instead of !!; I would suggest the following: !?

@nex3
Copy link
Member

nex3 commented Mar 13, 2021

As another data point, this has been a substantial source of pain in Dart Sass's null-safe refactor (I count about 40 instances of foo == null ? null : ...). For now, I'm using this (based on Rust's Option type):

extension NullableExtension<T> on T? {
  V? andThen<V>(V Function(T value) fn) {
    var self = this; // dart-lang/language#1520
    return self == null ? null : fn(self);
  }
}

but it's awkward, particularly due to the lack of constructor tear-offs.

That is also the biggest issue: you have to repeat the expression, which means you only get promotion for local variables. You can't do this.foo !! requiresNotNull(this.foo) because it won't promote.

Great catch Lasse...

#1201 provides a neat solution to this in (var this.foo) ?! requiresNotNull(foo).

@caseycrogers
Copy link

caseycrogers commented Jun 2, 2021

+1 to this proposal-regardless of the specific characters used to implement the operator.

I'm coming from Scala world where Options are king and the following is an extremely common paradigm:

Option<Foo> maybeFoo = getFoo();
bar(maybeFoo.map((val) => processFoo(val)));

This comes up commonly with default parameters in Dart:

int foo(String? overrideStr) => return (overrideStr != null ? int.parse(overrideStr) : null) ?? 42;
print(foo(maybeGetUserInput());

This is already supported if we're trying to call a class method. Notice how much readability improves when such an operator is supported:

extension StringToInt on String {
  int toInt() => int.parse(this);
}

int foo(String? overrideStr) => return overrideStr?.toInt() ?? 42;
print(foo(maybeGetUserInput());

Adding a "compute iff not null" operator would be highly useful and make writing functional-style code in Dart much friendlier.

@lrhn
Copy link
Member

lrhn commented Jun 2, 2021

That map looks like an operation which Dart could just define:

extension OptionMap<T> on T? {
  R? map<R>(R? Function(T) convert) => this == null ? null : convert(this);
}

I'd have made it an operator if we could have generic operators.

@ykmnkmi
Copy link

ykmnkmi commented Jun 2, 2021

Is dart supports operator type args?

@munificent
Copy link
Member

Is dart supports operator type args?

No, operators cannot be generic methods in Dart.

@Levi-Lesches
Copy link

Levi-Lesches commented Jun 30, 2021

I have to say, I've seen a few issues with exactly this proposal -- and it would massively help with Flutter code.

child: data == null ? null : MyLongWidgetName(
  data: data,
  more: properties,
)

// vs

child: data !! MyWidget(data: data, more: properties)

(keep in mind, this is usually a few indents deep)

@lrhn, do you think this can be considered to be added to Dart?

@lrhn
Copy link
Member

lrhn commented Jun 30, 2021

This syntax (or something equivalent) has been proposed a few times before.

My main worry is that it only works for variables which can be promoted, and it relies on promotion.
For e1 ?? e2, we only check whether e1 is null, and if not, we use the value of e1.
With e1 !! e2, we'd check that e1 is non-null, and then not use the value. (I guess we use the value if it is null).
So, you have to re-create the value later. That feels wasteful.

Not an impossible feature, can definitely be useful, and if we combine it with inline variable declaration, it might also avoid reading the value twice and work with non-variables;

  child: (var x = something.data) !! MyLongWidgetName(data: x, more: properties);

(That's with a the proposal to bind-and-promote in local variable declarations, like #1420).

Another issue is that by making the !=null check implicit, it only works with that check, not any other type promotion.

Another option would be a nullable conditional expression with no else branch (the else branch is always null),

  data != null ?: something(data)

But, honestly, I don't think an implicit null else branch is such a big advantage over

 data != null ? something(data) : null

which is being explicit about what happens, and likely easier to read. Introducing an implicit null is something I'd prefer to avoid.

@lrhn lrhn closed this as completed Jun 30, 2021
@lrhn lrhn reopened this Jun 30, 2021
@Levi-Lesches
Copy link

Levi-Lesches commented Jul 1, 2021

But, honestly, I don't think an implicit null else branch is such a big advantage over

data != null ? something(data) : null

I agree, and that's where I went with it at first. But let's do this in the context of Flutter, since that's where it's most useful:

child: data != null ? Center(
  child: MyWidget(dataObject: data)
) : null

Well, I decided I didn't like that hanging : null, since Flutter usually ends in ). So, I negated the condition and switched it around:

child: data == null ? null : Center(
  child: MyWidget(dataObject: data)
)

Now, with a slightly longer name and a few more indents:

class MyCard { 
  List<Widget> children;

  @override
  Widget build(BuildContext context) => Card(
    child: ListTile(
      subtitle: children == null ? null : Column(
        crossAxisAlignment: CrossAxisAlignment.start
        children: children,
      )
    )
  );
}
class MyCard { 
  List<Widget> children;

  @override
  Widget build(BuildContext context) => Card(
    child: ListTile(
      subtitle: children !! Column(
        crossAxisAlignment: CrossAxisAlignment.start
        children: children,
      )
    )
  );
}

With much indentation and long widget names, the extra == null ? null : eats a lot of characters (not to mention the linter/formatter are at 80 characters 😬). That's where the request for !! comes from.


Re: duplication, I would be in support of a syntax like:

class MyCard { 
  List<Widget> children;

  @override
  Widget build(BuildContext context) => Card(
    child: ListTile(
      subtitle: (var children) !! Column(
        crossAxisAlignment: CrossAxisAlignment.start
        children: children,
      )
    )
  );
}

@caseycrogers
Copy link

caseycrogers commented Dec 12, 2022

I realized this might just be well served just with an extension.
Bscly this here:
#361 (comment)

extension Optional<T extends Object> on T {
  V apply<V>(V Function(T) func) {
    return func(this);
  }
}

Highly contrived pure Dart and Flutter examples:

Foo? getValue() {...}

ProcessedFoo? processedValue = getValue()?.apply((v) => processFoo(v));
// or, slightly simplified:
processedValue = getValue()?.apply(processFoo);
class MyWidget extends StatelessWidget {
  const MyWidget(this.child);

  final Widget? child;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: child?.apply(
        (c) => Padding(
          padding: const EdgeInsets.all(4),
          child: c,
        ),
      ),
    );
  }
}

It's a tad verbose but its more explicit than !! and is more readable+doesn't require saving to a variable like using a ternary operator.

@lrhn
Copy link
Member

lrhn commented Dec 13, 2022

The apply function will definitely do the job.
Its main issue is that it introduces a new function body, which means you have to add another async and an await on the result if the body needs to do await. That's why a language feature would be more convenient. (But in the absence of such a language feature, I have made extension methods similar in spirit to apply, for more specialized cases.)

The apply extension is very similar to the pipe operator (#1246). I'd have used operator> for it, if operators could be generic and null-aware, but sadly they can be neither. A (e ?-> processFoo) would be short, and mostly readable.

Patterns offer ways to test for null and bind variables, so that's annother approach.
With patterns, the shortest version I can think of (with the currently proposed syntax) is the expression:

   switch(e){var v? => processFoo(v), _ => null)

(A nullable switch expression, which doesn't require being exhaustive, so has an implicit _ => null default) has been discussed, but not decided on. With #2664, it could be (e case var v?) ? processFoo(v) : null.)

@SupposedlySam
Copy link

I love this proposal, but I believe a better syntax for this would be ?| read null or.

Then, your example could be updated to this instead @Levi-Lesches

class MyCard { 
  List<Widget> children;

  @override
  Widget build(BuildContext context) => Card(
    child: ListTile(
      subtitle: children ?| Column(
        crossAxisAlignment: CrossAxisAlignment.start
        children: children,
      )
    )
  );
}

Additionally, it would be great if a tear-off could also be used

Original

return maybeData == null ? null : utf8.decode(maybeData)

After

// Normal use with new `null or` operator
return maybeData ?| utf8.decode(maybeData);

// Sugar syntax
return maybeData ?| utf8.decode;

@gruvw
Copy link

gruvw commented Oct 22, 2023

I really like the before mentioned proposal of @SupposedlySam, especially this tear-off idea.
However, I find the ?| symbol somewhat disturbing, as we are all used to | for binary or eager-evaluted boolean sum.

I think we should either use ?|| or ?!.

By the way, one can use the following dart function meanwhile:

Y? $<X, Y>(X? x, Y? Function(X v) y) => x == null ? null : y(x);

It can lead to a smaller expression, depending on the length of the original x variable name.

@gruvw
Copy link

gruvw commented Aug 30, 2024

Meanwhile, I created a dart extension called NullMap on any nullable type to achieve this behavior.
I packaged it under the namp name and published it on pub.dev: https://pub.dev/packages/nmap.

Usage

final int? a = null;
final int b = 1;
                                     
print(a.nmap((n) => n + 1)); // null
print(b.nmap((n) => n + 1)); // 2

@caseycrogers
Copy link

caseycrogers commented Dec 16, 2024

Meanwhile, I created a dart extension called NullMap on any nullable type to achieve this behavior. I packaged it under the namp name and published it on pub.dev: https://pub.dev/packages/nmap.

Small feedback/PSA for other readers:
After using this kind of pattern in the wild for awhile I've realized it's probably better to make your extension on Object NOT Object?.

Why? This changes the usage to a?.nMap(...) which makes it so if you refactor a nullable variable to be non-nullable, you'll get lint warnings anywhere where you used the transform pattern on it. Makes it so you don't accidentally end up with a bunch of null unpacking on non null fields.

@gruvw
Copy link

gruvw commented Dec 16, 2024

@caseycrogers interesting... I am not sure though how one would write it differently than I did on the namp package:

https://github.com/gruvw/nmap/blob/302bdf178b7ce19cb161e9c33ac2d9581171b46f/lib/src/nmap_base.dart#L1-L22

Edit: never mind, I misunderstood your initial comment, you would indeed change the usage to a?.nMap(...). You would prefer an extension more in the lines of #361 (comment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems null-aware-expressions Issues proposing new expressions to manage null
Projects
None yet
Development

No branches or pull requests