如何在不指定构造函数的情况下从列表中提取可区分的联合类型?

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

假设我有一个包含三个案例的可区分联合。 Case AC 每个都需要一个构造函数来分别键入 XY。我有一个包含不同 DU 类型的列表,我想将该列表过滤为单个 DU 类型。目前,我有一个由 ABC 组成的列表。现在,如果我只想过滤 DU 列表以键入 case A,我该怎么做而不必将构造函数传递给 case A? (或者传递一个默认的构造函数,我也不知道该怎么做)

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

没有办法通用地制作这样的过滤器。我会做的是

let filteredDUList =
    listDU |> List.filter (function A _ -> true | _ -> false)

如果要提取所有 X,可以改为执行以下操作:

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

如果您知道要在编译时过滤的联合案例,@torbonde 的建议很好,但如果您想要一个适用于任何联合案例的通用解决方案,我认为您需要 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"

请注意,您将支付运行时成本并失去编译器的一些类型检查优势(例如,如果 case A 的名称随后被修改,代码将自动中断),但它会为所欲为。

另一个版本:与@brianberns 解决方案(我认为这是一个很好的解决方案)相比,它不使用反射。它需要创建虚拟值来定义操作中的过滤条件。

这个和所有其他解决方案都不是很好的 f# 代码,应该有更好的方法来完成你想要完成的事情。

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))

为了像这样进行过滤,我们需要 DU 构造函数的对立面,即主动识别器。

不幸的是,您必须手动创建它们,尽管我建议使用 F# 编译器 derive them automatically,这是说明此类建议为何重要的一个很好的例子。

// 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|_|)

结果

val filteredDUList : List<DU> = [A { Id = "1"
                                     Name = "Test" }; A { Id = "2"
                                                          Name = "Test 2" }]

不用说你可以创建普通函数而不是主动识别器,因为仅在这种用法中我们根本不使用模式匹配,我的意思是你可以按照建议命名函数 tryA,而不是 (|A|_|).