點燈坊

失くすものさえない今が強くなるチャンスよ

Ramda 初體驗

Sam Xiao's Avatar 2019-10-03

一直很羨慕 F# 的 List Module 提供了豐富的 Function,而 ECMAScript 的 Array.prototype 卻只提供有限的 Function 可用,因此無法完全發揮 FP 威力。但這一切終於得到解決,Ramda 擁有豐富的 Function,且很容易自行開發 Function 與 Ramda 整合實現 Function Pipeline。

Version

Ramda 0.27.1

Imperative

let data = [
  { title: 'fp in JavaScript', price: 100 },
  { title: 'rxJS in Action', price: 200 },
  { title: 'speaking JavaScript', price: 300 }
]

let f = (x, a) => {
  let result = []
  
  for (let i = 0; i < a.length; i++) {
    if (a[i].price === x) {
      result.push(a[i].title)
    }
  }

  return result
}

f(300, data) // ?

很簡單的需求,data 有各書籍資料,包含 titleprice,我們想得到 price300 的資料,且只要 title 即可。

若使用 Imperative 寫法,我們會使用 for loop,先建立要回傳的 result Array,由 if 去判斷 price === 300,再將符合條件的 title 寫入 result Array,最後再回傳。

ramda000

Array.Prototype

let data = [
  { title: 'fp in JavaScript', price: 100 },
  { title: 'rxJS in Action', price: 200 },
  { title: 'speaking JavaScript', price: 300 }
];

let f = (v, a) => 
  a
  .filter(x => x.price === v)
  .map(x => x.title);

f(300, data) // ?

熟悉 FP 的讀者會很敏感發現,這就是典型 filter()map() 而已,我們可直接使用 ECMAScript 在 Array.prototype 內建的 filter()map() 即可完成。

ramda001

Ramda

import { pipe, filter, map } from 'ramda'

let data = [
  { title: 'fp in JavaScript', price: 100 },
  { title: 'rxJS in Action', price: 200 },
  { title: 'speaking JavaScript', price: 300 }
]

let f = v => pipe(
  filter(x => x.price === v),
  map(x => x.title)
)

f(300)(data); // ?

Ramda 身為 functional library,內建 filter()map() 自然不在話下。

我們可想像 data 先經過 filter() 處理,再將結果傳到 map(),如此 Function Pipeline 過程可使用 pipe() 加以整合。

至於 filter()map() 要傳入的 callback,可使用 arrow function。

我們發現 f()a argument 不見了,稱為 Point-free,讓程式碼更精簡

ramda002

Point-free

import { pipe, filter, map, propEq, prop } from 'ramda'

let data = [
  { title: 'fp in JavaScript', price: 100 },
  { title: 'rxJS in Action', price: 200 },
  { title: 'speaking JavaScript', price: 300 }
]

let f = x => pipe(
  filter(propEq('price', x)),
  map(prop('title'))
)

f(300)(data) // ?

既然 f() 能 Point-free,filter()map() 的 callback 也能 point-free 嗎 ?

  • proEq('price', v) 產生 x => x.price === v
  • prop('title') 產生 x => x.title

如此 callback 也 Point-free 了,不只更精簡,且可讀性更高。

ramda003

User Function

import { pipe, filter, map, propEq, prop } from 'ramda'

let data = [
  { title: 'fp in JavaScript', price: 100 },
  { title: 'rxJS in Action', price: 200 },
  { title: 'speaking JavaScript', price: 300 }
]

let capitalize = x => x[0].toUpperCase() + x.slice(1);

let f = x => pipe(
  filter(propEq('price', x)),
  map(prop('title')),
  map(capitalize)
)

f(300)(data) // ?

眼尖的讀者會發現結果的 speaking JavaScripts 為小寫,原始的資料就是如此,但 s 為大寫較符合英文閱讀習慣。

因此我們可以自行寫一個 capitalize() 將第一個字母變成大寫。

在 Ramda 要成為能夠 pipe() 的條件很簡單,只要 function 是單一 argument,且是 pure function 即可。

ramda004

Point-free

import { pipe, filter, map, propEq, prop, adjust, join, toUpper } from 'ramda'

let data = [
  { title: 'fp in JavaScript', price: 100 },
  { title: 'rxJS in Action', price: 200 },
  { title: 'speaking JavaScript', price: 300 }
]

let capitalize = pipe(
  adjust(0, toUpper),
  join('')
)

let f = x => pipe(
  filter(propEq('price', x)),
  map(prop('title')),
  map(capitalize)
)

f(300)(data) // ?

第 9 行

let capitalize = pipe(
  adjust(0, toUpper),
  join('')
)

capitalize() 亦可以 Point-free 實現:

  • adjust(0, toUpper):將 String 視為 Char Array,將 char[0]toUpper() 轉大小寫,回傳為 Array
  • join(''):將 Array 轉成 String

ramda005

Conclusion

  • Ramda 提供了 FP 該有的 function,不再侷限於 Array.prototype
  • Ramda 可很容易擴充 function,不再擔心污染 Array.prototype
  • Ramda 使用 pipe(),以 Function Pipeline 方式將資料逐步傳遞,它與 compose() 的 Function Composition 剛好是一體兩面
  • Point-free 也是 Ramda 一大特色,讓程式碼更精簡,可讀性更高