在街道上走著,四周空無一人。雨,無聲下了起來。
稍微加快了步伐向前,才想到我似乎還不知道要往哪裡去。然後我注意到打在路面的雨,以及踩上那雨的我的腳,忽然有一種感覺,我們看待這世界的方式,可能是錯的。下一個剎那,我的腳、那雨、這條街以及街道上的我還有這座城市跟整個整個世界都只是一群資料包著資料包著資料包著資料的參數而時間其實是個不斷流向終點的回傳回傳回傳回傳回傳回傳函式的函式的函式的函式的函式的函式的函式的....
醒來。我感覺額頭似乎有點刺痛。
沒睡好嗎?你看起來有點累噢。不過沒關係,我們的素材也蒐集的差不多了,今天本來就打算輕鬆的聊些簡單愉快的東西的。
在你們世界聽到「函數式程式設計」這幾個字時,map
、filter
跟 reduce
幾乎是大部份魔法師最可能會有的反應了,就來仔細的看一下這三個屬於陣列的高階函式吧。
在那之前,我們再來回顧一下最基本的函式操作:
// JavaScript 語法
let x = 1 // 首先,我們有個數字
function addOne(i) { return i + 1 } // 接著有個可以把某個數字加 1 的函式
addOne(x) // 把數字當做參數,呼叫函式,就可以得到回傳的結果
//=> 2
這次想解決的情況,是我們有個裝了很多數字的陣列:
// JavaScript 語法
let numbers = [1, 2, 3, 4, 5]
問題來了,我要怎麼讓陣列裡的每一個數字,都用來當做呼叫那個函式的參數,拿到每一個的回傳值,並且依然維持原本的順序,也保持有個陣列裝著它們。
在其它以指令式 (imperative) 的方式思考的國度裡,通常是先建立一個空的陣列,稱之為累加的陣列 (accumulator)。接著用迴圈跑過要處理的陣列,依序用取出的數字當參數,用來呼叫要處理的函式。最後將結果放進累加陣列中。
// JavaScript 語法
let accu = []
for (let n of numbers) {
let new_number = addOne(n)
accu.push(new_number)
}
accu //=> [2, 3, 4, 5, 6]
但是用函數式的方式來做,我們只要拎著要處理的函式,及被處理的陣列,把它們丟到 map
裡,就能把陣列裡的每個元素,各自當做參數,來呼叫處理一個元素的函式了。
// JavaScript 語法
[1, 2, 3, 4, 5].map(addOne) //#=> [2, 3, 4, 5, 6]
map
這個函式有許多特色,不過今天只要知道兩個就好了:
至於其它的就先別擔心了,我們之後還會跟它相遇很多次。
而 filter
跟 map
長得很像,但是目的不太一樣。它接受一個回傳布林值的函式,接著一樣用一個個的元素當參數去呼叫這個函式,如果回傳的結果是 true
的話,那麼就把這個元素留下來,反之則濾掉。
// JavaScript 語法
let isEven = x => x % 2 == 0 // 當 x 是偶數時回傳 true
[1, 2, 3, 4, 5].filter(isEven) //=> [2, 4]
filter
的特色則是這些:
false
,0
跟空字串都會被視為假值。reduce
則是有比較多魔法師會感覺不熟悉的。reduce
在其它地區有個名字叫 fold
,折疊,意思是我們拿到一個陣列後,想把它折疊成我們想要的新樣子。
除了開始的陣列之外,我們要傳給 reduce
的參數還有兩個。第一個參數是每一步要怎麼合併的函式,而這個函式我們常常稱它為 reducer
。而第二個參數是累加器的起始值。
reducer
本身也接受兩個參數,一個是之前的累加器目前的值,以及這一步的元素。
// JavaScript 語法
let sumReducer = (accu, x) => accu + x //每一步都把之前的結果,跟這步的元素加起來
[1, 2, 3, 4, 5].reduce(sumReducer, 0) //=> 15
仔細看一下,reduce
的做法其實跟我們最早示範的迴圈做法有點像,都有一個一開始的目標(累加器),接著一步步改變這個累加器的內容。
想像一下,如果我們今天來到一個新的國度,但發現它沒有內建 map
這個函式可以用,其實我們可以用 reduce
這個函式,就把 map
實作出來。訣竅就是: map
只是把陣列折疊成…另一個陣列而己。
// JavaScript 語法
function myMap(source, f) {
return source.reduce((accu, x) => [...accu, f(x)], [])
}
myMap([1, 2, 3], x => x * 10); //=> [10, 20, 30]
同樣的,filter
也可以用 reduce
實作出來:
// JavaScript 語法
function myFilter(source, f) {
return source.reduce((accu, x) => f(x) ? [...accu, x] : accu, [])
}
myFilter([1, 2, 3, 4, 5, 6], x => x > 3); //=> [4, 5, 6]
map
順便提一下,這個 Ramda 學院其實也另外做了一個 R.map
, 它的用法是這樣的:
// JavaScript 語法 + Ramda
R.map(i => i * 10, [1, 2, 3, 4, 5]) //=> [10, 20, 30, 40, 50]
最主要的差別,在於這個 map
並不是綁在陣列上用 .
來操作的,而參數的部份先傳了函式,最後才把陣列傳進去。看起來似乎差不多,但再過一陣子你可能就會了解到這是個好得多的設計,如果知道怎麼使用的話。這等到我們前往其它國度時,會看得更清楚。
瞭解了 map
跟 filter
可以用 reduce
做出來之後,順理成章的下一個問題就是:那麼 reduce
是怎麼來的呢?
[to be continue]