Robur_131
Robur_131

Reputation: 694

How to extract discriminated union types from a list without specifying the constructor?

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

Answers (4)

Gus
Gus

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

citykid
citykid

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

Brian Berns
Brian Berns

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

torbonde
torbonde

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 Xs, you can do the following instead:

listDU |> List.choose (function A x -> Some(x) | _ -> None)

Upvotes: 4

Related Questions