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: null-elimination operator - prefix "?" #219

Closed
tatumizer opened this issue Feb 11, 2019 · 17 comments
Closed

Proposal: null-elimination operator - prefix "?" #219

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

Comments

@tatumizer
Copy link

tatumizer commented Feb 11, 2019

This proposal is based on ideas from several commentators from this thread. (In other words, I don't pretend these ideas are mine) :-)

Proposal: implement null-elimination operator ?expr to drop value of null, in contexts where such operation makes sense.
Examples:

  • passing optional named parameters: suppose we have foo({int x=0}). Then the call
    foo(x: ?expr) is equivalent to calling foo() if expr evaluates to null, and to foo(x: expr) otherwise
  • passing (last) optional positional parameter: suppose we have foo([int x=0]) - then the call foo(?expr) is equivalent to foo() if expr evaluates to null, and to foo(expr) otherwise (only valid for the last such parameter. I'm not sure this makes sense though - to be discussed).
  • in map literals, e.g. { "key": ?expr } is {} if expr evaluates to null, and {"key": expr} otherwise
  • similarly in list literals.

Advantages:

  • allows to use full power of null-chaining null-safety features in expressions like [ 1, 2, ?(a?.b?.c) ]
  • solves the problem of dropping parameters while calling methods with optional parameters (improves null-safety, requires the callee to "sign" its part of non-nullability contract)

Precedents:
In a rudimentary form, we already have a device similar to null-elimination operator - with spread collections, we can write: [ ...?list] - when "list" is null, nothing gets inserted. The proposal is consistent with this feature and takes it just one step further: if elimination makes sense for the list, it seems logical that it should make sense for a single value, too.

Extra: a bit more controversial use case - in "if" statement, e.g.
if (?x) {...} - same as if (x != null)
if (var x = ?(a?.b?.c)) { ... } - like "let" in swift, but no new keyword is necessary, "var" works fine

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Feb 12, 2019
@lrhn
Copy link
Member

lrhn commented Feb 12, 2019

I assume map literals can work on both key and value, {?key: ?value} is an empty map if either key or value is null.
If we had tuples, maybe (?a, ?b) would be null if either a or b is null, and a tuple otherwise (so we can make inner and outer joins!)

It is annoying that positional parameters can't be handled better, but probably unavoidable as long as we don't have rest parameters (those would be similar-typed sequences that are combined in a single list, so you can omit values like in a list).

Another potential use-case: yield ?e;, which either yields nothing or a non-null value, equivalent to if (e != null) yield e;. Yields are also part of sequence generation, so it makes sense.

@ds84182
Copy link

ds84182 commented Feb 12, 2019

Still sorta on the fence when it comes to the syntax. Maybe an if keyword instead? foo(x: if expr), {"key": if expr}, yield if expr, return if expr?

@andreashaese
Copy link

Maybe the syntax to express this is already there: I'm thinking of ? :, ??, and _:

a > 3 ? a : _ // only a if it's greater than 3
a ?? _ // only a if it's not null

I'm not aware of other languages that do this, so take that with a grain of salt. Also note that _ currently can't be used this way, because to date _ is a readable variable. Along with proposals like #204 this could change at some point though.

@ds84182
Copy link

ds84182 commented Feb 13, 2019

@tatumizer My issue with the syntax is that it feels a question mark feels odd in a prefix position. I'd like it more if the question mark was against an another operator (e.g. a suffix to : and yield).

@andreashaese
Copy link

andreashaese commented Feb 25, 2019

The question mark achieves effectively the same without ever mentioning Nothing.

In my eyes ?null would essentially mean Nothing, and so would your suggested abbreviation ?. Does that affect the previous position on the matter?

@andreashaese
Copy link

Yes, that's exactly my concern. The concept of "Nothing" is already hidden in plain sight: if (false) 42 and ?null both are "Nothing literals".

With that said, "Nothing" is no data type. It's purely syntactical sugar, as you wrote. As such, it shouldn't be confused with the Optional type, or used in situations where the latter makes more sense.

@andreashaese
Copy link

"Nothing" is a logically necessary concept: whenever you throw it out the door, it comes back through the window.

Sure, but that's an argument for null and the Optional type, not for "null/expression elimination".

Being able to conditionally ignore expressions would be a pure convenience feature, to make your code shorter. That's tempting in cases where it's super obvious what's happening, but I'm concerned that things like foo(bar: ?x) and yield ?x actually make your code less clear, no matter the syntax.

In my opinion, many of the (valid) pain points could better be remedied with Optionals. And sometimes, a good ol' if is just the right tool.

@lrhn
Copy link
Member

lrhn commented Mar 26, 2019

It's a common theme that some expression should only be evaluated to completion if some sub-expression is non-null.

The x?.foo case is simple because x is the first expression to evaluate here.
We could also do the same for function invocations: foo(?arg) where it only calls the function if arg is non-null. The tricky case here is to delimit the expression that is short-circuited on null. Just shorting the entire surrounding expression is probably not useful, but eliding too little is also a problem.
Just eliding up to the nearest nullable type context would be too unpredictable.

Imagine an expression of the form (?? any-expression(?subexpresssion)) where the entire surrounding (??...) expression evaluates to null if any (?...) subexpression does.
Then a =?? b (from #288) would be (?? a = ?b) and foo(?a) would be (??foo(?a)).

It's nigh unreadable, though. :)

@eernstg
Copy link
Member

eernstg commented Mar 26, 2019

Forgive me for over-interpreting the proposals here, in order to emphasize a particular danger that seems to be relevant.

I can see proposals where the prefix ? causes some subexpression to be omitted (e.g., omit a named argument), and that's a bit more specialized, but the overall topic seems to be that the prefix ? can "make something disappear" if the operand is null. And that's a topic that we have discussed in the language team way back.

For that, I'm a little bit worried about having a large construct and then making that construct mean something very different (namely null) in case there is something in the middle which happens to be absent. Consider the following:

var mySet = {
  canvas.drawParagraph(
      (ui.ParagraphBuilder(
        ui.ParagraphStyle(textDirection: ui.TextDirection.ltr),
      )..addText('Hello, world.'))
          .build()
            ..layout(ui.ParagraphConstraints(width: logicalSize.width)),
      ui.Offset(...)),
  ui.window.render((ui.SceneBuilder()
        ..pushClipRect(physicalBounds)
        ..addPicture(ui.Offset.zero, ?picture)
        ..pop())
      .build()),
}

So do we want that whole thing to happen if and only if that picture is not null? It may be tempting to allow such embedded ? expressions to control a larger and larger enclosing expression, because that makes this mechanism "more and more powerful", but I think it's dangerous with respect to the overall readability of the code.

So when we're considering this ability to "call it off", I think it's important to consistently force that property up to the front:

var mySet = let ?v = picture in {
  canvas.drawParagraph(
      (ui.ParagraphBuilder(
        ui.ParagraphStyle(textDirection: ui.TextDirection.ltr),
      )..addText('Hello, world.'))
          .build()
            ..layout(ui.ParagraphConstraints(width: logicalSize.width)),
      ui.Offset(...)),
  ui.window.render((ui.SceneBuilder()
        ..pushClipRect(physicalBounds)
        ..addPicture(ui.Offset.zero, v)
        ..pop())
      .build()),
}

The idea is that we can mark the whole construct as "call-offable" by using ?v in the declaration of some (final) variables up front. The reader would then know that the whole thing is just null if any of the ?v variables end up being null, and it is executed if they are all non-null.

It might still be valuable enough to consider special cases like "omit this named argument", but for the more general case where we want to call it off I think it's important to make sure that nothing is called off by a tiny detail in the middle of a big construct.

@eernstg
Copy link
Member

eernstg commented Mar 27, 2019

Calling function f(?arg) only when arg != null? No, I never meant that! :)

Sorry about over-interpreting! That was exactly the kind of semantics that we've had proposals for on earlier occasions.

@lrhn
Copy link
Member

lrhn commented Dec 13, 2019

If we consider only the collection element case, and only the "element computation is null" case, then it might work. Say we use "suffix ??" as marker (a ?? with nothing after it, which we only make syntactically valid after a collection element):

 var x = [1, 2, computation() ??, 4];

That could work. The problem is that it only works for single elements, not for map entries. We cannot do {x: y ??} because it's not clear whether it's x or y or both being tested. We'd probably have to allow for {x??: y, z: w??} where you test each part and omit the entirety if one is null (left join/right join?).

It's not a good orthogonal feature, but it mirrors the nullability of ... vs ...?. (Which is probably also why prefix ? feels more right to me).

@lrhn
Copy link
Member

lrhn commented Dec 14, 2019

Ah, the "suffix ??" was something I discussed with someone somewhere, so it was in my head while writing this.
It makes a kind of sense to have an element ?? condition with no else branch, like we do for if-elements, so it would be consistent ... in some ways. Just not particularly readable, or generalizable.

@nxcco

This comment has been minimized.

@Levi-Lesches
Copy link

I personally vote for value ?? default, it reads more naturally: "pass this value, but if it's null use the default value instead".

@leoshusar
Copy link

This would be useful for late final variables if you sometimes don't need to inizialize them right in the constructor initializer.
Something like this:

class Something {
  late final String _text;

  class({String? text})
    : _text = ?text; // if text is null, _text won't get assigned anything and can still be assigned later
}

@sudha-anecure
Copy link

sudha-anecure commented May 3, 2023

I see this has been closed as "completed". Was this implemented? Can someone please link a reference to how we can achieve this?

@lrhn
Copy link
Member

lrhn commented May 8, 2023

It has not been implemented. The issue was closed by the original poster, not by the language or implementation teams.

It's an idea that we are aware of, but haven't found any optimal syntax for yet.

The general idea is a way to make a large expression evaluate to null eagerly if a subexpression evaluates to null.
We currently can only do that with null-aware member access, where the outer expression is automatically the selector chain, with no way to control that.
(An even more general concept is to break out of a nested expression with any value, not just null.)

So, the idea is not forgotten, even if this particular feature proposal is closed.

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

9 participants