-
Notifications
You must be signed in to change notification settings - Fork 21
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
Object/property/field patterns #968
Comments
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
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. |
To be clear, that seq pattern was not C#'s design. The linked proposal only works with any type that:
That said, during the LDM considering list patterns, they considered extending this to all Since F# does not have a generalized collection syntax, I decided to make
Infinite sequences already do not work well with aggregating functions in the
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.
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
But that would be up to the implementation. |
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 |
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. |
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.
Language cruft is real. |
No, it's different. In C#, if you change a property to a method (say one taking no arguments - In contrast, changing a property to a method taking no arguments is a routine change at all callsites for expressions/statements. |
They are common enough, e.g. when moving a property 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 |
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". |
@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. |
Would work. |
@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. |
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. |
@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:
|
@Happypig375 If it's ok I'll change the title of this just to deal with property/field matching. |
@dsyme It's ok. In #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 |
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? |
Because we can also align this with #969 (comment) if we don't have the |
Could the syntax just be inline with pattern matching on fields? For example, match x with
| {Length = 0} -> ....
| {IsEmpty = true} -> ...
| {Length = length; IsEmpty = false} -> .... |
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 The first syntax may be possible but it would seem strange not to have symmetry with #506. (The |
I actually really dislike the use of So I don't really like the idea of extending that. |
Would symmetry with #969 (comment) be doable if not #506? |
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 |
@kevmal |
Yes, because symmetry with #506 would mean 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 |
I added two more syntax suggestsions to the summary above. First 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:
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 match x with
| _(Length=0) -> .... Probably better just to have one "object patterns" feature? |
If we can apply other patterns while type testing too that would be great. |
On top of the |
On top of |
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 -> ...
| _ -> ... |
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 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. |
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 It's not that I guess I'm just a bit wary about object patterns not being clear enough syntactically. There might also be amiguity - |
I sketched an overall design in the top of this issue, giving in to |
@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? |
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.
Yes I believe the second pattern here would work (though not sure of the precedence for |
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,
To consider: can |
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. |
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 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 |
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. |
@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. |
@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. |
Trimmed down suggestion written by @dsyme: I propose we allow two additions - property/field patterns, and "object patterns".
Proposed syntax for property/field patterns:
The pattern may be parenthesized, e.g.
Property patterns can use nesting, so this is allowed:
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)Notes:
Property patterns depend on Allow _.Property shorthand for accessor functions #506
Property patterns can not resolve to methods. Use an active pattern, it's what they're there for.
Property patterns can't resolve to indexers. Just use an active pattern, it's what they're there for. So not this:
Proposed syntax for object patterns:
The type name can be given explicitly (if it doesn't already exist as a pattern discriminator):
Existing type-test patterns would be extended to allow object patterns:
Existing type-test patterns would also be extended to non-object patterns such as unions and records:
Notes
Object patterns can't use nesting of property names, so not
This is because the corresponding object creation syntax doesn't support nesting
Object patterns can't use indexers. This is because the corresponding object creation syntax doesn't support nesting
Object patterns can be used on records, despite the lack of a corresponding syntax for record construction
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
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.function seq { _; secondElem; .. } -> Some secondElem | _ -> None
We should be able to match on arbitrary sequences. If they are of type
IReadOnlyList<T>
,IList<T>
orIList
, 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 beseq []
, 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
andlast
functions.Pros and Cons
The advantages of making this adjustment to F# are
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 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.
The text was updated successfully, but these errors were encountered: