dart-lang / language

Design of the Dart language
Other
2.64k stars 198 forks source link

Null-aware elements #323

Open alorenzen opened 5 years ago

alorenzen commented 5 years ago

I would love to have a "null-aware" operator for adding an element to a list if it is non-null.

[
  foo,
  ?bar,
]

This would be equivalent to:

[
  foo,
  if (bar != null) bar,
]

I've been trying out the new "UI-as-code" features in the angular codebase, and find that I can't convert a common pattern to use the new element syntax.

We often iterate over a list, adding elements to a new list when the value is non-null.

var result = [];
for (var node in nodes) {
  var value = convert(node);
  if (value != null) result.add(value);
}

It would be pretty nice if I could instead use the new syntax:

var result = [
  for (var node in nodes) ?convert(node);
]
bwilkerson commented 5 years ago

In the short term, you can nest the if inside the for:

var result = [
  for (var node in nodes) if (node != null) convert(node)
]
alorenzen commented 5 years ago

@bwilkerson I want to check the value of convert(node), not node for nullability. You're example would require me to call convert twice, which I do not want to do:

var result = [
  for (var node in nodes) if (convert(node) != null) convert(node)
]
leafpetersen commented 5 years ago

cc @munificent

munificent commented 5 years ago

This is an interesting corner. We could do some kind of single-value null-aware element syntax like you suggest. I'm very hesitant to pile more semantics onto ? because it's already quite overloaded in the grammar (postfix for nullable types, in null-aware operators, and in conditional expressions).

In many cases, you can use the other UI-as-code features like Brian suggests, but in this case you want to avoid any redundant computation. Of course, the natural way to do that is with a local variable, which makes me think building the list imperatively like you do today probably is the best approach.

lrhn commented 5 years ago

We could introduce a let construct.

[ let tmp = something in if (tmp != null) tmp ]

which is obviously not as short as [? something], but it is more generally useful. The current existing workaround is

[ for (var tmp in [something]) if (tmp != null) tmp ]

or

[... [something].where(isNotNull)]

or (after NNBD)

[... [something].whereType<Object>()]

All of these will likely introduce an intermediate list, with only the first example being likely to have that optimized away.

munificent commented 4 years ago

I think the only difference is that in #219, you propose allowing ? to elide arguments in argument lists as well. Otherwise, I think they're the same.

lrhn commented 2 years ago

Just saw code of the form:

List<Property<Foo>> get elements => [
        something,
        other,
        orWhatnot,
        andYetOne,
        andFinally,
      ].whereNotNull().toList();

I tried to suggest using a literal with [if (something != null) something, ...], to avoid creating two lists where only one is needed, but the names were fields, so promotion wouldn't work. With null-aware elements, it would literally just be:

List<Property<Foo>> get elements => [
        ?something,
        ?other,
        ?orWhatnot,
        ?andYetOne,
        ?andFinally,
      ];

That would be awesome!

srawlins commented 2 years ago

Agreed! Null-aware spread has spoiled me. We have a fancy null-aware element, but not a simple one.

PeterMcKinnis commented 2 years ago

Nullable could be updated to implement the Iterable interface yielding either 0 or 1 items. Then we could just use the spread operator.

int? a = null;
int? b= 5;
final items = [
   ...a,
   ...b
];

// items is [5]
eernstg commented 2 years ago

That's a neat trick! :smiley:

But I'm afraid it requires every type to implement iterator (and a bunch of other methods), such that the null object can be the empty iterable, and every other object can be a one-element iterable, and that would not work. For instance, this creates a conflict for an object which is already an iterable yielding a bunch of other objects.

lrhn commented 2 years ago

If we had Nullable<T> as a type, shorthanded as T?, then we could assign methods to that. (C# has such a type, although it's probably a very special system type).

Or if we could iterate based on an extension get iterator, then we could assign one to T? only. Might surprise someone when their Iterable<X>? f; something; [...f] will give them a list containing a single iterable, not spread the iterable itself. True, it will apply to all the non-nullable subtypes which have no iterator themselves, which could be considered error-prone.

PeterMcKinnis commented 2 years ago

But I'm afraid it requires every type to implement iterator (and a bunch of other methods), such that the null object can be the empty iterable, and every other object can be a one-element iterable, and that would not work. For instance, this creates a conflict for an object which is already an iterable yielding a bunch of other objects.

Interesting! Didn't realize that nullables were implemented without a wrapper object. I have always wondered why e.g. int?? isn't a type. Now I know. Sigh...I wish we had first class discriminated unions.

munificent commented 2 years ago

In case you're curious, I wrote a blog post a while back about the rationale behind that design choice: https://medium.com/dartlang/why-nullable-types-7dd93c28c87a

rakudrama commented 1 year ago

The current existing workaround is

[ for (var tmp in [something]) if (tmp != null) tmp ]

... All of these will likely introduce an intermediate list, with only the first example being likely to have that optimized away.

Some time ago I took a shot at getting dart2js to optimize using a singleton list as a 'let'-expression and it was surprisingly difficult to get anything reasonable. I did manage to remove the allocation, but there were all kinds of weird artifacts in the generated that would not exist with a 'let'-expression. I would say that "being likely to have that optimized away" is too optimistic.

Reprevise commented 9 months ago

This should work for maps too, like:

final String? seasonId = '1';
final queryParameters = <String, Object>{
  'seasonId': ?seasonId, // the '?' could instead go in front of the key, instead of the value
};

If Dart allowed for inferring the key, it would look better:

final queryParameters = <String, Object>{
  ?seasonId,
};

Then again, now that I'm reading it, maybe Dart shouldn't allow for inferring the key, looks weird 😅.

Mike278 commented 9 months ago

FWIW patterns help with this - the example in the original post could be written as:


var result = [
  for (var node in nodes) 
    if (convert(node) case var converted?) 
      converted
];
munificent commented 3 months ago

I just added an in-progress proposal for this: https://github.com/dart-lang/language/commit/329f626a9bae65585065471d1cc59e236d7cf58b

Thanks again for the excellent suggestion @alorenzen!

Levi-Lesches commented 3 months ago

Looks great!

I know this is a separate issue. but would it be possible to extend this to optional parameters as well?

final padding = isMobile ? null : EdgeInsets.all(6);
return Container(
  padding: ?padding,
  child: Text("This may or may not have padding"),
);
lrhn commented 3 months ago

Argument elements is a different issue. It will likely need/want more than just null-aware arguments. At least if-arguments to. And then there is the complication of omitting a non-trailing positional argument.

tatumizer commented 3 months ago

And then there is the complication of omitting a non-trailing positional argument.

This is an intractable problem (semantic paradox), it cannot be solved without some restrictions, e.g. prohibiting conditionally omitting positional arguments. In the proposed null-aware operator in collections, ?value doesn't move the insertion position if the value is null; while passing optional parameters this attitude might be counterintuitive.

(Edited by removing some nuts)