-
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
Champion "User-defined Positional Patterns" #1047
Comments
This will be great for struct-based |
to be clear, this is not about "extension patterns", right? or this would be the only way to have a user-defined pattern? |
I don't care what they are called.
This proposal is not meant to exclude the possibility that there would be other things done to the language too. |
@alrz Since the proposal shows additional |
Not quite extension enough for me. Active patterns (which I think is what I think @alrz is referring to with "extension patterns") would complete the set. But this feature would certainly be a huge step toward that goal. |
@DavidArno it looks like only the input parameters are really missing from the proposal. You can write |
Input parameters and an active pattern name, eg: public static bool ValidPostcodePattern(this string str) { ...
if (postcode is ValidPostcode) ... |
public static class ValidPostcode
{
public static bool operator is(string str) => ...
}
if (postcode is ValidPostcode()) ... |
Oh! That's a really neat solution. In that case, you're right: input parameters are indeed the only part that's missing. |
Unfortunately, I can't find where I originally suggested it, but as a reminder to @gafter, input parameters could be handled by using public static class ValidAndFormattedPostcode
{
public static bool operator is(string str, in countryCode, out formattedPostcode) => ...
}
var postcode = "aa11Aa";
if (postcode is ValidAndFormattedPostcode("gb", var formattedPostcode))
{
// formattedPostcode assigned "AA1 1AA" |
@DavidArno If anything, you should be using Anyways, since this is about plugging fallible positional patterns to types, for each case we'll have to declare a full-blown class, static class Between { public static bool Deconstruct(...) {} }
static class Integer { public static bool Deconstruct(...) {} }
static class MyPattern { public static bool Deconstruct(...) {} } while it could be just one type, static class Patterns {
[PatternExtension] public static bool Between(...) {}
[PatternExtension] public static bool Integer(...) {}
[PatternExtension] public static bool MyPattern(...) {}
} In fact, the example given in the proposal doesn't need to be a type either, that is, the EDIT: there is actually a use case for bool-returning Deconstruct methods: when you want to define a "fallible conversion" between types. |
I think this will be useful but I feel it not good to have |
What keyword would you use? Also bear in mind that we talking patterns here, so, if for example the following were valid: if (postcode is ValidAndFormattedPostcode("gb", var formattedPostcode)) ... Then it would work for the following situations too: let ValidAndFormattedPostcode("gb", var formattedPostcode) = postcode else throw...
switch (postcode)
{
case ValidAndFormattedPostcode("gb", var formattedPostcode): ... |
Are public static class Polar
{
// public static bool operator is(Cartesian c, out double R, out double Theta)
public static bool Deconstruct(this Cartesian c, out double R, out double Theta)
{
R = Math.Sqrt(c.X*c.X + c.Y*c.Y);
Theta = Math.Atan2(c.Y, c.X);
return c.X != 0 || c.Y != 0;
}
} I don't really care that much as to what syntax is eventually adopted, but I do hope that it would allow creating named patterns without having to define (or modify) types specifically for that purpose as was the case with the above The ability to support input expressions would be really nice as well to allow for utility patterns like There's also the question of where ADTs stand which probably builds off of both records and "user-defined positional patterns". |
@DavidArno Well, I don't know. Maybe a new keyword? @HaloFour It not about Maybe if(obj is MyType m) // obj is MyType
{
}
else if(obj is ~MyType m) // obj is deconstructable to MyType
{
} I too don't care much about syntax. I just don't like it that it is the same as already used syntax Another problem arise is, with |
@Thaina http://gafter.blogspot.com/2017/06/making-new-language-features-stand-out.html People will not be stuck with C# 7 and earlier in their brains forever. They will learn the new features and integrate them into their understanding of the language. In any case, something like |
I would like to make sure that we are considering whether this feature would enable / prohibit F#-style active patterns in the future. @orthoxerox 's solution o public static class ValidPostcode
{
public static bool operator is(string str) => ...
}
if (postcode is ValidPostcode()) ... Works well as a C# equivalent of this partial active pattern in F#: let (|ValidPostcode|_|) s = ... Where a pattern can succeed or fail. I can't, however, see how this mechanism can be used to implement the full active pattern, such as this: let (|GreaterThan|Equal|LessThan|) (x, y) = ... |
From the proposal, so this would be possible? partial class BlockSyntax {
public static bool Deconstruct(SyntaxNode node) => node.IsKind(SyntaxKind.Block);
}
if (node is BlockSyntax()) {}
if (BlockSyntax.Deconstruct(node)) {} However, it's not specified that what happens if you add a pattern variable there, e.g. if (node is BlockSyntax() block) {} -- Also I noticed that the example given in the proposal conflates "polar deconstruction" and "cartesian to polar conversion". Assuming that the two are defined as records, deconstruction is compiler-generated as void-returning Deconstruct methods, so the only thing that is left is a fallible conversion, e.g. struct Cartesian(int X, int Y) {
// fallible conversion
public static explicit operator Polar?(Cartesian c) => c.X == 0 || c.Y == 0 ? null
: new Polar(Math.Sqrt(c.X*c.X + c.Y*c.Y), Math.Atan2(c.Y, c.X));
}
struct Polar(double R, double Theta) {
// compiler-generated
// public void Deconstruct(out double R, out double Theta) => (R, Theta) = (this.R, this.Theta);
}
if (cartesian is Polar(var R, _)) Not sure if Note: -- First of all, I can't imagine why on earth you would want to write something like this:
Sure, in F# it might makes sense to define an active pattern for that, but the idiomatic way of doing that in C# is a
Active patterns in F# use |
Can't call a |
I don't disagree with the use case. but I believe what makes it possible - according to the current proposal- which is defining a whole type with a single member, is just too much and unnecessarily convolutes the code. |
That I agree with. dotnet/roslyn#9005 Especially since in the current proposal that type is |
Polar is not defined as a record or anything like it. It is a static class; there is no "conversion" to conflate with deconstruction. |
I understand that it's just an example, but that itself raises the issue. If it is solely to represent a custom pattern (and nothing else), I'd argue that a full-blown type for it would be overkill. If it's not, e.g. Polar is itself a meaningful type or record, it already has a compiler-provided Deconstruct method (or a manually written one, for that matter), and you just need to add the conversion logic. |
@alrz I expect such a static class would also contain a pseudo-constructor - for example, in the type |
IMO this example is somehow biased on how you would want to handle Polar/Cartesian systems. You have a Cartesian class and only provide Polar utilities in a static class. One might define both as classes and define conversions in either directions. Let's take a more general example, like a static class Between {
public static bool Deconstruct(this int i, int from, int to) { ... }
} There is a couple of things to point out here:
I am still trying to think of an actual use for a bool-returning Deconstruct which does not suffer from these issues. |
The obvious one for me is with option/maybe types: struct Option<T> { ... }
static class Some
{
public static bool Deconstruct<T>(this Option<T> option, out T value)
{
if (option.HasValue)
{
value = option.Value;
return true;
}
value = default;
return false;
}
}
static class None
{
public static bool Deconstruct<T>(this Option<T> option) => !option.HasValue;
}
var optionalValue = F();
if (optionalValue is Some(var value))
{
// use value here Caveat: I've had to introduce This is the only use case I can think of, but it's an incredibly compelling use case for me. Not just because it offers a neat solution to using patterns to test and extract the value, but because it simplifies union types in general. For example (totally making up syntax as I go): struct ColorSpaces is RGB(int red, int green, int blue) |
HSV(int hue, int saturation, int lightness) {} Could be lowered to: struct ColorSpaces
{
public (int red, int green, int blue) Rgb { get; }
public (int hue, int saturation, int lightness) Hsv { get; }
public bool IsHsv { get; } // make the first type in the union the default;
public static RGB(int red, int green, int blue)
{
Rgb = (red, green, blue);
IsHsv = false;
}
public static HSV(int hue, int saturation, int lightness)
{
Hsv = (hue, saturation, lightness);
IsHsv = true;
}
...
}
static class RGB
{
public static bool Deconstruct(this ColorSpaces colorSpaces,
out int red,
out int green,
out int blue)
{
if (!colorSpaces.IsHsv)
{
(red, green, blue) = colorSpaces.Rgb;
return true;
}
(red, green, blue) = default;
return false;
}
}
public static class HSV { ...
var colorSpace = GetAColorSpace();
switch (colorSpace)
{
case RGB(var red, _, _)):
Console.WriteLine($"RGB. Red is {red}";
break;
case HSV(var h, var s, var v):
...
} |
I completely agree that the proposal here does not work well for active patterns. But as @gafter said two weeks ago, this proposal does not exclude the idea of adding active patterns later. What this proposal offers is a much needed way of expanding upon the currently very limited scope if |
The smelly part is this: static class Some {}
static class None {}
static class RGB {}
static class HSV {} I think that's too much boilerplate. Those types have no meaning beyond being a sole container for the static class OptionExtensions {
public static bool Some<T>(this Option<T> @this, out T value) {}
public static bool None<T>(this Option<T> @this) {}
} Which still has a downside: the compiler does not know if Ideally, it should be defined as an ADT so that the compiler have complete info re exhaustiveness. In my opinion, your example is not compelling at all. I won't suggest we try to encode such structures with "user-defined positional patterns" or "custom patterns" etc. For comparison, discriminated unions and active patterns in F# are totally different features. Actually, F# active patterns are built on top of DUs, not the other way around. |
Copying my comment over this relevant issue, (for an approach to user-defined positional patterns).
In a recursive pattern like that, when the type lookup for Each argument can be a:
This is mostly the same as how we handle |
Both links in the OP are broken, and I can't find the section that the first link's anchor is supposed to be pointing to in this file. |
@yaakov-h What we used to have in those documents no longer makes sense now that positional patterns are defined by a We would probably do this for a type public class Cartesian
{
public int X { get; }
public int Y { get; }
public void Deconstruct(out int X, out int Y) => (X = this.X, Y = this.Y);
} by permitting the definition of a user-defined pattern public static class Polar
{
public static bool Deconstruct(Cartesian value, out double R, out double Theta) => ...
} Which would allow void M(Cartesian cart)
{
if (cart is Polar(var r, var theta)) ...
}
} |
So has none of the above conversation been taken into consideration at all or is it just a matter of these design discussions not happening yet? I seriously hope that these user-defined positional patterns aren't going to be stuck as static methods of static classes both requiring way more boilerplate than seems reasonable and also makes those static classes useless as types themselves. |
Not sure if #1047 (comment) answers your question. |
Patterns are adjectives, so perhaps they should be properties? If they have to be types I hope that those types don't have to be I'm sure that the team is going to dig into this further something after 8.0 ships, I hope to see some design notes around it with many more use cases than "Cartesian"/"Polar" which would represent how people can expect to work with them. |
One entry point I'd like to see is an "Extract Pattern" code action that acts just like "Extract Method" on patterns - to extract out reusable/long patterns into their own. if (x is { P : <long pattern> }) from there, you can expect to be able to parameterize the resulting pattern and so on. I believe this helps to understand the potential of such feature. If anything, it demands the resulting code to be concise and flexible, because we're going to see a lot of these and perhaps new APIs will emerge around it. |
I believe that the operator |
I'm with the above comment. The addition of |
I believe your concerns are misplaced here. This proposal won't provide a way of changing what |
Ah, I assumed it was more like other operators. We don't call an overloaded If that is the case, though, carry on and don't mind me. |
Will this not be subsumed by #5497? |
Possibly, but probably not. That won't cover paramerized patterns, like different string comparison operators. |
This championed proposal is for that part of the pattern-matching specification that permits a user-defined positional pattern. As specified, it is a user-defined
operator is
. Given the current shape of the language post tuples, an alternative might be abool
-returning staticDeconstruct
method.The text was updated successfully, but these errors were encountered: