How to do numerical simulation with immutable data in Clojure?
Asked Answered
A

4

7

I'm using Clojure and I need to run a small simulation. I have a vector of length n (n is usually between 10 and 100) that holds values. On each simulation round (maybe 1000 rounds together), one of the values in the vector is updated randomly. I guess I could do this by using an Java array and calling the aset method, but this would break the functional programming/immutability idiom.

Is there a more functional way to do this, or should I just go with the Java array?

Armful answered 17/11, 2009 at 11:6 Comment(0)
T
6
(defn run-sim [arr num-iters update-fn]
 (if (zero? num-iters)
   arr
   (let [i (rand-int (count arr))
         x (update-fn)]
     (println "setting arr[" i "] to" x)
     (recur (assoc arr i x) (dec num-iters) update-fn))))

user> (run-sim [1 2 3 4 5 6 7 8 9 10] 10 #(rand-int 1000))
setting arr[ 8 ] to 167
setting arr[ 4 ] to 977
setting arr[ 5 ] to 810
setting arr[ 5 ] to 165
setting arr[ 3 ] to 486
setting arr[ 1 ] to 382
setting arr[ 4 ] to 792
setting arr[ 8 ] to 478
setting arr[ 4 ] to 144
setting arr[ 7 ] to 416
[1 382 3 486 144 165 7 416 478 10]

There's no shame in using a Java array if you need it though. Especially if you need it to go fast. Limit the array-mutation to the inside of your function (clone the input array and work on that maybe) and no one will be the wiser.

Toxicosis answered 17/11, 2009 at 11:55 Comment(0)
I
5

Adding to Brian's answer: If you need more speed, you can also resort to transients.

(defn run-sim
  [vektor num-iters update-fn]
  (loop [vektor    (transient vektor)
         num-iters (int num-iters)]
    (if (zero? num-iters)
      (persistent! vektor)
      (let [i (rand-int (count vektor))
            x (update-fn)]
        (recur (assoc! vektor i x) (dec num-iters))))))
Ichor answered 17/11, 2009 at 12:29 Comment(2)
Thank you! I'll have to dig into that transient stuff, speed is always good!Armful
Transients are easy: call (transient thing) in the beginning. Add a ! to operations, eg. assoc!, dissoc! etc. call (persistent! transient-thing) when returning. You should not leak transients outside your function, though.Ichor
H
2

Lets first define a function which updates a random index in a vector with a new value. Note that the original vector is not changed, instead a new vector (with the updated value) is returned:

(defn f [xs]
  (let [r (java.util.Random.)
        i (.nextInt r (count xs))
        b (.nextBoolean r)]
    (assoc xs i ((if b inc dec) (xs i)))))

This function chooses an index and then it either increases or decreases the value at that index by 1. You must of course change this function to your needs.

Then it is a simple matter to compose this function with itself as many times you want to run the simulation:

user=> ((apply comp (repeat 1000 f)) [0 0 0 0 0 0 0])
[7 -4 7 6 10 0 -6]
Hers answered 17/11, 2009 at 12:26 Comment(1)
Thanks for the answer, this compose-approach looks also interesting.Armful
M
1

It's not that Clojure won't let you change values, it's just a little more cumbersome.

(def vec-ref (ref my-vector))

(dosync (set! vec-ref (assoc my-vector index value))

to look at values in the changed vector, use @vec-ref.

Could be off in details - I'm not near a REPL, unfortunately. But it should get you started.

Moneymaker answered 17/11, 2009 at 11:21 Comment(1)
Thanks, I'll also check this way of solving the problem.Armful

© 2022 - 2024 — McMap. All rights reserved.