Reputation: 694
Let's say I have a discriminated union consisting of three cases. Case A
and C
each takes a constructor to type X
and Y
respectively. I have a list consisting of different DU types and I want to filter that list down to a single DU type. Currently, I have a list consisting each of A
, B
and C
. Now, if I want to filter the DU list only to type case A
, how can I do that without having to pass the constructor to case A
? (or pass a default constructor, I don't know how to do that either)
type X = {
Id: string
Name: string
}
type Y = {
Id: int
}
type DU =
| A of a:X
| B
| C of b:Y
let extractDUTypesFromList (listDU: List<DU>) (typ: DU) : List<DU> =
listDU
|> List.filter (fun m -> m = typ)
let a = (A {Id = "1"; Name = "Test"})
let aa = (A {Id = "2"; Name = "Test 2"})
let b = B
let c = (C {Id = 1})
let listDU: List<DU> = [a; b; c; aa]
let filteredDUList: List<DU> = // this list will only contain case A
extractDUTypesFromList listDU (A _) // doesn't work
Upvotes: 2
Views: 230
Reputation: 26184
In order to filter like that we need the opposite of the DU constructor, which is an active recognizer.
Unfortunately you'll have to create them by hand, although I did a suggestion to have the F# compiler derive them automatically, this is a good example of why such suggestion matters.
// Active recognizers (ideally autogenerated)
let (|A|_|) = function | A x -> Some x | _ -> None
let (|B|_|) = function | B -> Some () | _ -> None
let (|C|_|) = function | C x -> Some x | _ -> None
let inline extractDUTypesFromList (listDU: List<DU>) (typ: DU -> Option<'t>) : List<DU> =
listDU
|> List.choose (fun x -> typ x |> Option.map (fun _ -> x))
let a = (A {Id = "1"; Name = "Test"})
let aa = (A {Id = "2"; Name = "Test 2"})
let b = B
let c = (C {Id = 1})
let listDU: List<DU> = [a; b; c; aa]
let filteredDUList: List<DU> = // this list will only contain case A
extractDUTypesFromList listDU (|A|_|)
results in
val filteredDUList : List<DU> = [A { Id = "1"
Name = "Test" }; A { Id = "2"
Name = "Test 2" }]
No need to say that you can make normal functions instead of active recognizers, since in this usage alone we're not using pattern matching at all, I mean you can name the function tryA
as suggested, instead of (|A|_|)
.
Upvotes: 2
Reputation: 11090
another version: in contrast to @brianberns solution (which i think is a good one) this does not use reflection. it requires the creation of dummy values to define the filter criteria as in the op.
this and all other solutions are not really nice f# code, there should be a better way for what you want to accomplish.
type X = {
Id: string
Name: string
}
with static member Empty = { Id=""; Name="" }
type Y = {
Id: int
}
with static member Empty = { Id=0 }
type DU =
| A of a:X
| B
| C of b:Y
with
static member IsSameCase a b =
match a, b with
| A _, A _ -> true
| B, B -> true
| C _, C _ -> true
| _ -> false
let extractDUTypesFromList (listDU: List<DU>) (case: DU) : List<DU> =
listDU
|> List.filter (DU.IsSameCase case)
let a = (A {Id = "1"; Name = "Test"})
let aa = (A {Id = "2"; Name = "Test 2"})
let b = B
let c = (C {Id = 1})
let listDU: List<DU> = [a; b; c; aa]
let filteredDUList: List<DU> = // this list will only contain case A
extractDUTypesFromList listDU ((A (X.Empty)))
extractDUTypesFromList listDU ((A (X.Empty)))
extractDUTypesFromList listDU B
extractDUTypesFromList listDU (C (Y.Empty))
Upvotes: 2
Reputation: 17038
@torbonde's suggestion is good if you know the union case you want to filter by at compile time, but if you want a general solution that works for any union case, I think you'll need F# reflection:
open FSharp.Reflection
let extractDUTypesFromList (listDU: List<DU>) (unionCaseName : string) : List<DU> =
listDU
|> List.filter (fun m ->
let unionCase, _ = FSharpValue.GetUnionFields(m, typeof<DU>)
unionCase.Name = unionCaseName)
let filteredDUList: List<DU> = // this list will only contain case A
extractDUTypesFromList listDU "A"
Note that you'll pay a runtime cost and lose some of the type-checking benefits of the compiler (e.g. the code will silently break if case A
's name is subsequently modified), but it will do what you want.
Upvotes: 1
Reputation: 2459
There's no way to make such a filter generically. What I would do is
let filteredDUList =
listDU |> List.filter (function A _ -> true | _ -> false)
If you want to extract all the X
s, you can do the following instead:
listDU |> List.choose (function A x -> Some(x) | _ -> None)
Upvotes: 4