Thomas
Thomas

Reputation: 12087

How to group data attached to discriminated union values, in F#?

Here is an example:

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

and I have a list of those:

let events : Events list = ...

I need to build a list by event type. Right now I do this:

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

and, repeat for each type...

I also thought I could do something like:

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
        

Is there a more elegant manner to solve this?

This processes a lot of data, so speed is important here.

Upvotes: 4

Views: 116

Answers (3)

Lars
Lars

Reputation: 466

If you keep the union cases, you can group the list items like this.

let name = function
    | A _ -> "A"
    | B _ -> "B"
    | C _ -> "C"

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

And then you can extract the data you want.

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

(The compiler doesn't realize the list only consists of "A" cases, so it gives an incomplete pattern match warning😀)

Upvotes: 1

Sergey Berezovskiy
Sergey Berezovskiy

Reputation: 236188

You can fold back the list of events to avoid writing a recursive function and reversing results. With an anonymous record you will need to define it first and then pipe both arguments ||> to 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 |})

With a named record it is more elegant:

 { listA = []; listB = []; listC = [] } |> List.foldBack addEvent events

addEvent is the same as the lambda above except usage of a named record {} instead of {||}.

Upvotes: 3

Brian Berns
Brian Berns

Reputation: 17028

I think your solution is pretty good, although you do pay a price for reversing the lists. The only other semi-elegant approach I can think of is to unzip a list of tuples:

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

This creates several intermediate lists, so careful internal use of Seq or Array instead might perform better. You would have to benchmark to be sure.

Test case:

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

By the way, your current solution can be simplified to:

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

Upvotes: 2

Related Questions