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…


















