Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to take n random items from a collection in Clojure?

Tags:

clojure

I have a function that takes n random items from a collection such that the same item is never picked twice. I could accomplish this quite easily:

(defn take-rand [n coll]
  (take n (shuffle coll)))

But I have a pesky requirement that I need to return the same random subset when the same seed is provided, i.e.

(defn take-rand [n coll & [seed]] )

(take-rand 5 (range 10) 42L)  ;=> (2 5 8 6 7)
(take-rand 5 (range 10) 42L)  ;=> (2 5 8 6 7)
(take-rand 5 (range 10) 27L)  ;=> (7 6 9 1 3)
(take-rand 5 (range 10))      ;=> (9 7 8 5 0)

I have a solution for this, but it feels a bit clunky and not very idiomatic. Can any Clojure veterans out there propose improvements (or a completely different approach)?

Here's what I did:

(defn take-rand
  "Returns n randomly selected items from coll, or all items if there are fewer than n.
  No item will be picked more than once."
  [n coll & [seed]]
  (let [n (min n (count coll))
        rng (if seed (java.util.Random. seed) (java.util.Random.))]
    (loop [out [], in coll, n n]
      (if (or (empty? in) (= n 0))
        out
        (let [i (.nextInt rng n)]
          (recur (conj out (nth in i))
                 (concat (take i in) (nthrest in (inc i)))
                 (dec n)))))))
like image 364
Josh Glover Avatar asked Jul 03 '14 12:07

Josh Glover


2 Answers

Well, I'm no clojure veteran, but how about:

(defn shuffle-with-seed
  "Return a random permutation of coll with a seed"
  [coll seed]
  (let [al (java.util.ArrayList. coll)
        rnd (java.util.Random. seed)]
    (java.util.Collections/shuffle al rnd)
    (clojure.lang.RT/vector (.toArray al))))

(defn take-rand [n coll & [seed]]
    (take n (if seed 
              (shuffle-with-seed coll seed)
              (shuffle coll))))

shuffle-with-seed is similiar to clojure's shuffle, it just passes an instance of Random to Java's java.util.Collections.shuffle().

like image 95
sloth Avatar answered Oct 18 '22 06:10

sloth


Replace the shuffle function in your first solution with the reproducible one. I will show you my solution below:

(defn- shuffle'
  [seed coll]
  (let [rng (java.util.Random. seed)
        rnd #(do % (.nextInt rng))]
    (sort-by rnd coll)))

(defn take-rand'
  ([n coll]
     (->> coll shuffle (take n)))
  ([n coll seed]
     (->> coll (shuffle' seed) (take n))))

I hope this solution make results you expected:

user> (take-rand' 5 (range 10))
(5 4 7 2 6)

user> (take-rand' 5 (range 10))
(1 9 0 8 5)

user> (take-rand' 5 (range 10))
(5 2 3 1 8)

user> (take-rand' 5 (range 10) 42)
(2 6 4 8 1)

user> (take-rand' 5 (range 10) 42)
(2 6 4 8 1)

user> (take-rand' 5 (range 10) 42)
(2 6 4 8 1)
like image 43
tnoda Avatar answered Oct 18 '22 08:10

tnoda