fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
345 stars 21 forks source link

Object/property/field patterns #968

Open Happypig375 opened 3 years ago

Happypig375 commented 3 years ago

Trimmed down suggestion written by @dsyme: I propose we allow two additions - property/field patterns, and "object patterns".

Proposed syntax for property/field patterns:

match x with 
| _.Property1 pattern -> ....

The pattern may be parenthesized, e.g.

match x with 
| _.Elements([a;b;c]) -> ....

Property patterns can use nesting, so this is allowed:

match x with 
| _.Property1.Property2 pat -> ...

Property patterns can resolve to fields - as supported in #506

Boolean property patterns may elide a true pattern. (Will consider whether this also applies to other pattern elements)

match x with 
| _.IsCold -> ...

Notes:

  1. Property patterns depend on #506

  2. Property patterns can not resolve to methods. Use an active pattern, it's what they're there for.

  3. Property patterns can't resolve to indexers. Just use an active pattern, it's what they're there for. So not this:

    match x with 
    | _.[3] pat -> ...

Proposed syntax for object patterns:

match x with 
| (Property1=pattern, Property2=pattern, Field3=pattern) -> ....

The type name can be given explicitly (if it doesn't already exist as a pattern discriminator):

match x with 
| SomeObjectType(Property1=pattern, Property2=pattern, Field3=pattern) -> ....

Existing type-test patterns would be extended to allow object patterns:

let f3 (inp: obj) =
    match inp with 
    | :? SubType1(X=3) -> 1
    | :? SubType2(X=3, Y=4)-> 1
    | _ -> 2

Existing type-test patterns would also be extended to non-object patterns such as unions and records:

type U = U of int * int
type R = { A: int; B: int }

let f3 (inp: obj) =
    match inp with 
    | :? SubType1(X=3) -> 1
    | :? { A = 1; B = 2 }-> 2
    | :? U(a,b) -> 3
    | _ -> 4

Notes

  1. Object patterns can't use nesting of property names, so not

    match x with 
    | (Property1.Property2=pat, Property3=pat) -> ...

    This is because the corresponding object creation syntax doesn't support nesting

  2. Object patterns can't use indexers. This is because the corresponding object creation syntax doesn't support nesting

  3. Object patterns can be used on records, despite the lack of a corresponding syntax for record construction

    type R = { X: int; Y: int }
    match x with 
    | (X=pat, Y=pat) -> ...
  4. Where the above don't fit, use an active pattern. It's what they're there for.

Discussion and further suggestions below


Original suggestion:

Generalized collection patterns

Currently, list and array patterns can only match based on length, or in list's case, unconsing the first element and the rest of the list. We don't have patterns to match based on starting and ending elements, or patterns to match arbitrary types with indexes.

I propose we allow

  1. Slice patterns function [| firstElem; _; thridElem; ..; secondToLastElem; _|] -> f firstElem thridElem secondToLastElem |> Some | _ -> None The two dots indicate skipping zero or more elements. We can use as to get the sliced area: let unsnoc = function [.. as s; lastElem] -> s, lastElem Only one slice is allowed per collection for now.
  2. Seq patterns function seq { _; secondElem; .. } -> Some secondElem | _ -> None We should be able to match on arbitrary sequences. If they are of type IReadOnlyList<T>, IList<T> or IList, we can even access by index. If there are multiple matches, we should cache the elements to a ResizeArray and access by index. The empty case should be seq [], to unify with how we construct seqs. Sometimes we just want to match by index. Moreover, these collections may be hidden inside properties and fields.

The existing way of approaching this problem in F# is performing length checks and accessing indexes, or in the special cases, using library head and last functions.

Pros and Cons

The advantages of making this adjustment to F# are

  1. More uses for collection patterns
  2. Less code to write
  3. Being able to apply patterns to even more types

The disadvantage of making this adjustment to F# is the overlap with active patterns. However, active patterns completely disable completeness checks so an unnecessary catch-all pattern must be used every time.

Extra information

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

Related suggestions: Champion "list pattern" for C# List patterns proposal for C# C# 8 recursive pattern matching F# pattern matching: parity with C# 9

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

dsyme commented 3 years ago

The existing way of approaching this problem in F# is performing length checks and accessing indexes, or in the special cases, using library head and last functions.

The existing way to do nearly all of these is one much more general feature - active patterns. Please rewrite the suggestion taking this into account.

Now, it is likely we will make some extensions to pattern matching - I favour nested pattern matching on properties in particular. However in F# 1.0 we made a decision not to randomly extend the pattern matching algebra with more and more "cute" features, and instead embrace a single unified feature for extending pattern matching.

Continually extending pattern matching with new "features" is frankly a trap in programming language design that stems right back to the 1970s and 80s when pattern matching was first introduced. You can see remanants of this thinking in OCaml, Haskell and so on. I believe the C# team are falling directly in this trap - and frankly they will regret it over time. Why? Because

  1. these features are obscure in syntax and semantics, and some will come to be loathed by C# teams

  2. the performance profile of these features is hard to understand, e.g. adding something as subtle as sequence patterns is frankly nuts - what happens on infinite sequences? what happens on re-matching of sequences?

  3. these features often don't work well under changes to your program - e.g. change a property to a method (because an extra parameter is needed) and suddenly you have to change every bit of code that used pattern matching against that property - which can literally mean removing pattern matching from thousands of lines of code.

As explained in our 2006 paper, active patterns offer a single, unified point of extension for pattern matching capabilities, an observation based on 30+ years of programming language history. There are some small cases where they don't work particularly well, but in balance they should always be considered first.

Happypig375 commented 3 years ago

To be clear, that seq pattern was not C#'s design. The linked proposal only works with any type that:

  1. Has an accessible property getter that returns an int and has the name Length or Count
  2. Has an accessible indexer with a single int parameter
  3. Has an accessible Slice method that takes two int parameters (for slice subpatterns)

This rule includes T[], string, Span<T>, ImmutableArray<T> and more.

That said, during the LDM considering list patterns, they considered extending this to all foreach-able types, noting that if users need this pattern, they will code it by hand anyway, and the compiler can make it a bugfree process.

Since F# does not have a generalized collection syntax, I decided to make seq be the pattern designator.

the performance profile of these features is hard to understand, e.g. adding something as subtle as sequence patterns is frankly nuts - what happens on infinite sequences? what happens on re-matching of sequences?

Infinite sequences already do not work well with aggregating functions in the Seq module. Try Seq.initInfinite id |> Seq.max for example. For re-matching, obviously, some caching will be needed, like with a Seq.cache.

change a property to a method (because an extra parameter is needed)

We should have properties with parameters? Just kidding. Property to method conversions should be rare because properties should not depend on an external state. Properties are expected to be side-effect free while methods can have side-effects. Any conversion like this should expect large breaks.

these features are obscure in syntax and semantics, and some will come to be loathed by C# teams

This is really the point to emphasize. While syntax and semantics can be learned, we all like extensions of existing syntax. New active patterns should have less implementation complexity than adding new syntax. I imagine it would look like

let (|Start3|_|) x =
    if Array.length x < 3 then None
    else (x.[0], x.[1], x.[2]) |> Some
let (|Index|_|) index x =
    if Array.length x < index then None
    else Array.item index x |> Some
match [|1;2;3;4;5|] with
| Start3 (_, second, _) & Index 3 4 -> Some second
| _ -> None
|> printfn "%A" // Some 2

Ideally, this should be inline and be generic over

  1. Has an accessible property getter that returns an int and has the name Length or Count
  2. Has an accessible indexer with a single int parameter
  3. Has an accessible Slice method that takes two int parameters (for slice subpatterns)

But that would be up to the implementation.

kevmal commented 3 years ago

Field matching exists but only within F#:

type Bleh = 
    val Field1 : int 
    val Field2 : string
    new(a,b) = {Field1 = a; Field2 = b}

type Bleh2 = 
    inherit Bleh
    val Field3 : string
    new(a,b,c) = {inherit Bleh(a,b); Field3 = c}

let b1 = Bleh(1,"1")
let b2 = Bleh2(2,"2","c")

match b1 with 
| {Field1 = f1; Field2 = f2} when string f1 = f2 -> "="
| {Field1 = 1} -> "1"
| _ -> ""

match b2 with 
| {Field3 = "c"} -> "c"
// | {Field1 = 1} -> "1" // <--- would need to match on Bleh type for this
| _ -> ""

If Bleh was in a non F# asm this would not work, it would be nice if that wasn't the case. In particular I had a use-case where I was hoping (assumed) this would work on type provider provided types.

Grauenwolf commented 3 years ago

change a property to a method (because an extra parameter is needed) and suddenly you have to change every bit of code that used pattern matching against that property - which can literally mean removing pattern matching from thousands of lines of code.

So what? If you change a property to a function with a new parameter that's used in thousands of different places, that's a huge breaking change regardless if you used pattern matching or not.

It's like saying we shouldn't paint the walls blue because if the building burns down we will have to repaint.

Grauenwolf commented 3 years ago

these features are obscure in syntax and semantics, and some will come to be loathed by C# teams

That's not really an argument. You're basically saying "F# shouldn't do X because C# did X and while they like it now they might not like it in the future".

An argument should say what X is and why you think it will not be liked in the future.

laenas commented 3 years ago

these features are obscure in syntax and semantics, and some will come to be loathed by C# teams

That's not really an argument. You're basically saying "F# shouldn't do X because C# did X and while they like it now they might not like it in the future".

An argument should say what X is and why you think it will not be liked in the future.

The argument is, clear as day, about endless extensions to pattern matching. It becomes an eternal race to keep coming up with new cool syntax, but owing to the nature of these languages, you also can't deprecate old syntax, and so now every developer has to learn every syntax, even the ones they don't want to use, just to be able to process code. Consider polymorphic pattern matching in C# on an input arg of type object. Off the top of my head, the ways to presently handle 'figure out which of these known possible types it is' include:

Language cruft is real.

dsyme commented 3 years ago

So what? If you change a property to a function with a new parameter that's used in thousands of different places, that's a huge breaking change regardless if you used pattern matching or not.

No, it's different. In C#, if you change a property to a method (say one taking no arguments - .Length to .GetLength()), and that property was used as part of large nested pattern matches, then you literally have to remove all the pattern matching and rewrite the whole thing in statements and expressions. There is simply no escape route AFAIK (because C# lacks the general feature akin to active patterns).

In contrast, changing a property to a method taking no arguments is a routine change at all callsites for expressions/statements.

dsyme commented 3 years ago

Property to method conversions should be rare because properties

They are common enough, e.g. when moving a property .Foo to a method .GetFoo() because Foo is expensive to compute.

But of course this isn't a huge problem for F# as an active pattern can be written for the property/method and localised replacements made to pattern matching syntax

cartermp commented 3 years ago

To proceed with any of this in any shape I'd like to see some code samples where the proposed patterns actually do what the stated list of pros and cons state. As it stands, this just reads like "I'd like some more patterns".

En3Tho commented 3 years ago

@kevmal It exists but only within some specific FSharp. For example:

type Donk() =
    member val One = 1
    member val Two = 2

type Bleh = 
    val One : int 
    val Two : int
    new(a,b) = {One = a; Two = b}

let d = Donk()
let b = Bleh(1,2)

match d with // does not work
| { One = 1 } -> printf "yay!"
| _ -> printf "boo"

match b with // works
| { One = 1 } -> printf "yay!"
| _ -> printf "boo"

And I don't get this difference at all.

Also, I believe active patterns is a great mechanism and it can tremendously help with tricky matching of nested structures, lots of if-elif-else blocks and so on, but using it just to get a value of property is an overkill. I believe that language should just have a generic one-for-all mechanism for working with properties. If you can match properties of one type then you should be able to work with properties with any other type in similar fashion. And whether someone decided to put heavy logic there or whatever should not be a language's concern.

kevmal commented 3 years ago

And I don't get this difference at all.

type Donk() =
    member val One = 1
    member val Two = 2

One and Two are properties not fields.

type Donk() =
    [<DefaultValue>]
    val mutable One : int
    member val Two = 2

let d = Donk()

match d with
| { One = 1 } -> printf "yay!"
| _ -> printf "boo"

Would work. One is now a field Two is still a property with a backing filed Two@.

En3Tho commented 3 years ago

@kemval Thanks, but I actually meant it in a sense of looking from a language perspective. Not what it gets compiled to. Record "fields" are properties in fact and pattern matching works with them.

En3Tho commented 3 years ago

I've stumbled on this yet again when working with Source Generators. I'd really like to write generator backend on F# and leave C# with calling a few dedicated functions from F#.

Sadly, I feel like C# is better than F# in this kind of task simply because of Property matching. ActivePatterns are great, but they simply don't help in this case because you have no way to apply an ActivePattern to a property of C# object in a nested match expression. You have to write this matching manually.

It's kinda frustrating that instead of writing a simple and concise match expression I have to write additional active patterns, get object through type test matching then match properties of object one by one, but only 1st level because you simply can't go further. So to match other properties you have to get them first etc.

And to be honest, ActivePatterns aren't really better than simple bool returning functions in this case.

dsyme commented 3 years ago

@En3Tho Thanks, I can see where you're coming. Writing separate active patterns for every .NET property is indeed a PITA

So let's discuss property/field matching specifically, putting aside the other things in this suggestion. I can fundamentally see the value in these, despite some of my comments above.

There are several possible syntaxes for property matching:

match x with 
| _.Length as 0 -> ....

match x with 
| _.Length(0) -> ....

match x with 
| (Length=0) -> ....

match x with 
| _.(Length=0) -> ....

match x with 
| _(Length=0) -> ....

and for boolean properties either no special syntax:

match x with 
| _.IsEmpty as true -> ....

match x with 
| _.IsEmpty(true) -> ....

match x with 
| (IsEmpty=true) -> ....

match x with 
| _.(IsEmpty=true) -> ....

match x with 
| _(IsEmpty=true) -> ....

Questions:

  1. Which syntax? If we assume https://github.com/fsharp/fslang-suggestions/issues/506 is in then I quite like this, it is a little more verbose than the C# syntax, however that may be no bad thing - I think the C# syntax is perhaps too succinct on this:

    match x with 
    | _.IsEmpty(true) -> ....
  2. We could consider whether leaving off the pattern implies true so:

    match x with 
    | _.IsEmpty -> ....

    Though the "leaving off the pattern for booleans" rule doesn't quite fit well with me. If we have this kind of rule it should really apply to active patterns - something like that should be orthogonal. However I would find that a bit weird. So I'd be inclined to leave off any special treatment for boolean properties in a first regard.

  3. Do these include extension properties? I would assume so.

  4. Are these only instance properties? I would assume so

  5. The C# Deconstruct pattern should probably be dealt with in the same RFC, or at least the interaction with that considered.

dsyme commented 3 years ago

@Happypig375 If it's ok I'll change the title of this just to deal with property/field matching.

Happypig375 commented 3 years ago

@dsyme It's ok. In https://github.com/fsharp/fslang-suggestions/issues/1018 I hacked together a syntax if that is implemented along with #506.

let (|Member|_|) f = function null -> None | x -> Some <| f x
match typeof<int> with
| Member _.BaseType (Member _.BaseType null) -> printfn "A"
| Member _.BaseType (Member _.BaseType typeof<object>) -> printfn "B"
| Member _.BaseType typeof<object> -> printfn "C"
| Member _.BaseType null -> printfn "D"
| _ -> printfn "E"

But I guess compared to C# having an entire Member in front is still off-putting to C#ers coming to F#. This is also potentially much less efficient. This should be added to the RFC.

dsyme commented 3 years ago

Yes, interesting. With syntaxes proposed above this would be

match typeof<int> with
| _.BaseType (_.BaseType null) -> printfn "A"
| _.BaseType (_.BaseType ty) when ty = typeof<obj> -> printfn "B"
| _.BaseType ty when ty = typeof<obj>  -> printfn "C"
| _.BaseType null -> printfn "D"
| _ -> printfn "E"

etc. Looks ok?

Happypig375 commented 3 years ago

Is the _ just for aligning with #506? It seems to be wasting space.

Also to solve the property-changed-to-method problem, we can also allow whatever #506 enables, namely

_.Foo.Bar
_.Foo.[5]
_.Foo()
_.Foo(5).X
Happypig375 commented 3 years ago

Because we can also align this with https://github.com/fsharp/fslang-suggestions/issues/969#issuecomment-772153700 if we don't have the _.

kevmal commented 3 years ago

Could the syntax just be inline with pattern matching on fields? For example,

match x with 
| {Length = 0} -> ....
| {IsEmpty = true} -> ...
| {Length = length; IsEmpty = false} -> ....
dsyme commented 3 years ago

Is the _ just for aligning with #506? It seems to be wasting space.

Are you suggesting

match typeof<int> with
| .BaseType (.BaseType null) -> printfn "A"
| .BaseType (.BaseType ty) when ty = typeof<obj> -> printfn "B"
| .BaseType ty when ty = typeof<obj>  -> printfn "C"
| .BaseType null -> printfn "D"
| _ -> printfn "E"

or

match typeof<int> with
| BaseType (BaseType null) -> printfn "A"
| BaseType (BaseType ty) when ty = typeof<obj> -> printfn "B"
| BaseType ty when ty = typeof<obj>  -> printfn "C"
| BaseType null -> printfn "D"
| _ -> printfn "E"

The second syntax is not possible, We need something to know that we need to do property resolution at all (i.e. "this is a property pattern"), and to disambiguate with active patterns and other pattern discriminators called BaseType.

The first syntax may be possible but it would seem strange not to have symmetry with #506. (The _. is present in #506 mainly because naked .Prop is just hard to dismbiguate in nested positions, e.g. consider List.map .Prop inputs - that's really subtle, compared to List.map _.Prop inputs.)

dsyme commented 3 years ago

Could the syntax just be inline with pattern matching on fields? For example,

I actually really dislike the use of { ... } in patterns, I think it's always really hard to read, kind of unpleasant on the eye, and I sort of regret having it in F# at all. And in this case there's no symmetry with expression forms.

So I don't really like the idea of extending that.

Happypig375 commented 3 years ago

Would symmetry with https://github.com/fsharp/fslang-suggestions/issues/969#issuecomment-772153700 be doable if not #506?

kevmal commented 3 years ago

Could the syntax just be inline with pattern matching on fields? For example,

I actually really dislike the use of { ... } in patterns, I think it's always really hard to read, kind of unpleasant on the eye, and I sort of regret having it in F# at all. And in this case there's no symmetry with expression forms.

So I don't really like the idea of extending that.

Would the goal be to prefer the new syntax in the case of records/fields as well? When it comes to records (or fields) I would assume you have a choice in syntax at that point? Or would a defined "property" on a record need to & match along with the { ... } syntax?

Happypig375 commented 3 years ago

@kevmal { ... } does type inference which this will not do.

dsyme commented 3 years ago

Would the goal be to prefer the new syntax in the case of records/fields as well?

Yes, because symmetry with #506 would mean _.Ident will work for field, properties. So the pattern form should be likewise. So this should work:

type R = { X: int; Y: int }

let f (r: R) =
    match r with 
    | _.X 3 -> 1
    | _.X 3 & _.Y 4-> 1
    | _ -> 2

It is however unfortunate that this gives two ways to do record matching, both of them verbose and the & obscure.

dsyme commented 3 years ago

I added two more syntax suggestsions to the summary above. First _.(bindings):

match x with 
| _.(Length=0) -> ....

let f (r: R) =
    match r with 
    | _.(X=3) -> 1
    | _.(X=3, Y=4)-> 1
    | _ -> 2

then same without the .

match x with 
| _(Length=0) -> ....

let f (r: R) =
    match r with 
    | _(X=3) -> 1
    | _(X=3, Y=4)-> 1
    | _ -> 2

There is also the question of whether property matching is available immediately on a type test, e.g.

match x with 
| _(Length=0) -> ....

let f (inp: obj) =
    match inp with 
    | :? SubType1(X=3) -> 1
    | :? SubType2(X=3, Y=4)-> 1
    | _ -> 2

These options again lean more towards symmetry with object creation syntax.

To summarize today we have:

  1. One object creation syntax MyObject(X=1, Y=2)
  2. One record creation syntax { X = 1; Y = 2 }
  3. One record pattern syntax { X = pat; Y = pat }
  4. One proposed first-class property extraction syntax _.P

My initial proposal said "symmetry with (4)" but the above tend more towards "symmetry with (1)". We could allow both, so this:

let f1 (x: int list) =
    match x with 
    | _.Length 0 -> ....

let f2 (r: R) =
    match r with 
    | _(X=3) -> 1
    | _(X=3, Y=4)-> 1
    | _ -> 2

let f3 (inp: obj) =
    match inp with 
    | :? SubType1(X=3) -> 1
    | :? SubType2(X=3, Y=4)-> 1
    | _ -> 2

However it's not clear the _.P pat form adds much, e.g. over

match x with 
| _(Length=0) -> ....

Probably better just to have one "object patterns" feature?

Happypig375 commented 3 years ago

If we can apply other patterns while type testing too that would be great.

Happypig375 commented 3 years ago

On top of the f2, we can add the corresponding creation expression with an inferred type as well!

Happypig375 commented 3 years ago

On top of f3 we can also add #830 into the mix.

dsyme commented 3 years ago

On top of f3 we can also add #830 into the mix.

Yes, it's reasonable. I've updated the issue description at the very top to capture the various proposals https://github.com/fsharp/fslang-suggestions/issues/968

En3Tho commented 3 years ago

I believe that an object creation syntax like in f3 is the most intuitive one. It feels like "_" is not adding any value and is contra intuitive (I'm sure people will think something like "why it is there at all?"). I find parentheses the best choice here, it's easy to track scopes, more natural as it resembles existing pm syntax.

Something along

match x with
| ( Values = ( Count > 0 & SomeActivePattern as values), SomeProp = "abc" as prop) -> ...
| _ -> ...

Immediate pattern matching in type tests is super cool. I also would like to see something like


match obj with 
| :? MyType & MyActivePattern as myType-> ...
| :? MyOtherTypeContainingMyType ( MyType = MyActivePattern as myType ) & MyOtherActivePattern -> ...
| _ -> ...
dsyme commented 3 years ago

Also to solve the property-changed-to-method problem, we can also allow whatever #506 enables, namely...

This is an interesting suggestion. However, it effectively makes every single method into a pattern, which is kind of wild - we don't do that for F# functions (you have to write active patterns explicitly, rather than getting a pattern matching function out of your tryXYZ code for free)

Basically it would turn property/method/field patterns into a really, really weird way of running code. Property patterns are already kind of whacky in that

match p with 
| _.Elements [3; v] -> 

is just a weird way of doing

match p.Elements with 
| [3; v] -> 

For methods that goes further:

match p with 
| _.Foo() [3; v] -> 
| _.Goo() [3; v] -> 

is a weird way of doing

match p.Foo() with 
| [3; v] -> ...
| _ -> ... 
match p.Goo() with 
| [3; v] -> 
| ...

In this situation we surely want people to write a clear active pattern rather than just coding up stuff no one is going to be able to understand.

TBH I'm sort of in two minds about property patterns altogether (while being more convinced about object patterns - see the intro at the top of this issue for the difference). But I'll think it over. Overall I think the C# team have not correctly weighed the huge negatives of obfuscated code written using pattern matching.

dsyme commented 3 years ago

I believe that an object creation syntax like in f3 is the most intuitive one. It feels like "_" is not adding any value and is contra intuitive (I'm sure people will think something like "why it is there at all?"). I find parentheses the best choice here, it's easy to track scopes, more natural as it resembles existing pm syntax.

I'm in two minds about this. Pattern matching which has any executable code can be highly confusing (OCaml and Standard ML and Haskell largely avoid any code execution in pattern matching at all). It really doesn't hurt to add some more symbols which help indicate there's a particular kind of match going on.

The pattern (X= ..., Y= ....) just doesn't have a lot of indication that there's an object match with property/field access going on. It has some, but for a beginner it's easily confused with a tuple match, or anonymous record match, or an expression that's using named arguments, or just something the user has never seen before and doesn't know how to look up.

It's not that _(X= ..., Y= ....) helps much, though MyClassType(X= ..., Y= ....) obviously does, as does :? MyClassType(X= ..., Y= ....) (though that's goign to be a type-testing object pattern).

I guess I'm just a bit wary about object patterns not being clear enough syntactically. There might also be amiguity - = is not currently used in pattern syntax in this way, with the right precedence with , so we'll need new parser rules. I think I'll prototype it and see.

dsyme commented 3 years ago

I sketched an overall design in the top of this issue, giving in to (X= ..., Y= ....) for object patterns.

En3Tho commented 3 years ago

@dsyme Can you please clarify about using ActivePatterns in type check expressions? Will the example I provided above work? Now it doesn't and I'm interested if you like such capability/syntax at all?

Happypig375 commented 3 years ago

Hmmm... With #1018 with #506 instead, I can match on indexers and methods even though this won't be permitted here as-is.

dsyme commented 3 years ago

Hmmm... With #1018 with #506 instead, I can match on indexers and methods even though this won't be permitted here as-is.

Correct. I'm very concerned of indecipherable, unreadable pattern logic and indexers and methods both fall in this. Just write active patterns with good names for these.

Can you please clarify about using ActivePatterns in type check expressions?

Yes I believe the second pattern here would work (though not sure of the precedence for as). There's no plan for the Count > 0 in the first, define an active pattern for that.

dsyme commented 2 years ago

Possible spec for object patterns - focusing on C# record/Deconstruct interop.

Object patterns extend syntactic named/tuple patterns. The syntax is extended so each has an (optional) prefix of positional properties and (optional) suffix of named properties.

      | (pat1, .., patN, Property1 = pat1b, ... PropertyM = patMb) -> ,,,

      | TypeName(pat1, .., patN, Property1 = pat1b, ... PropertyM = patMb) -> ...

For a struct, class type or C# record type, pat1...patN correspond to outputs of Deconstruct. The property patterns correspond to properties of the input type, or fields of the input type.

NOTE: Syntactically these are much as named union case patterns today, though those use ; as a separator - object patterns will hopefully use "comma" and union case patterns will be augmented to accept this too.

NOTE: If the input is an F# union or record types, only (Property1 = pat1b, ... PropertyM = patMb) form can be used - no Deconstruct is considered. The properties should be properties and not record fields. This means that for F# union and record types, property qualifications should be specified by using &, e.g. { A = a } & (Property=3) or UnionCase(a, b) & (Property=3).

NOTE: A future integration may allow respectively Record(A=a, Property=3) and UnionCase(a, b, Property=3).

To consider: can TypeName be generic? Can it be a nested type? Is it necessary to have TypeName at all?

baronfel commented 2 years ago

union case patterns will be augmented to accept this too

This is awesome! Would that mean that existing tuple patterns for object properties will need to be parenthesized? Or are existing tuple patterns already required to be parenthesized so it's a no-op.

dsyme commented 2 years ago

Would that mean that existing tuple patterns will need to be parenthesized

Existing tuple patterns wouldn't change - and they don't need to be parenthesized. Object patterns would have to use target typing based on the input type, or an explicit type name, or an annotation. If we don't allow explicit type names then an annotation would be like this and need no addition to the language:

    | ((pat1, Property = pat2) : SomeType) -> ...

As an aside, one of my concerns above was about code transitions for property patterns - "what happens when you change your property inp.Property to a method inp.Method(args) " - do you have to rip out the pattern matching??

In this case you would use active patterns, e.g.

    | (pat1, Property = pat2) -> ,,,

becomes

let (|ExtractViaMethod|_|) args inp =
    match inp.Method(args) with 
    | whatever -> None
    | whatever -> Some res

then

    | pat1 & ExtractViaMethod args pat2 -> ,,,

The key thing here is that active patterns allow arbitrary expressions args to be incorporated into the pattern logic (without parameterized active patterns there's no way to do this without creating fake helper objects to match on?).

En3Tho commented 1 year ago

Any updates? This is my top of the list feature of F# vnext. When working with the broader .Net ecosystem pattern matching strictly on readonly-field-like values feels very limiting.

En3Tho commented 1 year ago

@dsyme Sorry to ping. But just wanted to bring your attention to this one. What I'm interested in how do you personally rank this feature from 1 to 5 in terms of "it fits well and makes F# better as a language" quality. It's my top-waited feature for a long time. Main motivation is consuming interface-based type hierarchies like they do in Roslyn (SourceGenerators) or in Resharper.FSharp (trees are interface based) and obviosuly for better interop with other .Net code based on interfaces or classes that are outside of F# paradigm.

dsyme commented 1 year ago

@En3Tho I've marked this as approved in principle - we should add this in some form - I'd rank it as a 4 or 5 on your scale.