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
yatli opened this issue Jun 13, 2019 · 10 comments

Comments

Projects
None yet
5 participants
@yatli
Copy link

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

This comment has been minimized.

Copy link
Contributor

commented Jun 21, 2019

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link
Collaborator

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

This comment has been minimized.

Copy link
Collaborator

commented Jul 1, 2019

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

@7sharp9

This comment has been minimized.

Copy link
Member

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

This comment has been minimized.

Copy link
Collaborator

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

This comment has been minimized.

Copy link
Author

commented Jul 2, 2019

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

@Happypig375

This comment has been minimized.

Copy link
Contributor

commented Jul 2, 2019

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

This comment has been minimized.

Copy link
Author

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

This comment has been minimized.

Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.