Object/property/field patterns

5 min read Original article ↗

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 Allow _.Property shorthand for accessor functions #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:

  • 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

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.