Weight-based random sampling

That’s a fun little problem. Here’s my take, from playing around in iex

iex(109)> items = [apples: 10, oranges: 30, bananas: 60]
[apples: 10, oranges: 30, bananas: 60]
iex(110)> accumulated_weights = Enum.scan(items, fn {k, w}, {_, w_sum} -> {k, w + w_sum} end)
[apples: 10, oranges: 40, bananas: 100]
iex(111)> {_, max} = List.last(accumulated_weights)
{:bananas, 100}
iex(112)> random_value = Enum.random(1..max)
17
iex(113)> sample = Enum.reduce_while(accumulated_weights, random_value, fn {k, w}, r -> if r <= w, do: {:halt, k}, else: {:cont, r} end)
:oranges

I believe this is the first time I’ve used Enum.scan. It lets us get that accumulated_weights format, which is handy for reuse if we will be getting more random samples from the same list of inputs.

We get our max by pattern matching on the last item of accumulated_weights

Enum.reduce_while then halts iteration when it has found the proper match. It might make some static type people twitch because when continuing the random_value is kept as the accumulator, but when halting, the keyword becomes the accumulator that is returned from the overall iteration.