Skip to content

Null-aware subscript operator #28389

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

Closed
rkj opened this issue Jan 13, 2017 · 29 comments
Closed

Null-aware subscript operator #28389

rkj opened this issue Jan 13, 2017 · 29 comments
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). core-l type-enhancement A request for a change that isn't a bug

Comments

@rkj
Copy link

rkj commented Jan 13, 2017

For example map access operator[]. It would be nice to be able to write something like:

return nullableMap.?['property'].?['subproperty']

but there is currently no proper syntax, so one needs to either do this:

if (nullableMap == null) return null;
var prop = nullableMap['property'];
if (prop == null) return null;
return prop['subproperty'];

or slightly shorter but hard to read:

return ((nullableMap ?? {})['property']) ?? {})['subproperty']
@MichaelRFairhurst
Copy link
Contributor

Syntactically, what about nullableMap[?'property']?

That fits with darts .? well. Operator first, question second. So .? becomes [?.

And what about other overloaded operators? nullableIntlike +? 5, nullable ==? other, etc. Are these supported, or should they be?

@rakudrama
Copy link
Member

The null aware getter / call operator is ?., e.g. node?.left?.right?.toString()

I believe extension to indexing via a?[i] is ambiguous since a?[1]?[2]:3 could mean
(a?[1]) ? [2] : 3 or
a ? ([1]?[2]) : (3)

If operators were allowed to be used in a call-like syntax, e.g. a.+(b) or a.operator+(b), a.[](i) or a.operaror[](i), then the null-aware call could follow from this, i.e a?.+(b) or a?.operator+(b), a?.operator[](i)

I think we would need the operator part, otherwise there are more ambiguities: -x could mean this.operator-(x) or x.operator unary-().

@MichaelRFairhurst
Copy link
Contributor

MichaelRFairhurst commented Jan 13, 2017

Dang. Always get them confused. Thought I saw '.?' in a discussion about this and went off of that.

Hmm, well, .? or ?., its being treated as a token, it seems.

In that case ?[, ?+, ?- could all be tokens as well and it wouldn't be ambiguous. In that case a?[1]?[2]:3 would be a syntax error because no ternary was begun before :.

Of course, that might be a crappy error in terms of diagnosability from someone who doesn't know there's a ?[ operator. And while .? is tokenized, if it weren't, a?.b would be a syntax error, where a?[b is currently not a syntax error if followed by ]:expr.

So not necessarily ambiguous, but not quite backwards compatible.

At least '?+' isn't ambiguous. But unary minus is a great point. ?- has the same problems ie a?-b?-c:d (dart doesn't have unary +, does it?)

+1 for for adding call-like syntax, its a feature that enables the other feature. But unary minus does make that one weird too.

@munificent munificent added the area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). label Jan 13, 2017
@munificent
Copy link
Member

Hmm, well, .? or ?., its being treated as a token, it seems.

In that case ?[, ?+, ?- could all be tokens as well and it wouldn't be ambiguous. In that case a?[1]?[2]:3 would be a syntax error because no ternary was begun before :.

That's a good point. We could tokenize ?[ as a single lexeme. That might trip people up if they try to use a list literal in a conditional operator:

getStuff() => condition ?[foo] : [bar];

If they don't put a space there, it will get tokenized wrong. That might be tolerable, though. dartfmt always spaces that out.

@Hixie
Copy link
Contributor

Hixie commented Jan 14, 2017

An alternative is to have a method that does the same as the operator []. Then you can just use .?, as in:

return nullableMap?.lookup('property')?.lookup('subproperty')

@lrhn
Copy link
Member

lrhn commented Jan 15, 2017

Adding a lookup that does the same as []` is a bad idea from a usability perspective. It means you have two ways to do the same thing - except that you should only use "the alternative" (which always works) when you are syntactically prevented from using the preferred one (which doesn't work in all situations).

That's just a sign of bad design. We should make the preferred one work all the time. The only question is which syntax to use for it. Introducing new and different syntax would just be the language designers admitting defeat.

We have the problems that

  • you cannot make operators null-aware.
  • you cannot tear off operators.

These are different - the former is an invocation, the latter is a reference-by-name (no arguments). Since operator syntax depends on operands for parsing, we'll probably need something different for tear-offs.

There are lots of options. The biggest problem with just adding a ? for null-awareness is ambiguity with the conditional expression syntax, otherwise x ?- y and x?[42] would be fine. Maybe it's enough to pick a default that makes existing code still work the same, and new code will need parentheses to avoid the ambiguity, but equally likely it's not going to work in practice because people will keep getting surprised by the syntax.

(Tear-offs are likely easier, but there are also more options: o.+, o.#+, o::+, (o +), (o + _), or o[], o.[], o::[], o[#], or something completely different).

@Hixie
Copy link
Contributor

Hixie commented Jan 16, 2017

One problem with using punctuation for adding null-aware variants of all the operators, which is also a problem with providing punctuation-based syntax for tearing off operators, is that you eventually turn the language into impenetrable line noise, like perl. The current syntax is mostly intuitive. The syntax for constructor tear-offs is less so (I believe I heard that syntax was going to be removed?). I would be very wary of adding more features if those features were not intuitive.

foo == null ? null : foo['bar'] isn't great, but it's better than foo?.['bar'], IMHO.

@munificent munificent changed the title Support nullability for operators Null-aware subscript operator Jan 17, 2017
@munificent
Copy link
Member

The syntax for constructor tear-offs is less so (I believe I heard that syntax was going to be removed?).

Yes, generalized tear-offs are gone.

I think we're hoping to add in support for constructor tear-offs that matches the method tear-off syntax:

var tearOff = SomeClass.someNamedConstructor;
var another = SomeClass.new; // For the unnamed constructor

Personally, I think that's more intuitive than the weird # generalized tear-off syntax.

@MichaelRFairhurst
Copy link
Contributor

MichaelRFairhurst commented Jan 18, 2017

The simplest tear off syntax faces this (syntactic but not semantic) ambiguity: if x + is a tearoff expression of type (T1) -> T2, then x + (y) could be y applied to tearoff x +, or it could be an AddExpression with operands x and y. Maybe this is fine, maybe its fine but a lot of work to fix.

But it also has issues with tearing off unary operators. Granted, tearing off unary operators into a noarg closure doesn't seem super crazy important...but there are a lot of them (++, --, ~, -). Oh wait, you can't override ++, or --...but maybe in the future?

From that perspective, I'd be inclined to say that x.operator+(y) is the best syntax (with x.?operator+(y) as the nullable version), but having x.operator++ would, unintuitively, be a tearoff and not an increment!

Unary minus is treated as operator negate I think, that should stick around.

I think this is the best I can think of:

  x.++; // tear off x++
  x.++(); // call x++ after tearoff
  x?.++(); // increment x if not null
  x?.+(y); // correct

seems good so far, but this is where it gets fishy.

  x?.+ y; // syntax error.
  x.[]; // tear off
  x?.[](y); // yucky yucky no-good null safe lookup
  x.?[y]; // hooow about this instead....

Definitely getting pretty perly. That second to last one makes me gag. Luckily I don't think its necessary.

Also, if unary minus is operator negate, that means unary minus tearoff is x.negate. That coincidentally brings us back to having two ways of doing operators. Not ideal. Or, if it is ideal, we maybe should just run with that.

Could be keywordized:

  x operator negate; // tearoff
  x operator negate(); // actually negate
  x? operator++(); // null-safe inc. But breaks current programs using vars name operator, ie x ? operator++ : y
  x? operator+ (y);
  x? operator + y; // syntax error
  x operator []; // tearoff
  x? operator [y]; // null safe index

In both cases, x.++ and x operator++ look to me like they should be incs not tearoffs. I'm not sure I see away around that unless we for instance add tearoff as a keyword.

Maybe its better just to catch x.++; as a lint rule. But still, here goes a new keyword...

  x tearoff operator negate; // tearoff of negate
  x operator++; // call inc. Disallowed since it adds no value?
  x? tearoff operator++; // tearoff of null-safe inc
  x? operator++; // null-safe inc
  x? operator + y; // nullsafe add x to y
  x? tearoff operator +; // nullsafe tearoff of +
  x tearoff operator [];
  x? operator[y];

I don't really like tearoff as a keyword, and I'm not sure if there's anything better.

I'd vote for in the style of x.?[y] if only it worked for unary minus. Maybe that's a small price to pay.

Seeing all the other syntactic complexity that comes up, I'm starting to lean to the simplicity of having an extra method name.

  x.operatorNegate;
  x?.operatorNegate;
  x?.operatorNegate();
  x.operatorAdd(y);
  x?.operatorAdd;
  x.?operatorIndex(y);
  x.?operatorIndexSet(y, z);

It is much, much much easier to understand this code without looking at a manual. And at 11 characters minimum, operatorAdd, it won't see much use even if it is technically a "new way" that could reduce consistency.

@lrhn
Copy link
Member

lrhn commented Jan 18, 2017

I wouldn't use x+ as a tear-off by itself, it's too ambiguous. I'd probably require parentheses, like (x+) which makes it very clear that something is missing. See also Haskell (https://wiki.haskell.org/Infix_operator).

For .operatorname (e.g., x.+) I think we are fairly safe wrt. ambiguity, even if we prefix the . with a ?. Examples x.+, x.-, x.[], x.[]=. These are not valid anything else.
The usual problem is unary-, but I have no problem not allowing tear-offs of unary operator. It's like trying to curry a function and provide all the parameters. Then only unary operator members are unary- and ~, so it's not a big loss. The operators ++, -- and ! are not members, just syntax, so there is nothing to tear off (no more than you can tear off if). We can use x.~ if we want to, but x.- should be the binary one and x.unary- is ambiguous. Still, a very viable idea.

The problem with x.operator+(4), as you say, is that operator is already a valid member name. That means that (no matter how distasteful) the following is currently valid:

class C {
  get operator => 15;
}
main() {
  var c = new C();
  print(c.operator+(21+16));
}

so taking that notation for something else is a breaking change.
It's probably not going to break anything in practice, but we have to be careful.
It's not that unrealistic that a class exists with a getter named operator (e.g., if the program is a Dart compiler).

Adjacent identifiers are not used, so an alternative, as you mention, is x operator+ (no .), but that doesn't work very well with null-awareness - then it becomes x?operator and we are back in conflict-with-conditional-expression territory.

@MichaelRFairhurst
Copy link
Contributor

MichaelRFairhurst commented Jan 18, 2017

It is worth noting that !, ++' and -- could very well become overloadable operators. It'd be best to support them.

It's not that unrealistic that a class exists with a getter named operator (e.g., if the program is a Dart compiler).

Preeetty sure this is BinaryExpression exactly. :)

And I thought that unary minus worked like

  class MyOverloader {
    operator negate() {
      // overload unary minus here
    }
  }

but I was wrong. Looks like its just operator -() {...} and that can coexist with operator -(x) {...}.

That makes my tearoff operator negate option not consistent with how unary minus is treated elsewhere, and means we need a plan to disambiguate the two without an very reusable precedent.

Once again, this makes me think the method names thing best: handles all our cases, in a way that's easy to understand without docs, in a way that doesn't look like perl, that disambiguates unary ops, and works for both tearoffs and null awareness intuitively. Hard to beat that I think.

@munificent
Copy link
Member

It is worth noting that !, ++' and -- could very well become overloadable operators.

I think it's really unlikely we've overload ++ and --. Those do assignment, which gets into all of the murky territory around lvalues, references, etc. Dart doesn't let you override = either.

@MichaelRFairhurst
Copy link
Contributor

MichaelRFairhurst commented Jan 18, 2017

I would have thought that would be all the more reason to allow overriding ++...like, you could try to treat x++ as by using the overridden + operator, but then that would require assignment (x = x + 1). Treating ++ as a method call which then supposedly mutates is simpler from that perspective.

Of course, not for immutable objects like int, so I think that's where you're coming from, which makes sense.

@munificent
Copy link
Member

Treating ++ as a method call which then supposedly mutates is simpler from that perspective.

Yeah, I don't think it should mutate. ++ should always semantically work as if it were syntactic sugar for the explicit:

x = x + 1;

@leafpetersen
Copy link
Member

This ranged fairly far afield. Do we have anything actionable here on the original topic? Do we (in the short to medium term) want to push on a null aware subscript?

@munificent
Copy link
Member

Do we (in the short to medium term) want to push on a null aware subscript?

I don't personally feel it's a burning issue right now. I'd rather focus on most structural language changes (strong mode, etc.) and do small-scale syntax additions later.

@lrhn lrhn added the type-enhancement A request for a change that isn't a bug label Jun 25, 2018
@lukepighetti
Copy link

lukepighetti commented Jul 18, 2018

Just posted #33903 and then I saw this. Hope we can get something going.

@lrhn
Copy link
Member

lrhn commented Aug 28, 2018

I think we should consider x?.[4] as the null-aware version of x[4], just as we allow x..[4] as the cascade version of x[4]. That is both consistent, memorable and not too ugly.

@lukepighetti
Copy link

lukepighetti commented Aug 28, 2018

Would it be more consistent if we allowed people to access properties of maps like they do in JS?

Map object = {"foo":"bar"};

object["foo"] // bar
object.foo // bar

object?.["foo"] // bar
object?.foo // bar

@matanlurey
Copy link
Contributor

matanlurey commented Aug 28, 2018

@lukepighetti That is not a Map in JS, that is an Object:

// ES6 JavaScript
var object = {"foo": "bar"};
object.foo; // OK

var map = new Map();
map.set("foo", "bar");
map.foo; // Error
map.get("foo"); // OK

Dart does not have true anonymous objects. You could simulate one using dynamic and noSuchMethod, but your experience will not be great. In any case, the JS-map is actually worse than the Dart one (.get versus []).

@lukepighetti
Copy link

lukepighetti commented Aug 28, 2018

I'm not privy to the details on how these languages work, I just figured that if you could build ?. into a map you could also build a .

@matanlurey
Copy link
Contributor

x?.y is just sugar for:

if (x != null) {
  x.y;
}

.. it is quite different from what you are asking.

@lukepighetti
Copy link

lukepighetti commented Aug 28, 2018

Oh, sorry, I figured the x.y would be sugar for x['y']

and I believe x?.['y'] is actually sugar for

( x ?? {} )['y']

and like I said, I'm not privy on the language details, so I can't say why one is easier than the other

@lrhn
Copy link
Member

lrhn commented Aug 30, 2018

Dart maps are not like JavaScript objects. They have methods and and their keys are not necessarily strings, so map.keys cannot be equivalent to map["keys"] because it would prevent you from accessing the keys iterable on the map. Using dot-notation member access as map key-access is just not viable in a language like ... well, in anything that's not like JavaScript and using JavaScript objects as maps.

The notation x?.[k] would be equivalent to x == null ? null : x[k] (except, as usual, that it only evaluates x once). It does not evaluate k at all if x is null, exactly the same as if it has been written x?.get(k).

It could be argued that the syntax should really be x?[k] because the . isn't there in x[k], unlike the dot in x?.member which comes from x.member. That's just too bothersome to parse (and read!) because a stand-alone ? already means something else. We already have x..[k] and x..member for cascades, so it's not that long a stretch to also have x?.[k] and x?.member for null-aware access.
And maybe x?..member for null-aware cascade :)
The real exception is that we allow you to omit the . for `x[k].

(We could also add x?.+ 4 for null-aware operators, but it doesn't propagate nearly as well as sequences of selectors - is (x?.+ 2) * 4 expanded to let the * be null aware? What about 4 * (x?.+2)?)

@lrhn lrhn added the core-l label Dec 6, 2018
@jamesderlin
Copy link
Contributor

jamesderlin commented Apr 12, 2019

I suspect that this is a terrible idea, but out of curiosity (and maybe for the sake of completeness), what about adding an implementation of Null.operator[] that always returns null?

@lrhn
Copy link
Member

lrhn commented Apr 12, 2019

@jamesderlin
It's a terrible idea 😃.

The point of not allowing method invocations on null, and non-nullable types in general, is to catch errors where something is accidentally null. By just trucking on, you camouflage the error, likely causing the eventual crash to happen much later, which makes it much harder to debug.

A "contagious null" would be one that allows every operation, and always returns null (that's how NaN works for double-operations). It is another possible design for null, but it suffers from the late-error issue. That's the behavior you suggest for [] only.

Dart has eager errors on null member invocations. We have given you the option of getting contagious null behavior by using ?.. The issue here is that it only works for plain method/getter/setter invocations, not operators.

We can fix the [] operator the same way it works for cascades, cascades are e1..[e2] = e3 so [] would be e1?.[e2] = e3. We likely will do something like that, likely together with non-nullable types.
That still won't help the other operators, but because all other user-definable operators (and non-user-derfinable for that matter) are infix, we just don't have a good place to put the ?.

@lukepighetti
Copy link

I dunno, I kind of like it as a solution.

@ThinkDigitalSoftware
Copy link

ThinkDigitalSoftware commented May 21, 2019

Same. I expected x?[y]. No "." because it's a map, not a member variable. But no pressure from me. I don't understand the specifics easier and will take what I can get 😁

@leafpetersen
Copy link
Member

We are adding a null aware subscript operator as part of the upcoming NNBD release. See additional discussion here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). core-l type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests