在關於 Transducer的文章中,我們看到了一種優化陣列上長操作序列的方法,但解決方案並不那麼簡單。許多讀者想知道我們是否能更簡單地實現同樣的結果,本文將解決這個問題:我們將不使用 Transducer(就像之前那樣),但在一些函數語言程式設計的幫助下,我們將得到同樣好的解決方案。
讓我們回憶一下最初的問題–這只是一個微不足道的例子,旨在強調我們遇到的問題。我們有一個陣列,我們想:(1) 只取奇數值;(2) 複製它們;(3) 保持值小於 50;(4) 在它們後面加上 3。下面的程式碼實現了這一點:
const testOdd = (x) => x % 2 === 1; const duplicate = (x) => x + x; const testUnderFifty = (x) => x < 50; const addThree = (x) => x + 3; const myArray = [22, 9, 60, 24, 11, 63]; const newArray = myArray .filter(testOdd) .map(duplicate) .filter(testUnderFifty) .map(addThree`` console.log(newArray); // [21,25]
問題出在哪裡?每次執行操作時,都會建立一個新的中間陣列,如果資料量很大,額外的時間可能會非常長。
在上一篇文章中,我們介紹了一種解決方案,即嘗試逐個陣列元素並按順序應用所有變換,我們還為此開發了 Transducer。不過,我們可以用更簡單的方法來解決!讓我們來看看。
第一個(但不太好的)解決方案
我們的例子很簡單明瞭,但有些讀者提出了反對意見,他們的理由是:很明顯,我們在原始陣列中只取 25 個以下的奇數值,然後將它們複製並加上三個,這樣一個簡單的迴圈就能完成工作……而且我們甚至不需要使用原來的四個函式!
const simplifiedPass = (someArray) => { const result = []; someArray.forEach((value) => { if (value % 2 && value < 25) { result.push(value * 2 + 3); } }); return result; }; const newArray2 = simplifiedPass(myArray); console.log(newArray2);
這樣做是可行的,也是完全正確的,但很難推廣。在我們這個簡單的例子中,推匯出哪些數字將被處理很容易,但在現實生活中,邏輯檢查可能非常複雜,你不可能像我們在這裡所做的那樣,將它們簡化為一個單一的測試。因此,雖然這表明優化是可行的,但這種方法本身並不能適用於所有情況;我們需要一種更通用的解決方案。
第二種(也是手工製作的)解決方案
與前一種情況一樣,如果我們逐個元素依次應用對映和過濾器,效果會更好。
const singlePassFourOps = (someArray) => { const result = []; someArray.forEach((value) => { let ok = false; // ① if (testOdd(value)) { value = duplicate(value); if (testUnderFifty(value)) { value = addThree(value); ok = true; } } if (ok) { // ② result.push(value); } }); return result; }; const newArray3 = singlePassFourOps(myArray); console.log(newArray3); // [ 21,25 ]
我們使用 ok
變數 ① 來了解原始陣列中的 value
是否通過了所有測試 ②。每個 filter()
都會轉化為 if
,每個 map()
都會更新 value
。如果操作結束,我們會將 ok
設為 true,這意味著我們會將最終值推入 result
陣列。這樣就成功了!不過,我們不得不從頭開始編寫一個函式,也許我們可以找到更通用的解決方案。
第三種(更通用的)解決方案
前面的程式碼要求為每一組轉換編寫一個函式。但是,如果將所有對映和過濾函式傳遞到一個陣列中呢?這意味著我們可以編寫一個通用函式,處理過濾器和對映的任何組合。
但有一個問題:我們怎麼知道陣列中的函式是指過濾器還是對映呢?讓我們建立一個包含成對值的陣列:首先是 “M “或 “F”,表示 “map” 或 “filter”,然後是函式本身。通過測試這對數值中的第一個值,我們可以知道如何處理第二個值,即函式。
const singlePassManyOps = (someArray, fnList) => { const result = []; someArray.forEach((value) => { // ① if ( fnList.every(([type, fn]) => { // ② if (type === "M") { // ③ value = fn(value); return true; } else { return fn(value); // ④ } }) ) { result.push(value); } }); return result; }; const newArray4 = singlePassManyOps(myArray, [ ["F", testOdd], ["M", duplicate], ["F", testUnderFifty], ["M", addThree], ]); console.log(newArray4);
這樣做的原理是:我們逐個元素地檢視陣列①。對於陣列 ② 中的每個函式,如果它是 map ③,我們就更新 value
,否則就測試是否繼續 ④。為什麼我們使用 every()
而不是 foreach()
?關鍵在於,一旦某個過濾器失效,我們就想停止遍歷函式陣列,但 foreach()
沒有停止迴圈的好方法。另一方面,一旦遇到錯誤結果, every()
就會停止迴圈–如果過濾函式返回 false,就會發生這種情況。(順便說一下,這也是我們在對映後返回 true
的原因,這樣 every()
就不會停止了)。現在情況好多了!還能再簡單點嗎?
第四種(更簡單的)解決方案
我們可以通過編寫一對函式來幫助我們生成所需的陣列,從而簡化工作;給定一個函式,它們就會生成正確的一對值。
const applyMap = (fn) => ["M", fn]; const applyFilter = (fn) => ["F", fn];
有了這些函式,我們就可以將上面的程式碼改為下面的程式碼。
const newArray5 = singlePassManyOps(myArray, [ applyFilter(testOdd), applyMap(duplicate), applyFilter(testUnderFifty), applyMap(addThree), ]); console.log(newArray5);
這樣更好看,也更具有宣告性:你可以清楚地看到,我們首先應用了一個過濾器( testOdd
),然後應用了一個對映( duplicate
),等等,順序與原始程式碼相同。此外,效能也達到了最佳;我們沒有任何中間陣列,也沒有執行任何不必要的過濾器或對映。我們就大功告成了!不過…
第五種(也是最終的)解決方案
在上一篇文章中,我們提到我們並不總是希望通過生成一個陣列來結束;我們可能希望應用最後的 reduce()
操作。我們可以讓 result
的初始值和更新它的函式成為可選項,並使用預定義的預設值來解決這個問題。
const singlePassMoreGeneral = ( someArray, fnList, initialAccum = [], reduceAccumFn = (accum, value) => { accum.push(value); return accum; } ) => { let result = initialAccum; // ① someArray.forEach((value) => { if ( fnList.every(([type, fn]) => { if (type === "M") { value = fn(value); return true; } else { return fn(value); } }) ) { result = reduceAccumFn(result, value); // ② } }); return result; }; const newArray6 = singlePassMoreGeneral(myArray, [ applyFilter(testOdd), applyMap(duplicate), applyFilter(testUnderFifty), applyMap(addThree), ]); console.log(newArray6);
除了增加 initialAccum
(用於還原操作的累加器初始值)和 reduceAccumFn
(還原函式本身)之外,其他變化是 result
的初始值 ① 和 result
的更新方式 ②。我們可以通過求最終值的總和來測試這一點。
const newValue = singlePassMoreGeneral( myArray, [ applyFilter(testOdd), applyMap(duplicate), applyFilter(testUnderFifty), applyMap(addThree), ], 0, (x, y) => x + y ); console.log(newValue);
這樣就成功了!現在,我們可以以最佳方式應用任意序列的對映和過濾操作,如果需要的話,還可以選擇以還原步驟結束。
小結
文章開始時,我們試圖找到一種更簡單、但同樣強大的方法來優化大型陣列上的操作序列。我們從一個非常簡單但不夠通用的解決方案,最終找到了一個完全等同於 transducer 的解決方案,儘管在很多方面也使用了函數語言程式設計,但可以說更容易理解。這充分說明了 “剝貓皮的方法不止一種” 這一觀點的正確性,或者說,在我們的案例中,”優化陣列過程” 的方法不止一種!
評論留言