點燈坊

學而時習之,不亦悅乎

Ramda 初體驗

Sam Xiao's Avatar 2019-07-10

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

Version

macOS Mojave 10.14.5
VS Code 1.35.1
Quokka 1.0.232
Ramda 0.26.1

Imperative

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

// getBooks :: Number -> [a] -> [b]
let getBooks = price => arr => {
  let result = [];

  for(let i = 0; i < arr.length; i++) {
    if (data[i].price === price) {
      result.push(data[i].title);
    }
  }

  return result;
};

getBooks(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 }
];

// getBooks :: Number -> [a] -> [b]
let getBooks = price => arr => arr
  .filter(x => x.price === price)
  .map(x => x.title);

getBooks(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 }
];

// getBooks :: Number -> [a] -> [b]
let getBooks = price => pipe(
  filter(x => x.price === price),
  map(x => x.title)
);

getBooks(300)(data); // ?

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

我們可先用 pipe()filter()map() 組合 出新的 fn(),再將 data 傳入。

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

我們發現 fn()arr parameter 不見了,稱為 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 }
];

// getBooks :: Number -> [a] -> [b]
let getBooks = price => pipe(
  filter(propEq('price', price)),
  map(prop('title'))
);

getBooks(300)(data); // ?

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

  • proEq('price', price) 產生 x => x.price === price
  • 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 }
];

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

// getBooks :: Number -> [a] -> [b]
let getBooks = price => pipe(
  filter(propEq('price', price)),
  map(prop('title')),
  map(capitalize)
);

getBooks(300)(data); // ?

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

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

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

ramda004

Conclusion

  • Ramda 提供了 FP 該有的 function,不再侷限於 Array.prototype
  • Ramda 可很容易擴充 function,不再擔心污染 Array.prototype
  • Ramda 使用 pipe(),觀念上更接近 FP 的 function composition
  • Point-free 也是 Ramda 一大特色,讓程式碼更精簡,可讀性更高

Reference

MDN, filter()
MDN, map()
Ramda, filter()
Ramda, map()
Ramda, pipe()
Ramda, prop()
Ramda, propEq()