跟著 Ramda 學 FP - 迴圈處理(4)
ramda forEach map -不同的語言提供不同的語法來處理迴圈,例如 for, while, until, do…while, do…until 等等。
但 ramda 只提供 forEach。
How
或許更多人更熟悉 map ,但相對的概念較為複雜,所以先比較一下 forEach 和 map 的簽名。
forEach :: (a -> *) -> [a] -> [a] map :: Functor f => (a -> b) -> f a -> f b
forEach 很好理解,有一個處理函式 (a -> *) 作用於輸入的陣列,並逐一處理每個元素。
後面的簽名 [a] -> [a] 告訴我們輸出的陣列等於輸入的陣列,在整個處理過程中不會改變陣列。
這些特性跟 js 中的 Array.prototype.forEach 有所不同。
js 的 forEach ,最後總是傳回 undefined,而在迭代期間若修改了陣列,則可能會跳過其他元素。
例如,總傳回 undefined ,
const ary = [1,2,3,4,5]
var result = ary.forEach((i)=> {
console.log(i)
return i // (1)
})
console.log(result)
// 1
// 2
// 3
// 4
// 5
// undefined
-
雖然
return但不會離開。
例如,因為在 forEach 內操作陣列,導致有些元素被跳過,
const ary = [1,2,3,4,5]
ary.forEach((i)=> {
if (i===2) ary.shift()
console.log(i)
})
// 1
// 2
// 4
// 5
而 forEach 也跟 for 不一樣,沒辦法中途離開,
例如,在 for 中 return 會中途離開。
const ary = [1,2,3,4,5]
for (let i=0; i < ary.length; i++) {
if (i===2) {
return
}
console.log(i)
}
// 1
// 2
了解 Ramda 對 forEach 的處理,來看看 map 跟他的不一樣。
再看一次簽名,
forEach :: (a -> *) -> [a] -> [a] map :: Functor f => (a -> b) -> f a -> f b
不同於 forEach 不會變動輸入的陣列元素,map 要求的是一個變換元素的函式 (a → b),所以可以視為 map 會將陣列中的每個元素轉換為另一個元素。
所以,下列是等效的。
func = a => b
forEach(func) = map(tap(func)) // (1)
-
tap是Ramda提供的一個函式,會將傳入的第二個參數原封不動的傳回。
上面的 map 簽名,有一個 ⇒(粗箭頭)這是表示對類型變量的約束。
他告訴我們 f 是一個函子 (Functor),後面出現的 f 都代表他是函子。
為什麼 map 要求的不是跟 forEach 一樣的是一個陣列 [a] ?
前面提過,陣列也是一個 functor 但沒解釋什麼是函子,這裡繼續不解釋。
因為,陣列也是一種函子,所以,上面的簽名有能特化為
map :: Array f => (a -> b) -> f a -> f b // 改寫為 map :: (a -> b) -> [a] -> [b]
這樣就跟 forEach 非常像了。
實際上, map 要求是一個函子,並傳回另一個函子。
陣列是函子,物件是函子,但字串不是函子。雖然字串能當陣列使用但最後輸出的是陣列而不是字串。
一般直覺比較不會想到的是,函式是函子。
但是把函式傳給 map 會是甚麼?
根據簽名,可以知道他的傳回會是一個函式。
模擬如下,
const f = (a) => f(a) // ~> b
const g = (x) => g(x) // ~> a
const func = map(f, g)
// func = (x) => f(g(x))
這看起來像甚麼?
在 Ramda 中有兩個等效的函式,分別是
map(f, g) = o(f, g)
或
map(f, g) = compose(f, g)
從實作的角度來說, o 的效率是最高的,但他只接受兩個參數。
而 compose 雖能接受多個參數,但卻不是自動 curry 的。
也就是可以寫成 o(f)(g) ,但不能寫成 compose(f)(g)。
map 同樣是自動 curry 也接受兩個參數,所以與 o 的形式更像。
比較如下,
map :: Functor f => (a -> b) -> f a -> f b o :: (b -> c) -> (a -> b) -> a -> c
整理一下,因為函式也是函子。 o 可以改寫如下,
o :: (b -> c) -> (a -> b) -> (a -> c) // (1) o :: Functor f => (b -> c) -> f b -> f c // (2)
-
先將最後的輸出改寫。
-
使用
functor替代function。
這樣看來兩個就一模一樣了。
map :: Functor f => (a -> b) -> f a -> f b o :: Functor f => (b -> c) -> f b -> f c