-
Notifications
You must be signed in to change notification settings - Fork 84
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
Feature: An alternative approach to handling "null conditional" ops #251
Conversation
…ops, adding them into the grammar at the same level as their respective unconditional siblings.
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 definitely prefer this approach over the one in #7 - thanks very much for the work here, Nigel.
As you can tell, I'm slightly confused about how null conditional invocation expressions work - see comments for details.
Nigel to have a look at whether this PR's approach makes sense, after we had more discussion about why the grammar in #7 was expressed that way. |
Before I look into this properly can I just confirm my understanding of our meeting conversation:
Note: In the second bullet I don't say the "same" as #251 might be described as "null propagating" where a null-conditional operator supplies a null to another null-conditional operator, etc., which by-description results in multiple "is this null" tests but any compiler would likely short circuit. So the semantics might not the same but be equivalent – just a different way of describing the same result. If that summary is either correct, or once it is corrected, I'll look into whether the approach in #7 is required and if not which of #7 & #251 might be more desirable. |
Remove null_conditonal_invocation_expression as it is not implemented.
Remove '?(' - null_conditional_invocation_expression - as it is not implemented
Remove null_conditional_invocation_expression as it is not implemented.
…_element_access to include trailing unconditional_access_part(s) which extend the access to include any immediately following member_access and/or element_access operations. This change alters the semantics so that a null LHS of either operation results in a null being the result of the operation and all immediately following unconditional accesses. Only the grammar required to be changed, the description of the semantics stays the same. Note: This could maybe be defined to include null_conditional access in unconditional_access_part but the current choice was chosen for simplicity – which is of course subjective!
The grammar has been extended so that null-conditional access operations "capture" any immediately following unconditional access operations. This should match the desired semantics, keep the description of these operators in the same part of the grammar as their unconditional siblings. Changes to statement expressions are not included as they feel like a wart that may have a better solution, but they could follow the same as #7 if there is no solution found. @MadsTorgersen is this the intended semantics? |
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.
- "null conditional invocations" (a subset of the existing null conditional expressions) which can be used as statement expressions or lambda bodies - "null conditional projection initializers" (another subset) supporting null conditional member access in anonymous object creation - some other tweaks
A summary of the key differences & changes compared to the original MS:
To test the grammar is parsing as expected I use a simple test file, This is hopefully #251 pretty much done, please comment & compare with #7. |
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.
A few more thoughts, but I definitely like this approach. Will discuss later.
standard/expressions.md
Outdated
: primary_expression '?' '.' Identifier type_argument_list? captured_access* | ||
; | ||
|
||
captured_access |
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 "captured access" a new concept here? If so, might we want to consider naming it something else to avoid it being confused with variable capture within anonymous functions?
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.
This is a new concept, the language doesn't really have any other operators which behave quite in the way the null -conditional ones do – they "capture" following operations (#7 captures more than #251, it is one of the differences, but that isn't fundamental per se and both have to find a way to describe the behaviour). After some iterations I came up with "captured access", I didn't consider the comparison with "variable capture"...
"[null] conditional access" didn't seem right as the accesses themselves are not conditional [and they will fault on null], they are just the subject of another conditional – as the explanation of the semantics in terms of an equivalent conditional (?
/':or
if`) shows. Captured, Controlled, Effected, Impacted, Subject? I just noticed I went with the first alphabetically (pure happenstance) ;-)
Another case where wordsmithing input is welcome!
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.
nested_access ?
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 don't think they’re nested – if anything isn't it the enclosed/first which is impacting the enclosing/following so nested sounds sort of backwards to me? So while captured isn’t great I think its a smidgen better. But is there something better? Words are hard, give me numbers any day ;-)
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.
So just to be clear, a captured/nested access is one that's gated by a null conditional member access - i.e. it looks unconditional in itself, but it's conditional based on the earlier null conditional member access, yes?
"Implicitly conditional access"? "Effectively conditional access"? I'm not liking either of those, but if we can agree on what impression we're trying to give, that'll be a start :)
Assigned to @MadsTorgersen as his "second item" to focus on. |
standard/expressions.md
Outdated
: primary_expression '?' '.' Identifier type_argument_list? captured_access* | ||
; | ||
|
||
captured_access |
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.
nested_access ?
…to language etc. - *null_conditional_invocation_expression* was omitted as an alternative in *method_body* (thanks @gafter) - fixed - the null conditional operations had not been added to the precedence table - fixed - added text specifying how overlapping alternatives must be resolved – take the first; and informative note explaining why the overlapping exists - descriptive simplicity, the rules could be elaborated to remove the overlap, and ANTLR uses the same conveneince to keep grammars shorter/easier to read
Yes, a captured access is one that is “gated” as you say by a preceding one.
So in A?.B.C.D, which groups as ((A?.B).C).D (i.e. same as A.B.C.D does), the .C & .D are captured and if A is null the whole expression is null (but if B or C turn out to be null there is an exception).
“Nested” doesn’t work as C & D are on the outside, it is A?.B that is nested. In an inside-out world it would be it ;-)
Your “implicitly conditional access” is I think the better of the two, but I suspect it implies *too* much – it sounds as though A?.B.C.D is the same as A?.B?.C?.D which isn’t the case, the former will fault if B or C is null while the latter won’t.
“Captured” works I think… partly as it doesn’t try to give the detail and leaves the reader to figure it out from the grammar and prose :-)
… On 8/09/2021, at 2:12 am, Jon Skeet ***@***.***> wrote:
@jskeet commented on this pull request.
In standard/expressions.md <#251 (comment)>:
> @@ -1280,6 +1282,50 @@ In a member access of the form `E.I`, if `E` is a single identifier, and if the
> ```
> Within the `A` class, those occurrences of the Color identifier that reference the Color type are delimited by `**`, and those that reference the Color field are not. *end example*
+### §null-conditional-member-access Null Conditional Member Access
+
+A *null_conditional_member_access* is a conditional relation of *member_access* ([§12.7.5](expressions.md#member-access)) and it is a binding time error if the result type is `void`. For a null conditional expression where the result type may be `void` see ([](expressions.md#null-conditional-invocation-expression)).
+
+A *null_conditional_member_access* consists of a *primary_expression* followed by the two tokens "`?`" and "`.`", followed by an *Identifier* with an optional *type_argument_list*, followed by zero or more *captured_access*es.
+
+```ANTLR
+null_conditional_member_access
+ : primary_expression '?' '.' Identifier type_argument_list? captured_access*
+ ;
+
+captured_access
So just to be clear, a captured/nested access is one that's gated by a null conditional member access - i.e. it looks unconditional in itself, but it's conditional based on the earlier null conditional member access, yes?
"Implicitly conditional access"? "Effectively conditional access"? I'm not liking either of those, but if we can agree on what impression we're trying to give, that'll be a start :)
—
You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub <#251 (comment)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/ABSYVW5J4X24UVRWUAQ3NJTUAYMTLANCNFSM42NTM6MA>.
Triage notifications on the go with GitHub Mobile for iOS <https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675> or Android <https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
|
I'm still uncomfortable with captured given that it has a very specific (and entirely different) meaning elsewhere. I think this we should discuss in the meeting. |
In current meeting: "dependent" has the most backing. (Somewhat reluctantly from all, but better than alternatives.) |
Note "captured" was not used in the text, this is a rule name change only
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.
This is great @Nigel-Ecma
I think I found 2 small nits that I commented on. Once we resolve those, I think we should merge this, in favor of #7
Typos caught by @BillWagner, two rather significant! Co-authored-by: Bill Wagner <[email protected]>
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'm a bit confused about the difference between null_conditional_invocation_expression and null_conditional_member_access, as noted in comments. I suspect this is a problem of me misunderstanding rather than in the proposal, but I'd like to just check it through during our meeting.
standard/expressions.md
Outdated
@@ -1283,6 +1285,50 @@ In a member access of the form `E.I`, if `E` is a single identifier, and if the | |||
> ``` | |||
> Within the `A` class, those occurrences of the Color identifier that reference the Color type are delimited by `**`, and those that reference the Color field are not. *end example* | |||
|
|||
### §null-conditional-member-access Null Conditional Member Access | |||
|
|||
A *null_conditional_member_access* is a conditional version of *member_access* ([§12.7.5](expressions.md#1275-member-access)) and it is a binding time error if the result type is `void`. For a null conditional expression where the result type may be `void` see (§null-conditional-invocation-expression). |
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.
Nit: double space in "and it"
: primary_expression '?' '.' Identifier type_argument_list? dependent_access* | ||
; | ||
|
||
dependent_access |
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.
Just to check, primary_expression
will cover nested null-conditional member accesses, yes? So if we have:
x?.M1()?.M2().M3
that ends up with a "top-level" null_conditional_member_access where the primary_expression is x?.M1()?.M2()
, which is itself a null_conditional_member_access with a primary_expression of x?.M1()
, which is itself a null_conditional_member_access with a primary expression of x
... is that right?
@@ -1283,6 +1285,50 @@ In a member access of the form `E.I`, if `E` is a single identifier, and if the | |||
> ``` | |||
> Within the `A` class, those occurrences of the Color identifier that reference the Color type are delimited by `**`, and those that reference the Color field are not. *end example* | |||
|
|||
### §null-conditional-member-access Null Conditional Member Access |
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've just been trying to figure out how this works in a couple of cases, and while I think it's the right approach, we may need a bit more explanation.
For example consider:
string x = null;
int? y = x?.GetHashCode();
My understanding is that that is a null_conditional_member_access with a dependent_access of ()
- so we get the null value. It's not a null_conditional_invocation_expression, because that's only used in places where the result (if any) is discarded. Is that the case? If so, it's a bit confusing because x.GetHashCode()
is an invocation expresson, isn't it?
I'm not suggesting that we change any of the existing text - just consider adding an example/note.
| block | ||
; | ||
``` | ||
|
||
When recognising an *anonymous_function_body* if both the *null_conditional_invocation_expression* and *expression* alternatives are applicable then the former shall be chosen. |
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'm not sure about this one. To go back to my previous example, consider:
Func<string, int?> func = x => x?.GetHashCode();
Does that satisfy the requirements of this paragraph? We'd still want to use an expression with a null_conditional_member_access rather than a null_conditional_invocation_expression in order to avoid "losing" the return value, wouldn't we? I may have missed something here.
I thought the distinction was meant to handle the case where an invocation expression had a |
I agree with the motivation - but then the part about choosing null_conditional_invocation_expression over expression confuses me. |
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.
Thanks for the change, Nigel. I think I now understand and it's all good :)
We believe the change has now been made (or a slightly different version)
Following on from the discussion on #7 at the last meeting this is a draft PR adding the null conditional operations into the grammar at the same level as their respective unconditional siblings.
Some of this is guesswork/invention...
On the invention side in particular note that three terminals:
?.
,?(
and?[
; have been added to Operator_Or_Punctuator.