跟著 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