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

Support C#-style Deconstruct method based pattern matching #751

Open
5 tasks done
yatli opened this issue Jun 13, 2019 · 41 comments
Open
5 tasks done

Support C#-style Deconstruct method based pattern matching #751

yatli opened this issue Jun 13, 2019 · 41 comments

Comments

@yatli
Copy link

yatli commented Jun 13, 2019

The C#-way of doing quick pattern matching and value extraction is by declaring member functions of name Deconstruct, or static extension methods accordingly. A Deconstruct method has the signature of:

public void Deconstruct(out T1 name1, out T2 name2, ...)

... which actively extracts values from the class instance.
Multiple overloads can be supplied to accommodate different ways of deconstruction.

In F#, we automatically receive pattern matching benefits for DUs and records, but currently the only way to peek into the content of a class instance in a pattern, is to create an active pattern for it. Since active patterns cannot be overloaded, one has to come up with different names for different ways of extraction, which adds extra complexity to the matter.

So I propose that we support this in F#.
A new kind of pattern is then added to classes, which allows a class to be matched against a tuple. When the compiler sees such a pattern, it looks up the class definition and extensions for Deconstruct methods, and align the tuple signature with the [<Out>] T byref parameters -- the [<Out>] and byref part should be removed. Then further matching of the elements in the tuple may proceed. Type inference rules unify the items.

Note, it's not possible practical to use records (anonymous or not) in this case, because there can be multiple Deconstruct overloads.

A quick glance of what it may look like:

type MyEventArgs() =
    inherits EventArgs()
    member val foo: int = 123 with get, set
    member x.Deconstruct([<Out>] foo: _ byref) =
        foo <- x.foo

// later:

myControl.MyEvent.Subscribe(fun (foo: int)  ->
    printfn "extracted foo = %d" foo
) |> ignore

Applications Brainstorming

  • It would be then very cool to also add support in FSharp.Data, so that the provided types can have better pattern matching.
  • A ResizeArray<T> can be then matched as a list!
  • Allow custom deconstruction on DU/records?
  • (Going too far maybe?) if some parameters are not marked as [<Out>] byref, it can be used as input parameter, giving it full active pattern matching capabilities

Pros and Cons

The advantages of making this adjustment to F# are:

  • More natural pattern matching on classes
  • Better interop with C# while not disrupting the F# paradigms

The disadvantages of making this adjustment to F# are:

  • Implicit dependency on the name Deconstruct
  • [<Out>] name: T byref seems taboo in F#
  • (Taken from the C# docs) Multiple Deconstruct methods that have the same number of out parameters or the same number and type of out parameters in a different order can cause confusion
  • ...but C# already did this

Extra information

Estimated cost (XS, S, M, L, XL, XXL): M

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this
@Happypig375
Copy link
Contributor

This is not a breaking change to the F# language design

let f x = match x with 1, 2 -> "1, 2" | y, z -> "y, z"

f is inferred to take a 2-element tuple as the input. How would this play with type inference?

@yatli
Copy link
Author

yatli commented Jun 21, 2019

Ooh, good question!
I think in this case, we should keep the type inference as-is (tuple).
The deconstruction routines should only be active if x is inferred as a non-tuple.
Not that the type checker will blow up (a tuple can now unify with an arbitrary type :/ ), but it does make things a bit more complicated.

Edit: escalating the workload estimation to 'M'.

@dsyme
Copy link
Collaborator

dsyme commented Jul 1, 2019

An alternative may be to have a special pattern matching construct, e.g.

    match x with 
    | Deconstruct(a,b) -> ...

that is known to the compiler and looks for the Deconstruct pattern and emits good code for it. A shorter name than Deconstruct could also be used.

Combining this with a type test may also be important, a possible syntax is this:

    match x with 
    | :? Node1 as (Deconstruct(a,b)) -> ...  // a full nested pattern would be allowed

I do understand why some F# programmers have down-voted this. Fully implicit, type directed deconstruction is really weird for F#, especially given the existence of active patterns in the language and the general lack of type-directed magic rules in pattern matching (a part of the language that is, I think, particularly prone to problems in code comprehension if magic is being applied).

@baronfel
Copy link
Contributor

baronfel commented Jul 1, 2019

@dsyme the pattern suggestion could be implemented right now with SRTP, right? Would that be a useful proof of concept for someone to contribute and show usage examples?

@7sharp9
Copy link
Member

7sharp9 commented Jul 1, 2019

@baronfel Please don't encourage the use of SRTP. @dsyme Spent ages trying to make the syntax as unmemorable as possible for a reason :-)

@baronfel
Copy link
Contributor

baronfel commented Jul 1, 2019

@7sharp9 it turns out that many deconstruct members are implemented as extension members + out parameters, which is problematic from an 'active pattern with SRTP' perspective for a few reasons:

  • extension members aren't visible to SRTP constraints
  • you'd have to initialize a dynamic series of out-parameters and pass them into the deconstruct call to collect them before returning them via the pattern

@yatli
Copy link
Author

yatli commented Jul 2, 2019

Well, using SRTP defeats the purpose of being C# compatible—which is my main motivation.

@Happypig375
Copy link
Contributor

Using SRTP in this case is C# compatible because it is used when matching, i.e. consuming information from C# (or other F# code). You can still define new deconstruct methods as you wish.

@yatli
Copy link
Author

yatli commented Jul 2, 2019

@Happypig375

I think @baronfel has already covered the points, but let me try to rephrase:

First, the deconstruction methods are not necessarily attached to the types, so type constraints are not enough to resolve them.

Second, when there are multiple deconstructs, they cannot be used in one single active pattern because we cannot overload active patterns (unless allocating a list, and let the user pass in the number of parameters for the pattern)

Edit:
Third, which is the most exotic — there can be multiple deconstructs with the same number of params so SRTP would be confused..

@yatli
Copy link
Author

yatli commented Jul 2, 2019

A shorter name than Deconstruct could also be used

Right, that's much better. To avoid clashing with an existing DU case, I'd prefer a new symbol for this new pattern matching construct, for example:

match x with
| :? Node1 as ?( a: T, b: U ) -> ...
// or, combining the two patterns:
| :? Node2(x, y, z) -> ...

... where the second form is similar to the C# switch (x) { case Node1(x, y): ... } syntax.

I prefer this form, because it then fix the type to deconstruct from, which feels safer to write.

@kekyo
Copy link

kekyo commented Nov 8, 2019

Fully implicit, type directed deconstruction is really weird for F#,

If we have to write explicitly type names in deconstruction:

match x with 
| MyEventArgs(a,b) -> ...   // Can write only both F#'s record type and C# style Deconstruct method patterns.

In this case, the compiler will check x arg type is MyEventArgs, and find Deconstruct method (with out parameters) in it. I know it's syntax sugar, pros is naturally decomstruction syntax in F# and improves interops for C#'s.

@abelbraaksma
Copy link
Member

abelbraaksma commented Nov 8, 2019

@kekyo, that won't work, because a DU case with the same name can be in scope (which is not a type name from F# perspective), or an active pattern. Also, because Deconstruct can have overloads, the type of a and b is not set.

And since Deconstruct can be an extension method, what if there's a DU or record with that extension?

One of the greatest strengths of F# is its predictability. To fix all these ambiguities, you'll need unique syntax.

@cartermp
Copy link
Member

cartermp commented Nov 8, 2019

Generally I don't think I'm in favor here. The most common use case in C# is something to the effect of, "turn this class into a tuple of the data that matters" and using it like that. This would work in F#, and perhaps feel natural to an extent, but not having a good or simple way to represent it in match expressions would feel like a dealbreaker.

@kekyo
Copy link

kekyo commented Nov 11, 2019

Understand problem, I vote @yatli 's solution, and:

  • I feel better the extension Deconstruct methods searchable from opened namespace like C#.
  • Thinking about Deconstruct method overloads:
pblic sealed class Node2
{
  public void Deconstrcut(out int a, out int b) { ... }
  public void Deconstrcut(out int a, out int b, out int c) { ... }
}
match x with
| :? Node2(x, y) -> ...
| :? Node2(x, y, z) -> ...

I feel these example matchers are possible, what's problem for ?

@abelbraaksma
Copy link
Member

abelbraaksma commented Nov 11, 2019

@kekyo, it doesn't solve what I mentioned before. The problem is that it still doesn't cover overloads. How can the compiler infer the correct types here?

public sealed class Node2
{
  public void Deconstruct(out int a, out int b) { ... }
  public void Deconstruct(out float a, out float b) { ... }
}
match x with
| :? Node2(x, y) -> ...  // int or float? 
| :? Node2(x, y) -> ...  // int or float? 

@auduchinok
Copy link

auduchinok commented Nov 11, 2019

@abelbraaksma We could require to specify types the same way it's now required when overriding a member with several overloads with the same parameters count.

match x with
| :? Node2(x: int, y: int) -> ...
| :? Node2(x: float, y: float) -> ...

Although it's kind of ugly it seems it happens rare enough in practice, at least for the override overloads case. The typed patterns are there too already so no need to change many things in regards to the parser.

@abelbraaksma
Copy link
Member

abelbraaksma commented Nov 11, 2019

@auduchinok, yes, that could work. But indeed, it isn't too pretty.

While syntactically that covers it, I'm not convinced of using :?, which currently means 'runtime type check'. In the above examples it would be a compile time resolution to an (extension or instance) method, which is not orthogonal I think.

Also, it doesn't cooperate well with completeness checking.

If we go in this direction, I'd suggest the arguments should themselves be allowed to be inline pattern matchable (ie if they consist of DU's), to give it strong and idiomatic F# language support.

Apart from syntax, which could be resolved by taking another operator, allowing this on extension methods as well as instance methods can allow for a simpler alternative to active patterns, that instead can be defined on the type as methods, as opposed to let bindings, which may be desirable in certain scenarios. One would just augment a type in F# with a Deconstruct method.

@auduchinok
Copy link

auduchinok commented Nov 11, 2019

@auduchinok, yes, that could work. But indeed, it isn't too pretty.

And at the same time is consistent with other design choices in the language. :)

While syntactically that covers it, I'm not convinced of using :?, which currently means 'runtime type check'. In the above examples it would be a compile time resolution to an (extension or instance) method, which is not orthogonal I think.

Could we then use the type names instead? It'd be similar to what matching union cases looks like. It'd be a breaking change, though. Not that I'm proposing this syntax, I just find it more or less suitable here.

match x with
| Node2(x: int, y: int) -> ...
| Node2(x: float, y: float) -> ...

@yatli
Copy link
Author

yatli commented Nov 11, 2019

I think in practice, most deconstruct methods are used together with a run-time type check.
If you know the type ahead of time, you can already call a concrete method to extract what you want.

In contrast, when matching against class hierarchy it makes much more sense, and currently there isn't a F# equivalence.

@yatli
Copy link
Author

yatli commented Nov 11, 2019

Also, it doesn't cooperate well with completeness checking

When we tap into OO it's lost already.
This is not intended to be used with ADT.

@TheJayMann
Copy link

I just did some quick checking with C#, and, the example given doesn't work in C#.

public sealed class Node2
{
  public void Deconstruct(out int a, out int b) { ... }
  public void Deconstruct(out float a, out float b) { ... }
}

When attempting to use it, whether by using tuple assignment or by using pattern matching switch expression, even when specifying the types in the pattern matching.

(int a, int b) = x; // The call is ambiguous between Node2.Deconstruct(out int a, out int b) and Node2.Deconstruct(out float a, out float b)

x switch {
    Node2(int a, int b) => ..., // Same error as above
}

Thus the above example already doesn't work for C#, at least when using C# 8.0 on .NET Core 3.0.

@yatli
Copy link
Author

yatli commented Nov 11, 2019

Example from MSDN:

using System;

public class Person
{
   public string FirstName { get; set; }
   public  string MiddleName { get; set; }
   public  string LastName { get; set; }  
   public  string City { get; set; }
   public  string State { get; set; } 
   public DateTime DateOfBirth { get; set; }
   public Decimal AnnualIncome { get; set; }

   public void Deconstruct(out string fname, out string mname, out string lname, out int age)
   {
      fname = FirstName;
      mname = MiddleName;
      lname = LastName;
      age = DateTime.Now.Year - DateOfBirth.Year;

      if (DateTime.Now.DayOfYear - (new DateTime(DateTime.Now.Year, DateOfBirth.Month, DateOfBirth.Day)).DayOfYear < 0)
         age--;
   }

   public void Deconstruct(out string lname, out string fname, out string mname, out decimal income)
   {
      fname = FirstName;
      mname = MiddleName;
      lname = LastName;
      income = AnnualIncome;
   }
}

@yatli
Copy link
Author

yatli commented Nov 11, 2019

@TheJayMann I think your example doesn't work because there's implicit conversion between float and integer -- try string instead?

@TheJayMann
Copy link

The first example I tried was between int and string, with the same results. I only changed it to int and float in my reply to match the discussion.

I'll try the example you gave above to see if I see the same results or not.

@TheJayMann
Copy link

After having just used the Person example listed above, and trying both the switch expression syntax as well as tuple deconstruction syntax, it still has the ambiguity errors.

32:        static void Main() {
33:            var person = new Person() {
34:                FirstName = "James",
35:                MiddleName = "Willy",
36:                LastName = "Smith",
37:                City = "DownTown",
38:                State = "ST",
39:                DateOfBirth = DateTime.Today.AddYears(-35),
40:                AnnualIncome = 95687.39m
41:            };
42:
43:            (string firstName1, string middleName1, string lastName1, int age1) = person;
44:            (string lastName2, string middleName2, string firstName2, decimal income2) = person;
45:
46:            var a = person switch
47:            {
48:                Person(string firstName, string middleName, string lastName, int age) => 19
49:            };
50:   }
1>Program.cs(43,83,43,89): error CS0121: The call is ambiguous between the following methods or properties: 'Person.Deconstruct(out string, out string, out string, out int)' and 'Person.Deconstruct(out string, out string, out string, out decimal)'
1>Program.cs(44,90,44,96): error CS0121: The call is ambiguous between the following methods or properties: 'Person.Deconstruct(out string, out string, out string, out int)' and 'Person.Deconstruct(out string, out string, out string, out decimal)'
1>Program.cs(48,23,48,86): error CS0121: The call is ambiguous between the following methods or properties: 'Person.Deconstruct(out string, out string, out string, out int)' and 'Person.Deconstruct(out string, out string, out string, out decimal)'

@TheJayMann
Copy link

The conclusion that I am coming to, assuming this were to be approved and implemented, is either remove the need for providing the types to determine which same parameter count overload to use, or to suggest to C# language design that they allow to disambiguate Deconstruct calls based on types provided.

@abelbraaksma
Copy link
Member

@TheJayMann, I think that's a bug, certainly considering that the documentation states otherwise. That doesn't remove for us the requirement to be able to deal with overloads, esp assuming c# will fix it.

I think it's already reported and related to, or the same as: dotnet/roslyn#25240.

@abelbraaksma
Copy link
Member

If you know the type ahead of time, you can already call a concrete method to extract what you want.

@yatli That's an interesting consideration. So far I saw this as a compile time feature. But using just a runtime type check is not gonna cover it, we need to know if the required methods are available. To do that at runtime is going to be expensive and unpredictable.

Any reasonable way forward should, imo, be compile time only. The compiler needs to find the appropriate methods, just as if you designed it yourself with SRTP. That way it's generic and the compiler will fail if it cannot find the appropriate Deconstruct method.

Since work is being done to allow SRTP with extension methods, once finished, this may be the proper groundwork to implement this with the necessary syntactic sugar (TBD), assuming the powers that be find the required work in balance with the benefits.

@yatli
Copy link
Author

yatli commented Nov 12, 2019

@abelbraaksma I understand your concern and I fully agree that the deconstruction should be compile-time only. Being best served with run-time type check does not mean it is inherently bound to that.

Plus, there's another use case outside match, in C# one would write:

SomeClass x = new SomeClass();
var (a,b) = x;

... and a :? there will be very strange.

When I look back in the thread I think I get a better understanding of @dsyme 's idea now:

    match x with 
    | :? Node1 as (Deconstruct(a,b)) -> ...  // a full nested pattern would be allowed

Here :? constraints the type (runtime check), and Deconstruct is compile time. It's two different things. If the type is known ahead, invoke Deconstruct directly.
Applying this to the non-match example:

let x = SomeClass()
let Deconstruct(a,b) = x

(looks too much like a function def?)

@abelbraaksma
Copy link
Member

abelbraaksma commented Nov 12, 2019

in C# one would write:

I wonder if in C# it works with a one-tuple. If it doesn't, we could make this feature closer to existing F# language by 'simply' testing for a Deconstruct method after we find that the input is not an F# or standard .NET Tuple<> type.

let Deconstruct(a,b) = x

This wouldn't work for backwards compat reasons. However, you can use pattern syntax already:

// gives warning for missing match, but is allowed
let (Some x) = x   

// valid syntax, but compiler will complain about indeterminate type. 
// I couldn't find a variant of this syntax that the compiler liked, but for F# it is legal syntax
let testThis (:? string as foo) = foo

// not allowed, when-clauses cannot appear on the lh side of let-bindings
let (Some x when x = 1) = x

// allowed, which could theoretically be used with dsyme's proposal as well
let (Some x as foo) = foo, x

// this would be a function that takes tuple
let Deconstruct(a,b) = x

// this would expect a DU that has a DU case named Deconstruct
let (Deconstruct(a,b)) = x

Here :? constraints the type (runtime check), and Deconstruct is compile time

Yes, that is probably how this could work, but that's unfortunate, as it requires potentially relatively expensive type checks. I would prefer the language feature to be compile-time. This could also work with @dsyme's suggestion, which would mean the compiler constraints x to be of Node1 or derived types of it.

However, it has the downside that it cannot be made to work with extension members that have Deconstruct(...) (unless perhaps we'd allow | :? _ as Deconstruct(a, b) -> ..., for any type that supports the Deconstruct member. Come to think of it, it could even be made into a feature that supports any member that has out parameters that has whatever name we put in there, assuming resolution is static).

@TheJayMann
Copy link

As it exists now in C#, Deconstruct must have two or more parameters to be used when deconstructing, but it can have a single parameter or be parameterless for pattern matching. When pattern matching on parameterless Deconstruct method, it does not invoke the parameterless Deconstruct method, but does require it to be present.

@yatli
Copy link
Author

yatli commented Aug 26, 2020

I think it's even possible use the tuple pattern syntax for class objects.

// Suppose ButtonEventArgs is derived from type TEventArgs<string>(obj, string)
let ev = ButtonEventArgs(this, "button pressed")
let (obj, data) = ev // what could go wrong?

match ev with // the type of ev is : ButtonEventArgs
| (obj, data) -> ... // calls ButtonEventArgs.Deconstruct, without runtime type check
| :? TEventArgs<string> as (obj, str) -> ... // runtime type check, then calls TEventArgs<string>.Deconstruct. same as c# case match

Edit: looks like I'm going back to my original proposal, after all :p
For the type deduction problem that happy raised, a solution is to typecheck a tuple always as a tuple, and only deconstruct when we match a class (type annotated) against a tuple.

@RealmPlume
Copy link

These code will work:

   type Deconstructor =
      | Deconstructor
         static member inline Invoke(_: ^a * ^b, x) =
               let mutable a = Unchecked.defaultof<_>
               let mutable b = Unchecked.defaultof<_>
               (^t:(member Deconstruct: ^a outref * ^b outref -> unit) (x, &a, &b))
               (a, b)
         static member inline Invoke(_: ^a * ^b * ^c, x) =
               let mutable a = Unchecked.defaultof<_>
               let mutable b = Unchecked.defaultof<_>
               let mutable c = Unchecked.defaultof<_>
               (^t:(member Deconstruct: ^a outref * ^b outref * ^c outref -> unit) (x, &a, &b, &c))
               (a, b, c)

            
   let inline (|Deconstruct|) x =
         let inline fob(_:^t) r x =
            ((^t or ^x or ^r):(static member Invoke: ^r * ^x -> ^r) (r, x))
         fob Deconstructor Unchecked.defaultof<'r> x

Usage:

   type Ah() =
         member __.Deconstruct(x: _ outref, y: _ outref) =
               x <- 1
               y <- 2
         member __.Deconstruct(x: _ outref, y: _ outref, z: _ outref) =
               x <- "a"
               y <- "b"
               z <- "c"

   let foo8 (x: Ah) =  
         match x with
         | Deconstruct(a, b, c) -> a + b + c  // "abc"

   let foo9 (x: Ah) =  
         match x with
         | Deconstruct(a, b) -> a + b  // 3

but there is tow problem:
int ref are boxed ;
And insteads outref, using pattern matching generates a inerasable tuple.

@yatli
Copy link
Author

yatli commented Dec 21, 2020

@greatim Thanks for the example :) @Happypig375 brought this idea up and I opposed with a few reasons. But now I think of it, some of the reasons don't stand (e.g. multiple overloaded Deconstruct -- because the AP can be inlined), and instead of a keyword-based approach, adding this to the core library would make it less invasive and more discoverable. Sorry Happy, I should've thought more about it..

int ref are boxed ;

I don't get it, outref should be stack-based reference?

And insteads outref, using pattern matching generates a inerasable tuple.

maybe #612 can help.

@RealmPlume
Copy link

@yatli

I don't get it, outref should be stack-based reference?

	// ref int reference = (ref int)(object)(ref item);
	IL_0006: ldloca.s 2
	IL_0008: box int32&
	IL_000d: unbox.any int32&
	IL_0012: stloc.s 4
	// ref int reference2 = (ref int)(object)(ref item2);
	IL_0014: ldloca.s 3
	IL_0016: box int32&
	IL_001b: unbox.any int32&

maybe #612 can help.
No, it doesn't.

@yatli
Copy link
Author

yatli commented Jan 28, 2021

let f (x: int outref) =
  x <- 123
  
[<EntryPoint>]
let Main _ =
    let mutable x = 0
    f &x
    printfn "%d" x
    0

IL_0000:  ldarg.0     
IL_0001:  stloc.0     
IL_0002:  ldc.i4.0    
IL_0003:  stloc.1     
IL_0004:  ldloca.s    01 
IL_0006:  call        Query_iqojmb.f
...
f:
IL_0000:  ldarg.0     
IL_0001:  ldc.i4.s    7B 
IL_0003:  stobj       System.Int32
IL_0008:  ret         

Works as I expect.

@RealmPlume
Copy link

RealmPlume commented Jan 28, 2021

let f (x: int outref) =
  x <- 123
  
[<EntryPoint>]
let Main _ =
    let mutable x = 0
    f &x
    printfn "%d" x
    0

IL_0000:  ldarg.0     
IL_0001:  stloc.0     
IL_0002:  ldc.i4.0    
IL_0003:  stloc.1     
IL_0004:  ldloca.s    01 
IL_0006:  call        Query_iqojmb.f
...
f:
IL_0000:  ldarg.0     
IL_0001:  ldc.i4.s    7B 
IL_0003:  stobj       System.Int32
IL_0008:  ret         

Works as I expect.

I just found that the code above will produce the box, it makes no sense for you to test different codes.
I guess the 'Box' will be generated when FSharpFunc is generated in the middle.

@yatli
Copy link
Author

yatli commented Jan 28, 2021

Understood.
Sounds very much like a double-dispatch pattern in C# with interfaces for specialization.. When you know T is U but you'll have to box a T and unbox to get U. It's implementation detail, of course :)

@dsyme
Copy link
Collaborator

dsyme commented Apr 12, 2023

I finally labelled this as approved-in-principle. I agree there should be some way of consuming Deconstruct methods automatically.

@brianrourkeboll
Copy link

I don't think anyone has mentioned this in this thread yet...

For what it's worth, you can already do something like this just by calling Deconstruct, since F# lets you match on byref params as though they were tuple items:

type PretendThisIsACSharpRecord =
    { A : int
      B : string
      C : decimal }

    member this.Deconstruct (a : int outref, b : string outref, c : decimal outref) =
        a <- this.A
        b <- this.B
        c <- this.C

let r = { A = 1; B = "2"; C = 3.0m }

// Bind to a tuple.
let a, b, c = r.Deconstruct ()

// Match as a tuple.
match r.Deconstruct () with
| a, b, c -> ()

It's not the same as the compiler calling it implicitly, of course, but still.

@baronfel
Copy link
Contributor

@brianrourkeboll that's true in the presence of anambiguous overloads, but we will likely need compiler support for handling overloaded Deconstructs:

type PretendThisIsACSharpRecord =
    { A : int
      B : string
      C : decimal }

    member this.Deconstruct (a : int outref, b : string outref, c : decimal outref) =
        a <- this.A
        b <- this.B
        c <- this.C
    
    member this.Deconstruct(a: int outref, b: string outref) =
        a <- this.A
        b <- this.B
    

let r = { A = 1; B = "2"; C = 3.0m }

// Bind to a tuple.
// works ok, inference can find a two-element tuple and go from there. also works if there are multiple two-element tuple
// overloads as long as you annotate enough parameters to find one
let a, b = r.Deconstruct () 

// Match as a tuple.
match r.Deconstruct () with // FS0041 triggered here because we can't guide inference?
| a, (b: string) -> ()

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

No branches or pull requests