One of my early lessons in Clojure was that it is not wrong to use a loop when a loop is the right solution. The original code is not bad and does not necessarily need any improvement.
If you had a lot of weights (sizes), a binary search for the correct interval would be better than the simple linear search. The most Clojure-ish way to do a binary search is probably to make Java do it.
(defn find-interval [intervals x]
(Math/abs (inc (java.util.Collections/binarySearch intervals x))))
An important functional idiom to learn is closures, or "let over lambda". We can save variables by placing them in the lexical environment of a returned function. In this case, we'll save the precomputed cumulative sum of weights w
, their grand total n
, and the values we want to choose from in a vector.
(defn weighted-choice-generator [choices]
(let [weights (java.util.ArrayList. (reductions + (map second choices)))
values (mapv first choices)
n (last weights)]
(fn [] (nth values (find-interval weights (long (rand n)))))))
We'll coerce the sample data to be a sequence of value-weight pairs as expected above and get our hitting function from the weighted-choice-generator.
(def hit-hobbit-asym (weighted-choice-generator
(map (juxt identity :size) asym-hobbit-body-parts)))
And now test thousands of times to confirm hits are in proportion to size:
(pprint (frequencies (repeatedly 59000 hit-hobbit-asym)))
{{:name "left-shoulder", :size 3} 2951,
{:name "chest", :size 10} 9922,
{:name "left-forearm", :size 3} 3046,
{:name "left-lower-leg", :size 3} 3038,
{:name "neck", :size 2} 1966,
{:name "back", :size 10} 9900,
{:name "left-ear", :size 1} 997,
{:name "nose", :size 1} 1023,
{:name "left-thigh", :size 4} 4020,
{:name "left-achilles", :size 1} 972,
{:name "left-hand", :size 2} 2075,
{:name "left-foot", :size 2} 2062,
{:name "left-eye", :size 1} 1047,
{:name "left-knee", :size 2} 2068,
{:name "left-upper-arm", :size 3} 2996,
{:name "abdomen", :size 6} 6020,
{:name "head", :size 3} 2933,
{:name "left-kidney", :size 1} 986,
{:name "mouth", :size 1} 978}