Search code examples
f#discriminated-union

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

Solution

  • 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|_|).