In the process of learning the awesomness that is F# + Linq I came to a problem I can not solve(nicely) using functional, OOP, nor Linq like syntax, would anyone be willing to help?
Lets say my input is the following sequence:
let db = seq [ ("bob", 90, ['x';'y'])
("bob", 70, ['z'])
("frank", 20, ['b'])
("charlie", 10, ['c']) ]
Rows could read for example "Student bob has enrolled in x,y in semester 90"
What I need is this instead:
[ ("bob", [90; 70], ['x'; 'y'; 'z'])
("frank", [20], ['b'])
("charlie", [10], ['c']) ]
This would read instead "Bob has finished semesters 90,70 and taken x,y,z".
Linq/Relational approach usualy gives the most readable solutions to such problems. But the best I can come up with is:
type Student = string
type Semester = int
type Class = char
let restructure (inp:seq<Student * Semester * Class list>) = query {
for (student, semester, classes) in inp do
groupValBy (semester,classes) student into data
yield (data.Key, Seq.map fst data, Seq.collect snd data)
}
Which is neither readable, nor fast, nor pretty, nor idiomatic, and due to intricacies of F# requires that I write the input type signature...
Is there a better way some GroupMultipleValBy function? Thank you very much!
If you can stick to "classic" F# code you can rewrite it in a more readable way (especially by using locals to make the code even more readable)
Well it seems Tomas beat me to this, we've gone roughly the same road with some "quirks" in the middle
let restructure inp =
// could have been defined at a more global scope as helpers
let fst3 (x, _, _) = x
let flip f y x = f x y
let folder cont (student, semester, classes) (_, semesters, allClasses) =
cont (student, semester :: semesters, classes @ allClasses)
let initialState = "", [], []
inp
|> Seq.groupBy fst3
|> Seq.map (snd >> flip (Seq.fold folder id) initialState)
The hard part to understand is the folder one, to keep semesters in the wanted order we either have to add an extra step reversing that part or as done here using a continuation
That (and the use of flip to keep it point free) added with the use of @ makes me think Tomas code is "better" (but his answer makes semesters and classes seq instead of list)
Addendum
Here's Tomas code written in a way I find more readable (but that's a matter of taste) and maybe more agnostic about what's being manipulated although it's longer
[that doesn't take anything to it's answer which is great]
let restructure inp =
// could have been defined at a more global scope as helpers
let fst3 (x, _, _) = x
let snd3 (_, y, _) = y
let trd3 (_, _, z) = z
let mapping (key, values) =
key,
// replace with commented part to have lists instead of seqs
values |> Seq.map snd3, //[ for value in values -> snd3 value ],
values |> Seq.collect trd3 //[ for value in values do yield! trd3 value ]
inp
|> Seq.groupBy fst3
|> Seq.map mapping
If you do not insist on using the query syntax (which is needed if you are working with databases, but is just one of the options when working with in-memory data), then I would probably use simple Seq.groupBy function:
db
|> Seq.groupBy (fun (name, _, _) -> name)
|> Seq.map (fun (name, group) ->
name,
group |> Seq.map (fun (_, sem, _) -> sem),
group |> Seq.collect (fun (_, _, courses) -> courses) )
Here, we are saying that we want to group records by student name and return a triple with:
This is not shorter than your version, but I think a combination of groupBy and map is a fairly common pattern that is quite easy to understand. That said, I'm quite curious to see other answers! I can imagine there would be a nicer way of doing this...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With