雖然我之前在函數式語言中做過少量的編程,但我剛開始使用Clojure。由於在學習一門新語言時做同樣的「Hello World」程序會變老,所以我決定通過Cinder「Hello,Cinder」教程,將它翻譯成Clojure和Quil。在本教程的Chapter 5,你碰到過這樣的C++代碼段來計算加速粒子的列表:你會如何在Clojure中編寫這個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);
}
}
}
}
在我眼裏,這個代碼有一個非常重要的特性:它是對顆粒和更新之間做比較這兩個粒子,然後在未來跳過相同的組合。這對於性能的原因非常重要,因爲這段代碼每幀執行一次,並且在任何給定時間屏幕上可能有成千上萬個粒子(比我更瞭解大O的人可能會告訴您這種方法之間的差異並多次迭代每個組合)。
作爲參考,我會展示我想出的。你應該注意到下面的代碼一次只更新一個粒子,所以我做了很多「額外」的工作,比較兩次相同的粒子。 (注:爲簡潔留下了一些方法,如「規範」):
(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))
更新:所以這就是我最終來到了,如果有人有興趣:
(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)))
從本質上講,過程是這樣的:
- 顆粒組合:計算使用組合數學「組合」粒子的所有組合功能
- 新加速度:計算基於組合的列表
- 新加速度-分組上的加速度的列表:組向上通過遍歷每個粒子和檢查組合的列表每個粒子的加速度(按順序),建立列表,其中每個子列表是所有單個加速度;還有一點微妙之處在於,如果粒子是組合列表中的第一個輸入項,它將獲得原始加速度,但如果它是第二個,則會得到相反的加速度。然後,它過濾掉尼爾斯
- 減少加速度的每個子列表的加速度
現在的問題的總和,這是任何速度比我在做什麼之前? (我還沒有測試過,但我最初的猜測是沒有辦法的)。
更新2: 下面是另一個版本,我想出了。我認爲這個版本在所有方面比我上面發佈的版本好得多:它使用瞬態數據結構來實現新列表的性能/易變性,並使用循環/重複。它應該比我在上面發佈的例子快得多,但我還沒有測試過要驗證。
(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)))))
FWIW,改變我的應用程序使用上面的瞬態循環後,它比我第一次更新中顯示的代碼運行速度快得多(如通過在屏幕上保持60fps 2-3倍的粒子所證明的那樣)。 – davertron