使用更 FP 風格開發 ECMAScript

一直很羨慕 F# 的 List module 提供了豐富的 Operator,而 ECMAScript 的 Array.prototype 卻只提供有限的 Operator 可用,因此無法完全發揮 FP 威力。

但這一切終於得到解決,Ramda 擁有豐富的 Operator,且很容易自行開發 Operator 與 Ramda 整合使用。

Version


WebStorm 2018.3.3
Quokka 1.0.134
Ramda 0.26.1

Imperative


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const books = [
{year: 2016, title: 'functional Programming in JavaScript'},
{year: 2017, title: 'rxJS in Action'},
{year: 2014, title: 'speaking JavaScript'},
];

const titlesForYear = (year, data) => {
const result = [];
for(let i = 0; i < data.length; i++) {
if (data[i].year === year) {
result.push(data[i].title);
}
}

return result;
};

const result = titlesForYear(2016, books);
console.log(result);

很簡單的需求,books array 有各書籍資料,包含 yeartitle,我們想得到 2016 年份的書籍資料,且只要 書名 即可。

若使用 Imperative 寫法,我們會使用 for loop,先建立要回傳的 result array,由 if 去判斷 2016 年,再將符合條件的 書名 寫入 result array,最後再回傳。

ramda000

Array.Prototype


1
2
3
4
5
6
7
8
9
10
11
12
13
const books = [
{year: 2016, title: 'functional Programming in JavaScript'},
{year: 2017, title: 'rxJS in Action'},
{year: 2014, title: 'speaking JavaScript'},
];

const titlesForYear = (data, year) =>
data
.filter(x => x.year === year)
.map(x => x.title);

const result = titlesForYear(books, 2016);
console.log(result);

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

ramda001

Ramda


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { filter, map, pipe } from 'ramda';

const books = [
{year: 2016, title: 'functional Programming in JavaScript'},
{year: 2017, title: 'rxJS in Action'},
{year: 2014, title: 'speaking JavaScript'},
];

const forYear = year => x => x.year === year;
const onlyTitle = x => x.title;
const titlesForYear = (year, data) =>
pipe(
filter(forYear(year)),
map(onlyTitle),
)(data);

const result = titlesForYear(2016, books);
console.log(result);

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

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

至於 filter()map() 要傳入的 callback,當然也可以直接使用 Arrow Function,不過在 Ramda 會習慣將 callback 也建立 function,如此可讀性較高。

ramda002

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { filter, map, compose } from 'ramda';

const books = [
{year: 2016, title: 'functional Programming in JavaScript'},
{year: 2017, title: 'rxJS in Action'},
{year: 2014, title: 'speaking JavaScript'},
];

const forYear = year => x => x.year === year;
const onlyTitle = x => x.title;
const titlesForYear = (year, data) =>
compose(
map(onlyTitle),
filter(forYear(year)),
)(data);

const result = titlesForYear(2016, books);
console.log(result);

pipe()由左向右,當然你也可以使用 compose()由右向左

至於該用 pipe()compose() 都可以,但本質都是 組合 function,看哪種寫法你的思考較順。

Ramda 風格有個特色:會 組合 小 function 成為 大 function,所以在 Ramda 幾乎看不到 {},最後一個 function 會使用 pipe()compose()小 function 組合起來

ramda003

User Operator


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { filter, map, pipe } from 'ramda';

const books = [
{year: 2016, title: 'functional Programming in JavaScript'},
{year: 2017, title: 'rxJS in Action'},
{year: 2014, title: 'speaking JavaScript'},
];

const forYear = year => x => x.year === year;
const onlyTitle = x => x.title;
const capitalize = data => map(x => x[0].toUpperCase() + x.slice(1), data);
const titlesForYear = (year, data) =>
pipe(
filter(forYear(year)),
map(onlyTitle),
capitalize,
)(data);

const result = titlesForYear(2016, books);
console.log(result);

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

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

在 Ramda 要成為能夠 pipe()compose() 條件很簡單,只要 function 是單一 parameter,且是 Pure Function 即可。

ramda004

Conclusion


  • Ramda 提供了 FP 該有的 operator,不再侷限於 Array.prototype 有限的 operator
  • Ramda 可以很容易的擴充 operator,不再擔心污染 Array.prototype
  • Ramda 使用 pipe()compose(),觀念上更接近 FP 的 Compose Function

Reference


Ramda, filter()
Ramda, map()
Ramda, compose()
Ramda, pipe()