Open Happypig375 opened 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
these features are obscure in syntax and semantics, and some will come to be loathed by C# teams
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?
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.
To be clear, that seq pattern was not C#'s design. The linked proposal only works with any type that:
- Has an accessible property getter that returns an
int
and has the nameLength
orCount
- Has an accessible indexer with a single
int
parameter- Has an accessible
Slice
method that takes twoint
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
- Has an accessible property getter that returns an
int
and has the nameLength
orCount
- Has an accessible indexer with a single
int
parameter- Has an accessible
Slice
method that takes twoint
parameters (for slice subpatterns)
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 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.
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.
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.
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.
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.
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
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.
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@
.
@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:
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) -> ....
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.
Do these include extension properties? I would assume so.
Are these only instance properties? I would assume so
The C# Deconstruct pattern should probably be dealt with in the same RFC, or at least the interaction with that considered.
@Happypig375 If it's ok I'll change the title of this just to deal with property/field matching.
@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.
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?
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
Because we can also align this with https://github.com/fsharp/fslang-suggestions/issues/969#issuecomment-772153700 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} -> ....
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
.)
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 symmetry with https://github.com/fsharp/fslang-suggestions/issues/969#issuecomment-772153700 be doable if not #506?
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?
@kevmal { ... }
does type inference which this will not do.
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.
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:
MyObject(X=1, Y=2)
{ X = 1; Y = 2 }
{ X = pat; Y = pat }
_.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?
If we can apply other patterns while type testing too that would be great.
On top of the f2
, we can add the corresponding creation expression with an inferred type as well!
On top of f3
we can also add #830 into the mix.
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
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 -> ...
| _ -> ...
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.
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.
I sketched an overall design in the top of this issue, giving in to (X= ..., Y= ....)
for object patterns.
@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?
Hmmm... With #1018 with #506 instead, I can match on indexers and methods even though this won't be permitted here as-is.
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.
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 - noDeconstruct
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)
orUnionCase(a, b) & (Property=3)
.NOTE: A future integration may allow respectively
Record(A=a, Property=3)
andUnionCase(a, b, Property=3)
.
To consider: can TypeName
be generic? Can it be a nested type? Is it necessary to have TypeName
at all?
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.
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?).
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. 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.
@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 #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 useas
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 typeIReadOnlyList<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 :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.