iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 10
1
Software Development

mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉系列 第 10

mostly:functional 第九章:高階函式與它們的產地

在街道上走著,四周空無一人。雨,無聲下了起來。

稍微加快了步伐向前,才想到我似乎還不知道要往哪裡去。然後我注意到打在路面的雨,以及踩上那雨的我的腳,忽然有一種感覺,我們看待這世界的方式,可能是錯的。下一個剎那,我的腳、那雨、這條街以及街道上的我還有這座城市跟整個整個世界都只是一群資料包著資料包著資料包著資料的參數而時間其實是個不斷流向終點的回傳回傳回傳回傳回傳回傳函式的函式的函式的函式的函式的函式的函式的....

醒來。我感覺額頭似乎有點刺痛。


沒睡好嗎?你看起來有點累噢。不過沒關係,我們的素材也蒐集的差不多了,今天本來就打算輕鬆的聊些簡單愉快的東西的。

在你們世界聽到「函數式程式設計」這幾個字時,mapfilterreduce 幾乎是大部份魔法師最可能會有的反應了,就來仔細的看一下這三個屬於陣列的高階函式吧。

在那之前,我們再來回顧一下最基本的函式操作:

// 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

但是用函數式的方式來做,我們只要拎著要處理的函式,及被處理的陣列,把它們丟到 map 裡,就能把陣列裡的每個元素,各自當做參數,來呼叫處理一個元素的函式了。

// JavaScript 語法
[1, 2, 3, 4, 5].map(addOne) //#=> [2, 3, 4, 5, 6]

https://ithelp.ithome.com.tw/upload/images/20200924/20103390aOyNUZvtE1.png

map 這個函式有許多特色,不過今天只要知道兩個就好了:

  • 原先的陣列回傳陣列長度永遠相同。
  • 丟進去的函式,只接受一個參數,就是陣列裡的單個元素

至於其它的就先別擔心了,我們之後還會跟它相遇很多次。

filter

filtermap 長得很像,但是目的不太一樣。它接受一個回傳布林值的函式,接著一樣用一個個的元素當參數去呼叫這個函式,如果回傳的結果是 true 的話,那麼就把這個元素留下來,反之則濾掉。

// JavaScript 語法
let isEven = x => x % 2 == 0 // 當 x 是偶數時回傳 true
[1, 2, 3, 4, 5].filter(isEven) //=> [2, 4]

https://ithelp.ithome.com.tw/upload/images/20200924/20103390QU7pfPhNKk.png

filter 的特色則是這些:

  • 回傳陣列的長度,只會等於或是小於原先的陣列。
  • 因為只決定是不是要把元素留下來,所以回傳陣列中的元素,一定也是原先陣列的元素。
  • 傳進去的函式不一定要回傳真正的布林值,而是看不同的國度決定什麼樣的值是真值 (truthy) 或假值 (flasy)。例如在 JS 莊園裡,false0 跟空字串都會被視為假值。

reduce

reduce 則是有比較多魔法師會感覺不熟悉的。reduce 在其它地區有個名字叫 fold折疊,意思是我們拿到一個陣列後,想把它折疊成我們想要的新樣子

https://ithelp.ithome.com.tw/upload/images/20200924/20103390WKBFD8yhyC.png

除了開始的陣列之外,我們要傳給 reduce 的參數還有兩個。第一個參數是每一步要怎麼合併的函式,而這個函式我們常常稱它為 reducer。而第二個參數是累加器的起始值

reducer 本身也接受兩個參數,一個是之前的累加器目前的值,以及這一步的元素。

// JavaScript 語法
let sumReducer = (accu, x) => accu + x //每一步都把之前的結果,跟這步的元素加起來

[1, 2, 3, 4, 5].reduce(sumReducer, 0) //=> 15

仔細看一下,reduce 的做法其實跟我們最早示範的迴圈做法有點像,都有一個一開始的目標(累加器),接著一步步改變這個累加器的內容。

map 是怎麼來的?

想像一下,如果我們今天來到一個新的國度,但發現它沒有內建 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 呢?

同樣的,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 並不是綁在陣列上用 . 來操作的,而參數的部份先傳了函式,最後才把陣列傳進去。看起來似乎差不多,但再過一陣子你可能就會了解到這是個好得多的設計,如果知道怎麼使用的話。這等到我們前往其它國度時,會看得更清楚。

瞭解了 mapfilter 可以用 reduce 做出來之後,順理成章的下一個問題就是:那麼 reduce 是怎麼來的呢?

[to be continue]


上一篇
mostly:functional 第八章:急躁的,耐心的,以及還不完整的。
下一篇
mostly:functional 第十章:自我指涉的藝術
系列文
mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉35
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言