Weight-based random sampling

Can’t we write select_item/3 roughly like this, making it effectively a select_item/2?

defp select_item([{name, weight} | _], idx) when idx < weight, do: name
defp select_item([{_, weight} | tail], idx), do: select_item(tail, idx - weight)

While also making use of Enum.reduce/3 in sum_weights/2 we can make it a sum_weights/1:

defp sum_weights(items) do
  Enum.reduce(items, 0, fn {_, weight}, sum -> weight + sum end)
end

Then we leave weight_based_choice nearly untouched (only adjusting calls to the functions we changed):

defmodule Demo do
  def weight_based_choice(items) do
    value =
      items
      |> sum_weights()
      |> :rand.uniform()

    select_item(items, value)
  end

  defp select_item([{name, weight} | _], idx) when idx < weight, do: name
  defp select_item([{_, weight} | tail], idx), do: select_item(tail, idx - weight)

  defp sum_weights(items) do
    Enum.reduce(items, 0, fn {_, weight}, sum -> weight + sum end)
  end
end

 1..10 |> Enum.map(fn _ -> Demo.weight_based_choice(items) end) |> IO.inspect
#=> [:bananas, :bananas, :bananas, :bananas, :bananas, :bananas, :bananas, :bananas, :oranges, :bananas]

This is probably one of the most idiomatic approaches. We could even implement select_item/2 in terms of Enum.reduce_while/3, but usually this doesn’t add much in terms of readability, also not many people are used to that function…

defp select_item(items, idx) do
  Enum.reduce_while(items, idx, fn
    {_, weight}, acc when weight < acc -> {:cont, acc - weight}
    {name, weight}, acc -> {:halt, name}
  end
end

PS: Perhaps we need to adjust </<=/>/>= in the guards… I’m writing this mostly freehand…