-
Notifications
You must be signed in to change notification settings - Fork 1k
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
[Proposal]: Extending patterns to "as" #8210
Comments
How would you reconcile this with all the guard methods that were recently introduced, such as The reason they exist is to avoid the whole By using the old I don't disagree with what you are proposing in essence (I've built something similar using I've actually completely replaced all of these in our codebase with the guard methods recently. For example: this.user = user ?? throw new ArgumentNullException(nameof(user)); With: ArgumentNullException.ThrowIfNull(user);
this.user = user; I don't think approving this with the intent of using it for argument validation is a great idea without also reconsidering all the guard methods: an ideal solution would handle both things. The ultimate approach to me would be to properly add contracts to the language and allow this (which has been discussed a lot in multiple issues by now): public MyClass(string user, int value)
where user is not null
where value is >= 0 and < 100
{
this.user = user;
this.value = value;
} |
That seems like a runtime issue. This pattern woudl be fairly trivial/idiomatic. So it could be detected and converted to the same IL that would be emitted for calling into the helpers. |
It's a good point that there are these methods to use, but there also situations where you might not want to produce an exception, or the existing guard methods don't cover all patterns you might want. There is also a potential of another neat way of parameters checking, using the proposed syntax: this.x = ArgumentOutOfRangeException.ThrowIfUnmatched(x as >= 0 and <= 255); |
I don't follow you here @CyrusNajmabadi . How would code like: this.b = b as >= 0 ?? throw new ArgumentOutOfRangeException(nameof(b)); Avoid having to pass in the explicit exception type, the Or perhaps you are suggesting something like this (which would be very interesting if possible)? this.b = b as >= 0 ?? throw; In this case, the compiler could see the various trivial comparisons and pick the correct exception type and message. Is this what you are alluding to? |
That presumes that the specific corresponding native guard method exists. And, if they do, that means they have a specific code pattern in them. For example I'm saying, if your callsite has effective the identical pattern that calling the guard method would emit, then the runtime can/should treat them uniformly. There should be no penalization that you happened to call one vs the other. |
Oh I see what you mean now @CyrusNajmabadi , but that was not my point. I wasn't saying one would miss "functional" benefits by using a different syntax, but maintenance-related ones. For example, having to provide the Similarly, each guard method has a built-in message for the And lastly, the guard methods also take care of choosing the more appropriate exception type and message based on the underlying checks being done. For example, using This kind of validation is harder to capture using a single expression, especially if using the syntax proposed here, as there is no way to throw different exceptions using a single throw expression after a coalescing operator. So one would need to go down into a switch statement instead, which brings its own issues such as exhaustiveness checks. My overall point is that any new feature that influences how argument validation is done should seriously consider how the existing guard methods will play with them, even if the answer is to deprecate the guard methods altogether. I would love to have something like I mentioned above though, which would be a combination of both worlds: this.b = b as >= 0 ?? throw; If the compiler can understand the validation pattern being applied, it would pick the correct exception and message to throw automatically. If it cannot understand it, it would signal this with a compilation error. The only caveat with an approach like this of course is how to make it extensible (and if that's even desirable in the first place). This would be a bit less complex (and less flexible) than full-blown contracts, but still much better than what we currently have. |
Given this feature is not specific to argument validation I don't think it makes sense to try to bake that much "magic" into the language specification for this feature. |
I don't disagree (what I said above would likely become a separate proposal), but at the same time, the main arguments presented in favor of this proposal here are tied directly to argument validation. I think they should be considered in conjunction to avoid disrupting existing argument validation patterns and creating clashing standards. For a very long time, argument validation wasn't strongly standardized. With the introduction of the native guard methods, this has now changed, as most common validations are covered there. With the introduction of extension types in .NET9, it will also be possible to introduce extra guard methods using the exact same pattern as the native ones. Using this |
I eprsonally disagree. Using |
This doesn't follow today's if (x is T t and P p) {
// use t and p here
} Today we are not allowed to do if (x is (T and P) tp) // error, waiting for union/intersection types coming in future Allowing |
@hez2010 Good point! It would be better to disable |
Actually, to provide more thoughts, the reasoning behind To refine on this, I think it is sufficient if the pattern is restricted (at the moment) to always result in an identifiable type. Practically, this means that |
I think these type unions and intersections should be prohibited in the proposed |
Not only that but it also allows for faster development of the rest of the design and gets some useful additions out of the door without having to develop (and especially discuss/define) the whole thing completely which as we know can take years sometimes. I particularly like when it is possible to implement something incrementally because of that benefit. It is basically what happened (and is still happening) with pattern matching. |
Is there a discussion open for this item? I feel like most of the comments here would benefit from the formatting and associations that a discussion provides. |
Interesting idea |
@Thaina I don't like it either, but that is how patterns work in general ‒ |
It's using targeted type. If |
My down-vote is because I don't find: this.a = a as not "" ?? "<unspecified>"; ... very readable, and it also feels weird to use the null-coalescing operator in this way. I prefer the alternative syntax. I even wonder if it's technically possible to shorten the 2nd example to: this.b = b when >= 0; ... and automatically throw an Although in this case, |
I like this proposal a lot, these are my considerations:
|
To elaborate: this.b = b when >= 0 else throw new ArgumentOutOfRangeException(nameof(b)); Spoken: Assign this.b = b as >= 0 ?? throw new ArgumentOutOfRangeException(nameof(b)); Spoken: Assign or Spoken: Assign (TBH, I don't even know how to read the latter.) I see the obj as Animal Treat object as an Animal if it's convertible. Not: obj as >= 0 Try to match object against a pattern. |
"Treat |
I would also add that regardless of whether I think that equivalence is important for adoption of a new feature, so since we have expanded the check to see if something I will also point out that using |
What about allowing both VALUE as PATTERN // else null And VALUE as PATTERN else SOMETHING_ELSE (Although it might be harder to parse) |
@AFatNiBBa Not really necessary, I don't think, because |
Unless Such a situation could not happen with |
@viceroypenguin Exactly as @IS4Code stated, I often find myself considering |
LDM discussed this here. Ultimately, we do not want to proceed with this proposal, and are closing it as rejected. |
Extending patterns to "as"
Summary
This proposal seeks to extend pattern matching to the
as
operator in a manner analogous tois
, to allowx as <pattern>
achieve an effect similar tox is <pattern> y ? y : null
, or to explore ways to arrive at a similar outcome.Motivation
A lot of the language is built around pattern matching and option-like
null
propagation or guards (?.
and??
), yet these two systems don't go that well together. For example:Yet in situations where
null
is not the only "exceptional" value, there is no syntax to simplify to:In many situations, there are special values outside of
null
that need to be checked ‒ empty strings or collections, 0, -1, orSomeEnum.None
, or "invalid" objects, but there is no easy way of discarding these values other than using conditions.What if we were instead able to leverage the existing
null
-centered syntax to "coerce" the unwanted values to benull
? Is there any piece of syntax with the meaning "make sure this isX
, otherwise benull
"? Actually, yes ‒as
!Detailed design
This proposed syntax aims to extend
as
, consistently with its original relation tois
, to support (non-assigning) patterns. Roughly speaking:would be more or less equivalent to
This is a natural extension for the original syntax
x as T
, now reinterpreted asx is T y ? y : null
. The operation corresponds to "matchx
toP
, or producenull
if unmatched".Concretely, this would parallel
is
in the following situations:is
syntaxis
semanticsas
syntaxas
semanticsx is T
x as T
T?
, ornull
on failure), possibly extended to allow non-nullable value typeT
x is T { ... }
x as T { ... }
T?
, ensuring the properties, ornull
on failurex is { ... }
x as { ... }
x
, ensuring the properties, ornull
on failurex is T and P
P
checkx as T and P
T?
(or a more specific applicable type fromP
), ensuringP
, ornull
on failurex is P
P
checkx as P
x
(or a more specific applicable type fromP
), ensuringP
, ornull
on failureThe pattern may not create any new variables, since otherwise their use would require the check that the result is
not null
.Examples of semantics
s as { Length: > 10 }
‒ results ins
ifs is { Length: > 10 }
, ornull
(typestring?
).o as >= 0
‒ results in a value of typeint?
as either>= 0
ornull
.b as not 0
‒ results in a value of typebyte?
with the value ofb
if non-0, ornull
.o as not null
‒ just equivalent too
.Examples of usage
The code above could be easily simplified to:
Other possibilities include:
(timeout as not 0) ?? DefaultTimeout
‒ treat 0 as unspecified timeout. Can be read as "timeout
as something non-0, otherwiseDefaultTimeout
".(myEnumValue as not MyEnum.Unspecified) ?? DefaultMyEnumValue
‒ same for anenum
value:numerator / (denominator as not 0)
‒null
if division would cause a black hole.(collection as not [])?[0]
‒ access first element if there is one.(entity as { Exists: true })?.Property
‒ access entity only if it still exists, otherwisenull
.stream.ReadByte() as not -1
‒ useful even alone so that you don't mistakenly use an invalidbyte
.IndexOf(...) as not -1
‒ likewise.obj as not null
‒ not useful much, just results inobj
(but makes the type nullable). Could be a warning.Drawbacks
The new interpretation of
as
might need some taking used to, to communicate the intended meaning.Alternatives
There are possibilities for an alternative syntax, not depending on
as
, with a similar effect:In this case, one would need to be explicit about the introduction of
null
, e.g. by writingb when not >= 0 else null
.Unresolved questions
When
T
is a non-nullable value type,x as T
is currently prohibited. While it possible to achieve a similar effect whilst keeping this restriction, it might make sense to allow producingNullable<T>
instead in such a situation. In any case,i as >= 0
for anint i
would need to produceint?
to be usable, unless an alternative syntax is picked that requires one to specify the "otherwise" value upfront.Design meetings
The text was updated successfully, but these errors were encountered: