The Issue of the Day Before

跟著 Ramda 學 FP - 迴圈處理(4)

ramda -

不同的語言提供不同的語法來處理迴圈,例如 for, while, until, do…​while, do…​until 等等。 但 ramda 只提供 forEach

How

或許更多人更熟悉 map ,但相對的概念較為複雜,所以先比較一下 forEachmap 的簽名。

forEach :: (a -> *) -> [a] -> [a]
map :: Functor f => (a -> b) -> f a -> f b

forEach 很好理解,有一個處理函式 (a -> *) 作用於輸入的陣列,並逐一處理每個元素。 後面的簽名 [a] -> [a] 告訴我們輸出的陣列等於輸入的陣列,在整個處理過程中不會改變陣列。

這些特性跟 js 中的 Array.prototype.forEach 有所不同。 jsforEach ,最後總是傳回 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
  1. 雖然 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 不一樣,沒辦法中途離開,

例如,在 forreturn 會中途離開。

const ary = [1,2,3,4,5]
for (let i=0; i < ary.length; i++) {
  if (i===2) {
    return
  }
  console.log(i)
}
// 1
// 2

了解 RamdaforEach 的處理,來看看 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)
  1. tapRamda 提供的一個函式,會將傳入的第二個參數原封不動的傳回。

上面的 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)
  1. 先將最後的輸出改寫。

  2. 使用 functor 替代 function

這樣看來兩個就一模一樣了。

map :: Functor f => (a -> b) -> f a -> f b
  o :: Functor f => (b -> c) -> f b -> f c
閱讀在雲端