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

Null-aware elements #323

Open
alorenzen opened this issue Apr 22, 2019 · 20 comments
Open

Null-aware elements #323

alorenzen opened this issue Apr 22, 2019 · 20 comments
Assignees
Labels
feature Proposed language feature that solves one or more problems small-feature A small feature which is relatively cheap to implement.

Comments

@alorenzen
Copy link

alorenzen commented Apr 22, 2019

Admin comment: This is under implementation: feature specification, implementation issue

Null-aware elements offer a shorthand for adding an element to a list, if the element is non-null. Consider this code before the feature:

Stack(
  fit: StackFit.expand,
  children: [
    const AbsorbPointer(),
    if (widget.child != null) widget.child!,
  ],
)

The causes repetition of widget.child, and is verbose. With null-aware elements this becomes:

Stack(
  fit: StackFit.expand,
  children: [
    const AbsorbPointer(),
    ?widget.child,
  ],
)

Original issue contents:

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
Copy link
Member

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
Copy link
Author

@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
Copy link
Member

cc @munificent

@munificent
Copy link
Member

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
Copy link
Member

lrhn commented Apr 26, 2019

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
Copy link
Member

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 lrhn added the feature Proposed language feature that solves one or more problems label Jul 7, 2020
@lrhn lrhn added the small-feature A small feature which is relatively cheap to implement. label Jul 8, 2020
@lrhn
Copy link
Member

lrhn commented Mar 23, 2022

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
Copy link
Member

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

@PeterMcKinnis
Copy link

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
Copy link
Member

eernstg commented Apr 7, 2022

That's a neat trick! 😃

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
Copy link
Member

lrhn commented Apr 7, 2022

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
Copy link

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
Copy link
Member

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
Copy link
Member

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
Copy link

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
Copy link

Mike278 commented Nov 16, 2023

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
Copy link
Member

munificent commented May 10, 2024

I just added an in-progress proposal for this: 329f626

Thanks again for the excellent suggestion @alorenzen!

@Levi-Lesches
Copy link

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
Copy link
Member

lrhn commented May 10, 2024

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
Copy link

tatumizer commented May 10, 2024

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)

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 small-feature A small feature which is relatively cheap to implement.
Projects
Status: Being implemented
Development

No branches or pull requests