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

null-conditional for boolean operation should be typed to nullable #624

Open
jrmoreno1 opened this issue Jun 29, 2023 · 6 comments
Open

Comments

@jrmoreno1
Copy link

The Null Conditional operator when used in conjunction with a boolean function should result in a nullable value and not require parentheses around it before checking GetValueOrDefault or HasValue

Dim ids As List(Of Integer) = Nothing
If Not ids?.Any() Then 
End If

vs

Dim ids As List(Of Integer) = Nothing
If Not (ids?.Any()).GetValueOrDefault  Then 
End If
@VBAndCs
Copy link

VBAndCs commented Jun 30, 2023

@jrmoreno1
Any checks if the collection has any members. It is a Boolean method and you can't call any other method from it. This is how you should write your code:

        If ids?.Any() Then
            Console.WriteLine(ids.First)
        End If

Or directly:

Console.WriteLine(ids?.FirstOrDefault)

@jrmoreno1
Copy link
Author

@VBAndCs : I know how to make it work, I am saying that how it works is wrong. Any returns a Boolean, ?.Any returns a nullable Boolean, nullable booleans should not require parentheses in order to work.

@VBAndCs
Copy link

VBAndCs commented Jul 4, 2023

@jrmoreno1
the .Any method returns a Boolean, and the dot belongs to it:

        If ids?.Any().CompareTo(False) = 1 Then
            Console.WriteLine("OK")
        End If

        If (ids?.Any().CompareTo(False)).GetValueOrDefault() = 1 Then
            Console.WriteLine("OK")
        End If

@AnthonyDGreen
Copy link
Contributor

Hey @jrmoreno1,

I'm pretty sure I was the language PM when this feature was implemented (or soon thereafter). The behavior you're describing is confusing and the behavior you want makes perfect sense in the exact scenario you're describing. I can't change it but perhaps I can provide some context as to why it behaves as it does by design.

First, it's important to point out the the way ?. behaves is determined syntactically, not semantically. That is to say the behavior of a?.b.c() is decided without regard to whether b is a property or a function or returns Boolean or Integer or a nullable type like String. So when you say "when used in conjunction with a boolean function" that's not how the feature is designed (regardless of whether it should have been).

Second, as to how it is designed--we had a very heated debate when implementing this feature in both VB and C# as to whether the feature would be left-associative or right-associative, i.e. whether a?.b.c.d.e.f would mean (a?.b).c.d.e.f or (made up syntax) a?(.b.c.d.e.f). This is a question of how the syntax is parsed and that impacts how its meaning is determined.

Your original post appears to be requesting that for Boolean functions specifically, ?. be parsed or at least interpreted left-associatively. First, I'll try to explain why we didn't make it left-associatively for all functions/members and then we can discuss whether an exception for booleans is reasonable/possible.

Given the expression id?.First().ToString() if ids is null, left-associative (null?.First()).ToString() would throw an exception. In order to get the current (and likely more desired behavior of safety) one would need to type ids?.First()?.ToString(). In fact, for any expression a?,b,c,d,e,f an exception would be thrown if a is null. The feature would be near useless unless you always typed a?.b?.c?.d?.e?.f. That would be the only safe code.

Interestingly enough, the members of Nullable(Of T) (and, I suppose an extension method which tolerates a null receiver) are literally the only members that are useful to call off of (a?.b) in the case that a is null. Any other member invocation would throw an exception.

Now for value types this isn't that bad because the members if T? aren't the same as the members of T, which is your situation. IntelliSense would naturally lead you to know that the left-associative behavior of a?.b.c was unsafe. But for reference types it would be invisible and just blow up at runtime. We felt that was kinda useless so we made the feature do what it does now where you only need to use ?. immediately to the right of a thing which you expect could be null.

So back to your specific exception: Boolean functions. Booleans tend to be terminal in member access chains--if .M() returns a Boolean there isn't like to be a .M().Anything. In fact that's probably true of most of the primitive value types other than Date because Date has subparts like .Hour/Minute/Second. i.e. I would think your request makes as much sense for If ids?.Count().GetValueOrDefault() Then as well even though Count returns an Integer and not for If dates?.First().IsDaylightSavingTime Then because dates?.First() returns a Date. Hopefully, you agree that toggling the behavior of the feature on whether a?.b() returns an Integer, Boolean, or Date is ... messy.

All of that being said, I agree your situation sucks. I think the reason is that most of what we consider a person will do with nullable result of a ?. expression naturally obviates the need for parentheses. Using a binary operator like a comparison (sorta) does what you'd want and using the "null coalescing" operator If(ids?.Any(), False) would naturally work. Except no one in their right mind would ever write If If(ids?.Any(), False) Then and therein lies the problem. If the syntax had been If ids?.Any() ?? False Then you would be fine. Already, If ids?.Any() Is Nothing Then reads and writes fine (though it's not particularly helpful). I suspect we've forced you to use GetValueOrDefault() and that's why you're here (I could be wrong).

That's a lot of words to say, the team almost certainly can't (and won't) do what you're asking. I tried to provide some context of how we got here. Now all I can do is try to think of ways to help you and others that fall into this unfortunate ditch on the side of the road (in my next comment).

@AnthonyDGreen
Copy link
Contributor

OK, so:

  1. Introducing ?? to VB is out. MSFT isn't adding new syntax to VB and even if they were it's dubious just adding a new syntax that does the same thing (this is perhaps the only syntax in the whole of C# that I envy).
  2. One could imagine a tooling fix wherein when you type If ids?.Any(). the IDE shows members of both whatever Any() returns and the members of nullable of whatever Any() returns only if you pick on of the members of Nullable you get an error telling you to parenthesize the whole expression.
  3. Option 2 by itself is stupid and I only wrote it as a necessary step to Option 3, which would be to do the same thing but if you select a member of nullable, when you commit the line the IDE automatically parenthesizes the expression ids?.Any() for you, which wouldn't be pretty like you want but wouldn't leave others confused and would still be a nice typing experience, I think. This is similar to the experience in the IDE today which lets you pick extension members which are not in scope only to add the necessary Imports statement to your file if you select them.

I personally like Option 3 as a compromise. Not sure if the IDE would accept a PR doing this, and no one has volunteered to do the work, and I also don't know if you find it acceptable. I find a lot of language/tooling decisions I think about lately to fall into a "don't want to have to say it" vs "don't want to have to see it" dichotomy. My solution addressing having to say the parentheses though your original post suggests you don't want to see them either and there just isn't anything to be done about that at this point :(

Warmest regards,

-ADG

@jrmoreno1
Copy link
Author

@AnthonyDGreen : I used Boolean, simply because that was where I was encountering it, and it seems especially egregious. Thanks for the explanation, it does make sense, and a primitive could have a ToString after it…so even they wouldn’t necessarily be the end of the chain. I generally find If(a,b) at least as good as ??, but this is definitely an exception. I have done some analyzers / code fixes but not for a while and not much. I’ll have to check to see if just parentheses does what I want….

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants