mirror of
https://github.com/luau-lang/luau.git
synced 2025-05-04 10:33:46 +01:00
RFC: Do not implement safe navigation operator
This meta-RFC proposes removing the previously accepted RFC on safe navigation operator. This is probably going to be disappointing but I feel like this is the best course of action that reflects our ideals in language evolution. ## Problem The RFC specifies a new navigation operator, `?.`, that returns `nil` when the left hand side is `nil`, as well as skipping the rest of the indexed chain evaluation (e.g. `vehicle.Turret?.Frame.CFrame.LookVector` evaluates to `nil` if `vehicle.Turret` is `nil`). Initially the RFC only specifies this one operator, although future extensions like `?:`, `?[]`, `?()` are possible -- the RFC stands on its own, but it opens the path to more "nil-safe" operators in the future. Unfortunately, we discovered a significant problem (after the RFC was merged) in the operator, in its interaction with the Roblox instance hierarchy. This is a userdata based DOM tree that (unfortunately) overloads __index to access children, and (fortunately?) attempting to access a non-existent child raises an error. As such, it's not compatible with this proposal in the sense that trying to use `?.` in combination with the instance hierarchy will only work if the child is present, and will raise an error otherwise. ## Alternatives There are several ways to address that problem and still ship this; all of them are unsatisfactory: 1. We can change the instance hierarchy at Roblox to return `nil` instead. If Luau existed 15 years ago with `?.` feature, we probably would not have implemented dot-access in the first place, or maybe had it return `nil`... However, at present this is a very significant change that very dramatically changes the contract around instance access and may have large unintended consequences. It's something that isn't obviously a good idea in the first place, and not something we'd want to do for a single language feature as other features may pull us into maintaining the status quo instead. 2. We can ignore this footgun and/or try to help with static analysis. The issue here is that static analysis must depend on us knowing the precise type and tree location of the first object in the chain, something that will very often not work reliably, and the pattern is too convenient and too common - so we should probably admit that with or without analysis, people will be hitting this often accidentally. This can be treated as a reasonable compromise - we're adding a convenient feature that requires a little care around some constructs, and as long as the users "know what they are doing" it's going to be okay - but my perspective is that we should try to keep the language as free of footguns / as orthogonal / as simple as possible, and any feature that has significant issues around these must be a critical feature we can't live without for us to tolerate these. 3. We can introduce a new metamethod, `__safeindex`, that will be called for `?.` instead of `__index`, if present. This metamethod can then be defined for Roblox instance hierarchy to return `nil`. My perspective here is that while it removes the footgun, it violates the orthogonality / simplicity of the null safety operators in general - just for this one operator it now requires new VM opcodes, new table lookup functions that use either `__safeindex` or `__index` metamethods, and creates questions for any future null safety operators wrt whether or not we will go through the same process for consistency (eventually yielding `__safecall`, `__safenewindex`, `__safeadd` etc. in the extreme), or if `__safeindex` is a one-off ad hoc addition to solve a specific problem we have for one of the (well, largest) users of Luau language. This turns the safe navigation operator from "cheap convenient syntax sugar" to "full blown operator every element of the stack must be aware of", which - again, in my perspective - shifts the balance for this feature, as the costs no longer seem to justify the benefit. ## Conclusion While we have ways forward for this proposal, they either make the language less robust [when used in Roblox environment], mean this feature is much less orthogonal/simple than initially understood, or mean we need to dramatically change the access rules [in Roblox environment] for a single small language feature. While it's unusual to judge a feature of a general purpose language based on a single user of the said language, given Luau's heritage and the fact that the majority of the programmers who interact with Luau presently use it in context of Roblox, I think we must take this into account. These situations will probably happen rarely - in fact it's the first feature proposal like this! - but when they do, we should strive to keep the language simple and devoid of footguns, thus I believe it's overall beneficial to maintain the status quo and not implement this proposal. As suggested in the RFC, this still means you can use `and` operator as a replacement for very basic single-element chains, e.g. `dog and dog.name`, but for more complex chains with `nil`s unfortunately the existing longer patterns must be used. We plan to look into common subexpression elimination that, under certain conditions, will allow us to maximally efficiently evaluate seemingly redundant expressions like `foo and foo.bar and foo.bar.baz`, and if that doesn't work you'd need to settle for multiple expressions to evaluate these chains efficiently in presence of nils - that said, I think in this case shorter code for some cases like this is not a strong enough motivation to ship the feature in the face of the problem discussed above. cc @Kampfkarren for visibility
This commit is contained in:
parent
fb9c4311d8
commit
1c6a7e61a6
1 changed files with 0 additions and 104 deletions
|
@ -1,104 +0,0 @@
|
|||
# Safe navigation postfix operator (?)
|
||||
|
||||
**Note**: We have unresolved issues with interaction between this feature and Roblox instance hierarchy. This may affect the viability of this proposal.
|
||||
|
||||
## Summary
|
||||
|
||||
Introduce syntax to navigate through `nil` values, or short-circuit with `nil` if it was encountered.
|
||||
|
||||
|
||||
## Motivation
|
||||
|
||||
nil values are very common in Lua, and take care to prevent runtime errors.
|
||||
|
||||
Currently, attempting to index `dog.name` while caring for `dog` being nil requires some form of the following:
|
||||
|
||||
```lua
|
||||
local dogName = nil
|
||||
if dog ~= nil then
|
||||
dogName = dog.name
|
||||
end
|
||||
```
|
||||
|
||||
...or the unusual to read...
|
||||
|
||||
```lua
|
||||
local dogName = dog and dog.name
|
||||
```
|
||||
|
||||
...which will return `false` if `dog` is `false`, instead of throwing an error because of the index of `false.name`.
|
||||
|
||||
Luau provides the if...else expression making this turn into:
|
||||
|
||||
```lua
|
||||
local dogName = if dog == nil then nil else dog.name
|
||||
```
|
||||
|
||||
...but this is fairly clunky for such a common expression.
|
||||
|
||||
## Design
|
||||
|
||||
The safe navigation operator will make all of these smooth, by supporting `x?.y` to safely index nil values. `dog?.name` would resolve to `nil` if `dog` was nil, or the name otherwise.
|
||||
|
||||
The previous example turns into `local dogName = dog?.name` (or just using `dog?.name` elsewhere).
|
||||
|
||||
Failing the nil-safety check early would make the entire expression nil, for instance `dog?.body.legs` would resolve to `nil` if `dog` is nil, rather than resolve `dog?.body` into nil, then turning into `nil.legs`.
|
||||
|
||||
```lua
|
||||
dog?.name --[[ is the same as ]] if dog == nil then nil else dog.name
|
||||
```
|
||||
|
||||
The short-circuiting is limited within the expression.
|
||||
|
||||
```lua
|
||||
dog?.owner.name -- This will return nil if `dog` is nil
|
||||
(dog?.owner).name -- `(dog?.owner)` resolves to nil, of which `name` is then indexed. This will error at runtime if `dog` is nil.
|
||||
|
||||
dog?.legs + 3 -- `dog?.legs` is resolved on its own, meaning this will error at runtime if it is nil (`nil + 3`)
|
||||
```
|
||||
|
||||
The operator must be used in the context of either a call or an index, and so:
|
||||
|
||||
```lua
|
||||
local value = x?
|
||||
```
|
||||
|
||||
...would be invalid syntax.
|
||||
|
||||
This syntax would be based on expressions, and not identifiers, meaning that `(x or y)?.call()` would be valid syntax.
|
||||
|
||||
### Type
|
||||
If the expression is typed as an optional, then the resulting type would be the final expression, also optional. Otherwise, it'll just be the resulting type if `?` wasn't used.
|
||||
|
||||
```lua
|
||||
local optionalObject: { name: string }?
|
||||
local optionalObjectName = optionalObject?.name -- resolves to `string?`
|
||||
|
||||
local nonOptionalObject: { name: string }
|
||||
local nonOptionalObjectName = nonOptionalObject?.name -- resolves to `string`
|
||||
```
|
||||
|
||||
### Calling
|
||||
|
||||
This RFC only specifies `x?.y` as an index method. `x?:y()` is currently unspecified, and `x?.y(args)` as a syntax will be reserved (will error if you try to use it).
|
||||
|
||||
While being able to support `dog?.getName()` is useful, it provides [some logistical issues for the language](https://github.com/Roblox/luau/pull/142#issuecomment-990563536).
|
||||
|
||||
`x?.y(args)` will be reserved both so that this can potentially be resolved later down the line if something comes up, but also because it would be a guaranteed runtime error under this RFC: `dog?.getName()` will first index `dog?.getName`, which will return nil, then will attempt to call it.
|
||||
|
||||
### Assignment
|
||||
`x?.y = z` is not supported, and will be reported as a syntax error.
|
||||
|
||||
## Drawbacks
|
||||
|
||||
As with all syntax additions, this adds complexity to the parsing of expressions, and the execution of cancelling the rest of the expression could prove challenging.
|
||||
|
||||
Furthermore, with the proposed syntax, it might lock off other uses of `?` within code (and not types) for the future as being ambiguous.
|
||||
|
||||
## Alternatives
|
||||
|
||||
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?""`.
|
||||
|
||||
It is possible to make `x?.y = z` resolve to only setting `x.y` if `x` is nil, but assignments silently failing can be seen as surprising.
|
Loading…
Add table
Reference in a new issue