简体   繁体   English

你将如何在Clojure中编写这个C ++循环?

[英]How Would You Write This C++ Loop in Clojure?

Although I've done some small amount of programming in functional languages before, I've just started playing with Clojure. 虽然我以前在函数式语言中做了一些少量的编程,但我刚开始玩Clojure。 Since doing the same kind of "Hello World" programs gets old when learning a new language, I decided to go through the Cinder "Hello, Cinder" tutorial, translating it to Clojure and Quil along the way. 由于在学习一门新语言时做同样的“Hello World”程序已经老了,我决定通过Cinder “Hello,Cinder”教程,将其翻译成Clojure和Quil In Chapter 5 of the tutorial, you come across this C++ snippet to calculate acceleration for a list of particles: 在本教程的第5章中,您会遇到此C ++代码段来计算粒子列表的加速度:

void ParticleController::repulseParticles() {
    for( list<Particle>::iterator p1 = mParticles.begin(); p1 != mParticles.end(); ++p1 ) {
        list<Particle>::iterator p2 = p1;
        for( ++p2; p2 != mParticles.end(); ++p2 ) {
            Vec2f dir = p1->mLoc - p2->mLoc;
            float distSqrd = dir.lengthSquared();

            if( distSqrd > 0.0f ){
                dir.normalize();
                float F = 1.0f/distSqrd;

                p1->mAcc += dir * ( F / p1->mMass );
                p2->mAcc -= dir * ( F / p2->mMass );
            }
        }
    }
}

In my eyes, this code has one very important characteristic: it is doing comparisons between pairs of particles and updating both particles and then skipping the same combination in the future. 在我看来,这段代码有一个非常重要的特征:它正在对粒子对进行比较并更新两个粒子,然后在将来跳过相同的组合。 This is very important for performance reasons, since this piece of code is executed once every frame and there are potentially thousands of particles on screen at any given time (someone who understands big O better than I do can probably tell you the difference between this method and iterating over every combination multiple times). 这对于性能原因非常重要,因为这段代码每帧执行一次,并且在任何给定时间屏幕上可能有数千个粒子(比我更了解大O的人可能会告诉你这个方法之间的区别并多次迭代每个组合)。

For reference, I'll show what I came up with. 作为参考,我将展示我的想法。 You should notice that the code below only updates one particle at a time, so I'm doing a lot of "extra" work comparing the same particles twice. 你应该注意到下面的代码一次只更新一个粒子,所以我做了很多“额外”的工作,比较相同的粒子两次。 (Note: some methods left out for brevity, such as "normalize"): (注意:为简洁起见,遗漏了一些方法,例如“normalize”):

(defn calculate-acceleration [particle1 particle2]
  (let [x-distance-between (- (:x particle1) (:x particle2))
        y-distance-between (- (:y particle1) (:y particle2))
        distance-squared (+ (* x-distance-between x-distance-between) (* y-distance-between y-distance-between))
        normalized-direction (normalize x-distance-between y-distance-between)
        force (if (> distance-squared 0) (/ (/ 1.0 distance-squared) (:mass particle1)) 0)]
    {:x (+ (:x (:accel particle1)) (* (first normalized-direction) force)) :y (+ (:y (:accel particle1)) (* (second normalized-direction) force))}))

(defn update-acceleration [particle particles]
  (assoc particle :accel (reduce #(do {:x (+ (:x %) (:x %2)) :y (+ (:y %) (:y %2))}) {:x 0 :y 0} (for [p particles :when (not= particle p)] (calculate-acceleration particle p)))))

(def particles (map #(update-acceleration % particles) particles))

Update : So here's what I ultimately came up with, in case anyone is interested: 更新 :所以这就是我最终提出的,万一有人感兴趣:

(defn get-new-accelerations [particles]
  (let [particle-combinations (combinations particles 2)
        new-accelerations (map #(calculate-acceleration (first %) (second %)) particle-combinations)
        new-accelerations-grouped (for [p particles]
                                    (filter #(not (nil? %)) 
                                            (map 
                                              #(cond (= (first %) p) %2
                                                     (= (second %) p) (vec-scale %2 -1))
                                              particle-combinations new-accelerations)))]
    (map #(reduce (fn [accum accel] (if (not (nil? accel)) (vec-add accel accum))) {:x 0 :y 0} %) 
         new-accelerations-grouped)))

Essentially, the process goes something like this: 从本质上讲,这个过程是这样的:

  1. particle-combinations: Calculate all combinations of particles using the combinatorics "combinations" function 粒子组合:使用组合“组合”函数计算粒子的所有组合
  2. new-accelerations: Calculate a list of accelerations based on the list of combinations 新加速度:根据组合列表计算加速度列表
  3. new-accelerations-grouped: Group up the accelerations for each particle (in order) by looping over every particle and checking the list of combinations, building a list of lists where each sub-list is all of the individual accelerations; 新加速度分组:通过循环每个粒子并检查组合列表,建立列表列表,其中每个子列表是所有单独的加速度,从而对每个粒子的加速度(按顺序)进行分组; there's also the subtlety that if the particle is the first entry in the combination list, it gets the original acceleration, but if it's the second, it gets the opposite acceleration. 还有一个微妙之处在于,如果粒子是组合列表中的第一个条目,它将获得原始加速度,但如果它是第二个,则获得相反的加速度。 It then filters out nils 然后过滤掉nils
  4. Reduce each sub-list of accelerations to the sum of those accelerations 将每个加速度子列表减少到这些加速度的总和

The question now is, is this any faster than what I was doing before? 现在的问题是,这比我之前做的更快吗? (I haven't tested it yet, but my initial guess is no way). (我还没有测试过,但我最初的猜测是没有办法)。

Update 2: Here's another version I came up with. 更新2:这是我提出的另一个版本。 I think this version is much better in all respects than the one I posted above: it uses a transient data structure for performance/easy mutability of the new list, and uses loop/recur. 我认为这个版本在各个方面都比我上面发布的版本好得多:它使用瞬态数据结构来提高新列表的性能/易变性,并使用loop / recur。 It should be much faster than the example I posted above but I haven't tested yet to verify. 它应该比我上面发布的示例快得多,但我还没有测试过验证。

(defn transient-particle-accelerations [particles]
  (let [num-of-particles (count particles)]
    (loop [i 0 new-particles (transient particles)]
      (if (< i (- num-of-particles 1))
        (do 
          (loop [j (inc i)]
            (if (< j num-of-particles)
              (let [p1 (nth particles i)
                    p2 (nth particles j)
                    new-p1 (nth new-particles i)
                    new-p2 (nth new-particles j)
                    new-acceleration (calculate-acceleration p1 p2)]
                (assoc! new-particles i (assoc new-p1 :accel (vec-add (:accel new-p1) new-acceleration)))
                (assoc! new-particles j (assoc new-p2 :accel (vec-add (:accel new-p2) (vec-scale new-acceleration -1))))
                (recur (inc j)))))
          (recur (inc i) new-particles))
        (persistent! new-particles)))))

Re- def -ing particles when you want to update them doesn't seem quite right -- I'm guessing that using a ref to store the state of the world, and then updating that ref between cycles, would make more sense. 重新def我猜测,用裁判来存储世界的状态,然后更新周期之间的裁判,会更有意义-当你想更新他们似乎并不完全正确-ing颗粒。

Down to the algorithmic issue, to me like this is a use case for clojure.math.combinatorics . 至于算法问题,对我来说这是clojure.math.combinatorics的一个用例。 Something like the following: 类似于以下内容:

(require '[clojure.math.combinatorics :as combinatorics])

(defn update-particles [particles]
  (apply concat
    (for [[p1 p2] (combinatorics/combinations particles 2)
          :let [x-distance-between (- (:x p1) (:x p2))
                y-distance-between (- (:y p1) (:y p2))
                distance-squared (+ (* x-distance-between x-distance-between)
                                    (* y-distance-between y-distance-between))
                normalized-direction (normalize x-distance-between y-distance-between)
                p1-force (if (> distance-squared 0)
                             (/ (/ 1.0 distance-squared) (:mass p1))
                          0)]]
     [{:x (+ (:x (:accel p1)) (* (first normalized-direction) p1-force))
       :y (+ (:y (:accel p1)) (* (first normalized-direction) p1-force))}
      {:x (+ (:x (:accel p2)) (* (first normalized-direction) p2-force))
       :y (+ (:y (:accel p2)) (* (first normalized-direction) p2-force))}]))

...you'll still need the reduce, but this way we're pulling updated values for both particles out of the loop. ......你仍然需要减少,但这样我们就可以将两个粒子的更新值拉出循环。

So, essentially, you want to select all subsets of size two, then operate on each such pair? 所以,基本上,你想要选择所有大小为2的子集,然后对每个这样的对进行操作?

Here's a combinatorics library http://richhickey.github.com/clojure-contrib/combinatorics-api.html with 这是一个组合库http://richhickey.github.com/clojure-contrib/combinatorics-api.html with

 combinations 
 function 
 Usage: (combinations items n) All the unique
 ways of taking n different elements from items

Use that to generate your list, then iterate over that. 使用它来生成列表,然后迭代它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM