Fork me on GitHub
#clojure
<
2021-03-27
>
martinklepsch13:03:34

I just wrote this to cycle through a list of values in a stateful way, would be curious if others have better ideas on how to implement this. I think there might be a way with keeping a reference to a lazy seq but I don’t know if that’s a good idea and also I’m not sure how that would be done

(defn stateful-cycle [xs]
  (let [state (atom (shuffle xs))]
    (fn []
      (let [r (first @state)]
        (reset! state (conj (vec (rest @state)) r))
        r))))

👏 3
andy.fingerhut13:03:03

cycle is built into Clojure core library, and returns infinite (lazy) sequence that cycles through its input collection values.

andy.fingerhut13:03:19

If you want a stateful function that returns the elements in a cyclic order on multiple subsequent calls, you could put the return value of cycle into an atom, and call first and rest / next on that sequence on each call.

andy.fingerhut13:03:47

There would be no difference in behavior from the implementation you show, but an implementation using cycle would probably be more efficient in CPU cycles than yours, where you are building a vector of all elements of the cycle as a vector using vec on every call.

andy.fingerhut13:03:41

The implementation you show is not safe to call from multiple threads, since it reads the current atom value twice, and modifies it once, with no synchronization. If you needed something that could be used from multiple threads correctly, you could make a pure function that takes the current value of the cycle, and returns the next value of the cycle, and call that function on the atom via swap!

andy.fingerhut13:03:56

Here is an implementation that should be safe to call from multiple threads, and uses cycle for better efficiency (should be O(1) time per call, rather than O(n) where n is the number of elements in xs):

(defn stateful-cycle2 [xs]
  (let [state (atom (cycle xs))]
    (fn []
      (let [next-cycle (swap! state next)]
        (first next-cycle)))))

andy.fingerhut13:03:59

I did not include the call to shuffle like yours does. In terms of API, it seems better to me to allow the caller to make the decision to shuffle the sequence, or not, e.g. by calling (stateful-cycle2 (shuffle xs)) if they want randomization of order, or call (stateful-cycle2 xs) if they do not.

andy.fingerhut13:03:36

"better" in the sense that the function is useful in more situations, because it does not force the shuffle behavior.

martinklepsch15:03:36

Wow Andy, that’s great feedback all around! Good point about shuffle as well

martinklepsch15:03:20

thread safety wasn’t really a concern since it’s JS but I like how your usage of next just simplifies the whole thing

chrisn16:03:03

Another jvm-specific formulation that may be interesting:

(let [state-iter (.iterator ^Iterable (cycle xs))] 
  #(locking state-iter
     (.next state-iter)))

noisesmith23:03:01

slightly more compact version

(defn stateful-cycle3
  [xs]
  (let [state (atom (cycle (shuffle xs)))]
    #(first (swap! state rest))))

seancorfield19:03:16

Thank you @slipset:

(! 1012)-> clj -Sdeps '{:deps {org.clojure/data.json {:mvn/version "2.0.2"}}}' -A:1.11
Clojure 1.11.0-alpha1
user=> (require '[clojure.data.json :as json])
nil
user=> (json/write-str {:a 1 :b 2})
"{\"a\":1,\"b\":2}"
user=> (json/write-str {:a 1 :b 2} :value-fn (fn [_ v] (inc v)))
"{\"a\":2,\"b\":3}"
user=> (json/write-str {:a 1 :b 2} {:value-fn (fn [_ v] (inc v))}) ; Clojure 1.11 calling style
"{\"a\":2,\"b\":3}"
user=> 

seancorfield19:03:40

Now I can clean up a bit more code at work! 🙂

slipset19:03:38

You’re welcome!