The way I understand it, logically works as follows:
Sum all weights, then select a random double somewhere between 0 and the sum of all weights. Find the item that matches your likelihood.
In other words, you want to display your list as follows:
Item Val Offset Max (Val + Offset) ---- --- ------ ------------------ a 0.7 0.0 0.7 b 0.6 0.7 1.3 c 0.5 1.3 1.8 d 0.1 1.8 1.9
Converting a list (item, probability) to (item, max) is simple:
let probabilityMapped prob = [ let offset = ref 0.0 for (item, probability) in prob do yield (item, probability + !offset) offset := !offset + probability ]
Although this applies to mutables, its clean, deterministic and readable code. If you insist on avoiding a volatile state, you can use this (rather than tail recursively):
let probabilityMapped prob = let rec loop offset = function | [] -> [] | (item, prob)::xs -> (item, prob + offset)::loop (prob + offset) xs loop 0.0 prob
Despite the fact that we list the state through a list, we are executing a map, not a bend operation, so we should not use the Seq.fold or Seq.scan methods. I started writing code with Seq.scan and it looked hacked and weird.
Whichever method you choose, once you get your list, it is very easy to select a random weighted element in linear time:
let rnd = new System.Random() let randomPick probSeq = let probMap = [ let offset = ref 0.0 for (item, probability) in probSeq do yield (item, probability + !offset) offset := !offset + probability ] let max = Seq.maxBy snd probMap |> snd let rndNumber = rnd.NextDouble() * max Seq.pick (fun (item, prob) -> if rndNumber <= prob then Some(item) else None) probMap