csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

Champion "User-defined Positional Patterns"

Open gafter opened this issue 8 years ago • 58 comments

  • [ ] Proposal added
  • [ ] Discussed in LDM
  • [ ] Decision in LDM
  • [ ] Finalized (done, rejected, inactive)
  • [ ] Spec'ed

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 a bool-returning static Deconstruct method.

gafter avatar Oct 25 '17 19:10 gafter

This will be great for struct-based Option<T> and friends.

orthoxerox avatar Oct 25 '17 19:10 orthoxerox

to be clear, this is not about "extension patterns", right? or this would be the only way to have a user-defined pattern?

alrz avatar Oct 25 '17 20:10 alrz

to be clear, this is not about "extension patterns", right?

I don't care what they are called.

or this would be the only way to have a user-defined pattern?

This proposal is not meant to exclude the possibility that there would be other things done to the language too.

gafter avatar Oct 25 '17 22:10 gafter

@alrz Since the proposal shows additional is deconstructors being declared in a separate static class, the patterns look extension enough to me.

orthoxerox avatar Oct 26 '17 06:10 orthoxerox

@orthoxerox,

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 avatar Oct 26 '17 07:10 DavidArno

@DavidArno it looks like only the input parameters are really missing from the proposal. You can write str is Integer() with a positional pattern, or even str is Integer(var i).

orthoxerox avatar Oct 26 '17 07:10 orthoxerox

@orthoxerox,

Input parameters and an active pattern name, eg:

public static bool ValidPostcodePattern(this string str) { ...

if (postcode is ValidPostcode) ...

DavidArno avatar Oct 26 '17 07:10 DavidArno

@DavidArno

public static class ValidPostcode
{
    public static bool operator is(string str) => ...
}

if (postcode is ValidPostcode()) ...

orthoxerox avatar Oct 26 '17 08:10 orthoxerox

@orthoxerox,

Oh! That's a really neat solution. In that case, you're right: input parameters are indeed the only part that's missing.

DavidArno avatar Oct 26 '17 08:10 DavidArno

Unfortunately, I can't find where I originally suggested it, but as a reminder to @gafter, input parameters could be handled by using in. A highly contrived example:

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 avatar Oct 26 '17 08:10 DavidArno

@DavidArno If anything, you should be using in at the call-site - because that's where it's ambigious, although I believe we don't need it if we define pattern syntax as a subset of expression syntax (https://github.com/dotnet/csharplang/issues/277).


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 Polar class has no other usages besides being a mere container for is operator . ~~So I think in the presence of active patterns, we don't need bool-returning Deconstruct methods.~~ Records can use a void-returning Deconstruct method to enable positional patterns because they wouldn't be fallible.

EDIT: there is actually a use case for bool-returning Deconstruct methods: when you want to define a "fallible conversion" between types.

alrz avatar Oct 26 '17 10:10 alrz

I think this will be useful but I feel it not good to have is keyword that should be a fast type check mechanism do any work like implicit conversion operator

Thaina avatar Oct 27 '17 08:10 Thaina

@Thaina,

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): ...

DavidArno avatar Oct 27 '17 11:10 DavidArno

Are is operators still in play? I thought that idea went out when the Deconstruct convention was added to the language:

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 Polar example, especially since the example implies that the type must be static. That seems to preclude conversion patterns between types, e.g. if (x is string(var s)) { ... }

The ability to support input expressions would be really nice as well to allow for utility patterns like if (x is between(1, 5)) but I understand the parser ambiguities that creates.

There's also the question of where ADTs stand which probably builds off of both records and "user-defined positional patterns".

HaloFour avatar Oct 27 '17 14:10 HaloFour

@DavidArno Well, I don't know. Maybe a new keyword?

@HaloFour It not about Deconstruct function but the fact it utilize is and case keyword that normally be a fast checking to add a conversion that could be complex and might expensive. That destroy the assumption about usage of is. I wish we could make something more difference

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 obj is var x keyword I think we would x being the same reference instance of obj but with the deconstruct it could return difference object?

Thaina avatar Oct 28 '17 04:10 Thaina

@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 obj is MyType m will never be a deconstruction and will always be no more than a type test (assigning the value to a variable of the given type). We have not considered hijacking that syntax to give it a new meaning.

gafter avatar Oct 29 '17 01:10 gafter

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) = ...

Richiban avatar Nov 06 '17 11:11 Richiban

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 is operators consider conversions when they are matching against different types.

Note: T? accounts for an incomplete pattern in F# e.g. let (A|_) = ...

--

@Richiban

First of all, I can't imagine why on earth you would want to write something like this:

public static class ValidPostcode
{
   public static bool operator is(string str) => ...
}

if (postcode is ValidPostcode()) ...

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 TryParse method. e.g. public static bool TryParse(string, out Postcode). You might just want to use in a switch case, though I don't think it worth the overhead (see my comment above).

how this mechanism can be used to implement the full active pattern

Active patterns in F# use Core.Choice<..> ADTs under the hood, so no new type is emitted for the output. If is operators consider conversion operators (like the example above) I imagine you can define a discriminated union and declare a conversion operator to that type. However, your specific example needs the pattern to be "parametrized" (check out F# doc on active patterns to see what I mean). In that case, there will be some ambiguities when we're parsing expressions vs. patterns ( https://github.com/dotnet/csharplang/issues/277).

alrz avatar Nov 09 '17 14:11 alrz

@alrz

Can't call a TryParse method in the middle of a recursive pattern. Should, you could use a variable pattern and a guard in that case but there's something nice about keeping everything in one place.

HaloFour avatar Nov 09 '17 15:11 HaloFour

@HaloFour

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.

alrz avatar Nov 09 '17 15:11 alrz

@alrz

That I agree with. https://github.com/dotnet/roslyn/issues/9005

Especially since in the current proposal that type is static. It makes little sense that anything could be that type since that type isn't actually a value.

HaloFour avatar Nov 09 '17 15:11 HaloFour

@alrz

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...

Polar is not defined as a record or anything like it. It is a static class; there is no "conversion" to conflate with deconstruction.

gafter avatar Nov 09 '17 17:11 gafter

@gafter

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 avatar Nov 09 '17 17:11 alrz

@alrz I expect such a static class would also contain a pseudo-constructor - for example, in the type Polar to create an actual object of type Cartesian, and other utilities for working with a point conceptually as a Polar, even though it is physically a Cartesian.

gafter avatar Nov 09 '17 18:11 gafter

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 Between pattern. (of course, in the absence of range literals).

static class Between {
  public static bool Deconstruct(this int i, int from, int to) { ... }
}

There is a couple of things to point out here:

  • Between has no meaning as a "type" or even a utility static type, because it's a pattern.
  • I can't imagine that this class could contain any other meaningful members in it because it's a pattern.
  • You don't Deconstruct a Between, you match a value against the Between pattern which is not clearly expressed here.
  • And, of course, we can't accept non-out parameters here while it's crucial for custom patterns to be useful.

I am still trying to think of an actual use for a bool-returning Deconstruct which does not suffer from these issues.

alrz avatar Nov 09 '17 18:11 alrz

@alrz,

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 Deconstruct<T> here, which I don't think is (yet) part of the scope of the feature.

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):
        ...
}

DavidArno avatar Nov 10 '17 08:11 DavidArno

@alrz,

There is a couple of things to point out here:

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 x is T y to support simpler syntax for eg unions. It's not the whole story, but it's a big step in the right direction. And for what it offers, using types to provide those patterns makes complete sense to me.

DavidArno avatar Nov 10 '17 08:11 DavidArno

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 Deconstruct method and there is absolutely no correspondence between these types/patterns, like

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 Some and None cover all the possible cases - there will be a false negative/positive exhaustiveness warnings depending on whether or not we want match to produce such warnings and you might still need to have a catch-all case.

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.

alrz avatar Nov 18 '17 13:11 alrz

I wonder if this would allow what's described in this question to be done.

Logerfo avatar Jan 05 '18 16:01 Logerfo

@Logerfo

It wouldn't allow the auto-unboxing directly into a variable as requested in that question. At best he could come up with a helper pattern like follows:

public struct Custom<T> { ... }

public static class Some {
    public static bool Deconstruct<T>(this Custom<T> custom, out T value) {
        if (custom.HasValue) {
            value = custom.Value;
            return true;
        }
        value = default;
        return false;
    }
}

Which could possibly be used with the following:

Custom<int> custom = ...;
if (custom is Some(var value)) {
    // use value here
}

Not quite the most elegant solution and I think it illustrates the same issue that we've been discussing with requiring the pattern to be its own static class. But without more context regarding this use case it's hard to know exactly what would fit better. Trying to recreate Nullable<T> is probably not something that the team cares to solve, but perhaps trying to create an Option<T> ADT is on their radar.

As for Nullable<T>, that is a special type both to the compiler and to the CLR and there isn't any way to achieve the same functionality with your own types. For example, when you box a Nullable<T> an actual null is pushed onto the stack, not a boxed copy of the Nullable<T> that happens to have the IsNull flag set to true. The same is true when unboxing. The CLR does that automagickally.

HaloFour avatar Jan 05 '18 16:01 HaloFour