實務上為了簡化 User 操作,我們希望輸入 Keyword 後,能對 Array 內 Object 所有 Property 都進行檢查,只要包含此 Keyword,就會顯示該 Object,這該如何實現呢 ?
Version
macOS Mojave 10.14.5
VS Code 1.37.0
Quokka 1.0.240
ECMAScript 2017
Ramda 0.26.1
Functional
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
];
// fn :: String -> [a] -> [a]
let fn = keyword => arr => arr.filter(
x => x.title.includes(keyword.trim()) ||
x.price.toString().includes(keyword.trim())
);
fn('FP')(data); // ?
data
內的 object 只有 title
與 price
兩個 property,若 title
與 price
內任何一個 property 包含 keyword
,則顯示該 object。
建立 fn()
,argument 為 keyword
與 arr
,直覺會使用 Array.prototype.filter()
處理。
由 x.title
與 x.price
取得資料,透過 String.prototype.includes()
判斷是否包含 keyword
,若有則回傳 true
,否則回傳 false
,因為只要任何一個 property 成立皆可,所以使用 ||
串起來。
由於 price
為 number,必須先經過 toString()
轉成 string 後才能使用 includes()
。
為了讓 空白
顯示所有資料,特別使用 keyword.trim()
將前後空白都清除。
Refactoring
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
];
// fn :: String -> [a] -> [a]
let fn = keyword => arr => arr.filter(x => Object.values(x).join(' ').includes(keyword.trim()));
fn('FP')(data); // ?
之前方法雖然可行,但缺點是 object 有幾個 property,||
就要寫幾次,是否有更通用的方式能檢查 無限 property
呢 ?
若能將所有 property 取出轉成 string,只要 includes()
判斷 string 即可。
- 使用 ES2017 的
Object.values()
只取 object 的 property value 部分成為 array - 再使用
Array.prototype.join()
將 array 轉成 string - 最後使用
String.prototype.includes()
判斷 keyword 是否存在
如此無論 object 有多少 property,寫法都不用改變。
Ramda
import { filter, values, join, includes, trim, compose } from 'ramda';
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
];
let pred = keyword => compose(
includes(keyword),
join(' '),
values
);
// fn :: String -> [a] -> [a]
let fn = keyword => filter(pred(trim(keyword)));
fn('FP')(data); // ?
fn()
目前尚有 keyword
與 arr
兩個 argument,我們嘗試使用 Ramda 的 filter()
將 arr
point-free。
filter()
的 predicate 也有 keyword
與 x
兩個 argument,也嘗試將其 point-free。
其實由 Object.values(x).join(' ').includes()
已經看出端倪,就是先執行 Object.values()
,再執行 join()
,最後執行 includes()
,以 FP 角度就是將 values()
、join()
與 includes()
組合出新的 pred()
。
Point-free
import { filter, values, join, includes, trim, compose, useWith, identity } from 'ramda';
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
];
let pred = useWith(
includes, [
identity,
compose(join(' '), values)
]
);
// fn :: String -> [a] -> [a]
let fn = useWith(
filter, [
compose(pred, trim),
identity
]
);
fn('FP')(data); // ?
可否連 keyword
也 point-free 呢 ?
其實原本 fn()
與 pred()
都是兩個 argument,經觀察得知:
fn()
最後執行為filter()
,需要 transformer function 運算後才將結果傳給filter()
,因此適合使用useWith()
point-freepred()
最後執行為includes()
,需要 transformer function 運算後才將結果傳給includes()
,也適合使用useWith()
point-free
Conclusion
- FP 可先從 ECMAScript 原生的
Array.prototype
、String.prototype
學習,只是寫法沒那麼漂亮而已,但觀念都是一樣的 - 接下來可嘗試將 data 使用 Ramda 加以 point-free
- 若要連其他 argument 也 point-free,則要使用
useWith()
、converge()
、chain()
…等高級 function,若覺得有難度,可先練習到能將 data 部分 point-free 即可,不用強求連其他 argument 也要 point-free,這需要時間練習
Reference
MDN, Array.prototype.filter()
MDN, String.prototype.includes()
MDN, String.prototype.trim()
MDN, Array.prototype.join()
MDN, Number.prototype.toString()
MDN, Object.values()
Ramda, filter()
Ramda, includes()
Ramda, trim()
Ramda, join()
Ramda, values()
Ramda, compose()
Ramda, useWith()
Ramda, identity()