-
Notifications
You must be signed in to change notification settings - Fork 483
RFC: Safe navigation operator #142
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
RFC: Safe navigation operator #142
Conversation
|
Also missing from this is what to do about them when they are in lvalue assignments, |
Co-authored-by: Alexander McCord <11488393+alexmccord@users.noreply.github.com>
|
Good catch on not mentioning that, though do we have to make it a syntax error? I was assuming that if a ~= nil then
a.b = c
end |
|
For the sake of usability I'd suggest |
|
Works for me, I'll put it in the RFC. |
|
|
||
| ```lua | ||
| dog?.name | ||
| dog?.getName() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. This would be equivalent to (dog?.getName)() under current/intuitive grammar. What was the intent here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intent is for getName to be ran only if dog is not nil, which matches how a few languages, including TypeScript, interpret this code. I can see this being ambiguous, but that is the intent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is the expression gets short circuited when the ?. fails. That's why a?.b.c.d.e will work, even if (a?.b).c.d.e wouldn't (because (a?.b) is it's own expression, that gets replaced with nil, then indexed by c.d.e)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the expression boundary though? E.g. a?.b + 1 presumably doesn't short circuit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd have to read up on how an expression is expressed in the Luau parser, but no.
I'm not really sure how to word this, or what ambiguities I'm missing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To find all that, we have prior art to help us along most of the way. It's also great because it helps to maintain some consistency in the behavior across languages.
Here's JavaScript's RFC on optional chaining: https://tc39.es/proposal-optional-chaining/
From some light reading, the reason why t?.m() automagically works in JavaScript is because they have ?. start a different branch of the grammar that is identical to normal indexing syntax, but parses in a different way if ?. is involved. Some expert in JS syntax will be able to confirm that.
For example, here's two call sites (I won't get into TemplateStringsArray because that's a red herring):
t.m`hi`; // this is parsed as a function call that passes some TemplateStringsArray
t?.m`hi`; // this is illegal syntax
As a result of that, the expression (t?.m)() would limit the scope of safe navigation to the expression t?.m. () is a dangerous operation here because it might call some undefined value. The same applies in t?.x + 5, because + operator is not part of the index access/function call operation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. I think we should be explicit about the semantics of these operators in this RFC so that questions like "why can t?.x + 5 cause a runtime error while t?.x() can't" are obvious from how the RFC specifies the behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll write down in the RFC how binary operators like + operate, as well as how parentheses will operate. Is there anything else I should be thinking about?
| The list of valid operators to follow the safe navigation operator would be: | ||
|
|
||
| ```lua | ||
| dog?.name --[[ is the same as ]] if dog == nil then nil else dog.name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the intent actually for this to only work with nil values, rather than a general falsiness check? It has odd interactions with operators like and if not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is only for nil. I think that attempting to index a false value should error. I mention the falsiness thing in the dog and dog.name mention of the RFC.
|
|
||
| Doing nothing is an option, as current standard if-checks already work, as well as the `and` trick in other use cases, but as shown before this can create some hard to read code, and nil values are common enough that the safe navigation operator is welcome. | ||
|
|
||
| Supporting optional calls/indexes, such as `x?[1]` and `x?()`, while not out of scope, are likely too fringe to support, while adding on a significant amount of parsing difficulty, especially in the case of shorthand function calls, such as `x?{}` and `x?""`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't say x?[index] or x?(...) are fringe cases. The following example comes to mind which uses both:
local function sendMessage(object, message, ...)
object?.__prototype?[message]?(object, ...)
endThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line was added by request from Luau maintainers, I think it's a reasonable request but perhaps not something that needs to be in this initial RFC.
| @@ -0,0 +1,137 @@ | |||
| # Safe navigation postfix operator (?) | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the operator cannot be used without . and : is it even a standalone operator ? or are these really operators ?. and ?:?
For example, C# calls them Null-conditional operators ?. and ?[] and that seems to align better with the behavior that is described here (but ? and . are still separate tokens)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't ? just be sugar for if x then x... else nil? It feels like we could support them and have this be its own operator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vegorov-rbx It's probably just simpler documentation-wise to say that ?. and ?[] is a whole new operator than to explain the internal details of how ? actually works from the parser/codegen standpoint. I imagine that we'd do the same in our documentation.
@bradsharp I don't think that would be a 1:1 sugar. You don't want redundant calls to __index for every ?./?:/?[ in the expression path.
local print_index_mt = {
__index = function(self, key)
print(key)
return rawget(self, "_" .. key)
end
}
local t = setmetatable({
_p1 = setmetatable({ _p2 = 5 }, print_index_mt)
}, print_index_mt)
print(t?.p1?.p2)
-- Should print the following:
--> p1
--> p2
--> 5
--
-- And not:
--> p1
--> p1
--> p2
--> 5There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexmccord ah yes I didn't consider that. A more accurate version would be:
local _temp = <lhs>
<result> = if _temp then _temp<rhs> else nil|
One thing that was raised internally where this RFC doesn't specify the behavior exactly, and depending on the implementation may be a significant burden, is what happens when the function call returns more than one value. E.g. |
|
I think returning all values if not nil, returning one nil otherwise, is the ideal outcome. I'll specify that in the RFC. |
We've discussed this internally. The big conundrum remains the semantics of The RFC as specified ( IOW I don't think we can commit to supporting this sort of multret handling today - maybe we can do that in the future but right now it feels uncomfortable. I was originally thinking we can use the simple So we're a bit at the crossroads. We could:
I personally feel like 3 is the best option because it seems like safe navigation is still useful and it's semantically unambiguous, and this option keeps the road open for future support for calls if we decide to implement it. If we do go with 3 then we'd need to prohibit Thoughts welcome. |
|
I am open to going for option 3 in terms of implementation, but if the problems with |
|
Our design process isn't really disjoint from implementation - for example, if we discover issues during implementation we may pull the RFC post-factum completely (as happened with #34) or revise it. If an RFC can only be partially implemented in the short term we won't be able to mark it as implemented for a while which is a sign that the RFC should be split in two. Cases like this occasionally come up after the RFC gets approved, but when we identify these cases during the RFC process it's better to correct them ahead of time. |
|
Fair enough. I'll get to cutting out ?: and ?.() when I can. |
|
Double-reminder to self to finish that--been busy |
|
RFC has been updated to focus on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
|
I'd like to raise awareness about the fact that Something we can probably do is create a lint pass for this case where you generate a warning given an expression
However, this lint pass cannot help in all situations. Maybe the script is in non-strict mode and so most of the types are local function GetPlayerPosition(player)
return player?.Character?.HumanoidRootPart?.Position
end |
|
Oh, that's actually disappointingly problematic! I didn't even think about that, but that's a fairly strong driving force behind the feature 😦 |
Rendered document