如何在F#中对联合类型的值所附带的数据进行分组?

4

以下是一个例子:

type Events =
    | A of AData
    | B of BData
    | C of CData

我有一个列表:

let events : Events list = ...

我需要按事件类型建立一个列表。目前我是这样做的:

let listA =
    events
    |> List.map (fun x ->
        match x with
        | A a -> Some a
        | _ -> None
    )
    |> List.choose id

然后,对于每种类型都进行重复...

我还认为我可以做类似于:

let rec split events a b c =
    match events with
    | [] -> (a |> List.rev, b |> List.rev, c |> List.rev)
    | h :: t ->
        let a, b, c =            
            match h with
            | A x -> x::a, b, c
            | B x -> a, x::b, c
            | C x -> a, b, x::c
        split t a b c
        

有没有更优雅的方式来解决这个问题?

这里处理了许多数据,因此速度很重要。


可能是List.group - Bent Tranberg
type Events 可能不太适合当前的任务。当然,这取决于可以承受的重构规模。 - kkm
我之前遇到过这个问题。这是我在F#中注意到的几种(相对罕见的)情况之一,你知道你想做什么,但语言没有提供一种优雅的方式来实现它。所以我没有任何有用的建议。你的B选项是我通常采取的方法(它并不太不优雅;)) - Overlord Zurg
3个回答

3
你可以折叠回事件列表,避免编写递归函数和反转结果。使用匿名记录,你需要先定义它,然后将两个参数||>管道到List.foldBack中:
let eventsByType =
    (events, {| listA = []; listB = []; listC = [] |})
    ||> List.foldBack (fun event state ->
        match event with
        | A a -> {| state with listA = a :: state.listA |}
        | B b -> {| state with listB = b :: state.listB |}
        | C c -> {| state with listC = c :: state.listC |})

使用命名记录更加优雅:
 { listA = []; listB = []; listC = [] } |> List.foldBack addEvent events

addEvent与上面的lambda相同,只是使用命名记录{}而不是 {||}


是的,fold和friends是以命令式方式实现的,所以曾经被认为更有效率。另一方面,反转列表非常惯用。如果考虑到编译需要花费多少时间,我不会感到惊讶F#编译器优化了这个问题...在没有(a)性能要求和(b 如果有)基准测试的情况下,偏好只是个人喜好而已。我完全不会争论,只是分享我的想法。在缺少数据(a)和(a)->(b)的情况下,我对OP和你的解决方案犹豫不决,所以我会点赞两个:-))哦,顺便说一句,“Thinking...”by Dani Kahneman 是一本好书!!! - kkm
哦,而且匿名的_value_记录不应该影响性能。但是...(b->a)。基准测试,基准测试,基准测试——如果你有理由让代码更高效的话。 - kkm

2

我认为你的解决方案相当不错,尽管你需要付出反转列表的代价。我能想到的唯一其他半优雅的方法是解压元组列表:

let split events =
    let a, b, c =
        events
            |> List.map (function 
                | A n -> Some n, None, None
                | B s -> None, Some s, None
                | C b -> None, None, Some b)
            |> List.unzip3
    let choose list = List.choose id list
    choose a, choose b, choose c

这会创建多个中间列表,因此仔细地在内部使用SeqArray可能会更好地执行。您需要进行基准测试以确保。

测试用例:

split [
    A 1
    A 2
    B "one"
    B "two"
    C true
    C false
] |> printfn "%A"   // [1; 2],[one; two],[true; false]

顺便说一下,你目前的解决方案可以简化为:

let listA =
    events
    |> List.choose (function A a -> Some a | _ -> None)

1
如果保留联合案例,您可以像这样对列表项进行分组。
let name = function
    | A _ -> "A"
    | B _ -> "B"
    | C _ -> "C"

let lists =
    events 
    |> List.groupBy name
    |> dict

然后,您可以提取所需的数据。

let listA = lists["A"] |> List.map (fun (A data) -> data)

(编译器没有意识到列表只包含“A”情况,因此会给出不完整的模式匹配警告)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接