點燈坊

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

如何使 Callback Point-free ?

Sam Xiao's Avatar 2019-08-09

若只求結果正確,Ramda 其實不難,而是難在 Callback 如何 Point-free,很多看似簡單,卻會讓你想破頭,本文整理出幾個常見的技巧,可以視為 Pattern 使用。

Version

macOS Mojave 10.14.5
VS Code 1.35.0
Quokka 1.0.224
Ramda 0.26.1

One Argument in Callback

__

import { map } from 'ramda';

let data = [1, 2, 3];

let bookLut = {
  1: 'FP in JavaScript',
  2: 'RxJS in Action',
  3: 'Speaking JavaScript',
};

let usrFn = lut => map(x => lut[x]);

usrFn(bookLut)(data); // ?

data 為 book 的 id,而實際 id書名 的 lookup table 在 bookLut object,若需求只要顯示 書名,我們會使用 map()

若以需求而言,這樣寫就很棒了,唯 map function 不是 point-free,還帶著一個 x argument,若能將 x 也拿掉就更棒了。

flip002

import { map, prop, __ } from 'ramda';

let data = [1, 2, 3];

let bookLut = {
  1: 'FP in JavaScript',
  2: 'RxJS in Action',
  3: 'Speaking JavaScript',
};

let usrFn = lut => map(prop(__, lut));

usrFn(bookLut)(data); // ?

回顧一下 map()prop() 的 signature:

map()
(a -> b) -> [a] -> [b]
將 a 格式 array 轉換成 b 格式 array,且筆數不變

map() 的 map function 必須是 a -> b

prop()
s -> {s: a} -> a | undefined
傳入 key 與 object,回傳 value

prop() 原本的設計是當你只傳入 s key 時,回傳 {s: a} -> a,這剛好與 map function 的 signature 一樣,可將原本x argument 拿掉成 point-free。

但很不幸的,我們需求反而是要對 prop() 傳入 {s: a},也就是 object 部分,而非 key,這在實務上經常遇到,很多人在這裡卡關不知道怎麼繼續了。

Ramda 提供了 __(),能讓我們延遲給參數,讓 prop() 依然回傳 {s: a} -> a,原本要傳入的 key 先用 __() 取代,等 map function 傳進 x 參數。

flip003

flip()

import { map, prop, flip } from 'ramda';

let data = [1, 2, 3];

let bookLut = {
  1: 'FP in JavaScript',
  2: 'RxJS in Action',
  3: 'Speaking JavaScript',
};

let usrFn = lut => map(flip(prop)(lut));

usrFn(bookLut)(data); // ?

Ramda 另外提供了 flip(),可以將 function 的前兩個 argument 順序做調換,讓原本無法 point-free 的 function 變成可 point-free。

flip()
((a, b, c, …) → z) → (b → a → c → … → z)
將 function 的前兩個參數順序調換,以配合 Point-free

與其使用 prop(__, bookLut),若能將變成 prop(bookLut, __) 那就太好了,這樣 data 可以放在最後一個 argument,可安心做 point-free。

先使用 flip(prop) 產生新 function,使其 signature 成為 {s: a} -> s -> a | undefined,如此 s key 成為第二個 argument,就能直接 point-free 掉 x

flip004

Two Argument in Callback

import { includes, map, pickBy } from 'ramda';

let data = [
  { id: 1, title: 'FP in JavaScript', year: 2016 },
  { id: 2, title: 'RxJS in Action', year: 2017 },
  { id: 3, title: 'Speaking JavaScript', year: 2014 },
];

let usrFn = map(pickBy((v, k) => includes('i', k)));

console.dir(usrFn(data));

當 callback 有兩個 argument,且要 point-free 的 data 又在第二個 argument 時。

pickBy() 原本的設計,就是希望你提供 (v, k) → Boolean 這種 signature 的 callback,而我們的需求是顯示 key 的部分包含 i 的所有 object 與 property,因此 idtitle 兩個 property 都會被 pick 出來。

pickBy()
((v, k) → Boolean) → {k: v} → {k: v}
根據傳入的 predicate function,回傳新的符合條件 property 的 object

flip000

若以需求而言,這樣就打完收工了。

但若仔細看,會發現 pickBy() 的 callback 還有 (v, k) 並沒有 Point-free,是否有繼續優化的空間呢 ?

import { map, pickBy, includes, flip } from 'ramda';

let data = [
  { id: 1, title: 'FP in JavaScript', year: 2016 },
  { id: 2, title: 'RxJS in Action', year: 2017 },
  { id: 3, title: 'Speaking JavaScript', year: 2014 },
];

let usrFn = map(pickBy(flip(includes('i'))));

console.dir(usrFn(data));

使用 flip() 之後,vk 都不見了,所有 function 都 point-free,且結果依然正確。

flip001

pickBy() callback 的 signature 只有一個 k argument:

pickBy(k => includes('i', k))

我們能夠輕易的 point-free 沒問題:

pickBy(includes('i'));

但偏偏目前 pickBy() callback 為兩個 argument (v, k),因此無法 point-free 將 (v, k) 都拿掉。

(v, k) => includes('i', k)

所以我們希望能產生新的 function 取代 (v, k) => includes('i', k)

includes()
a -> [a] -> Boolean
若 value 存在於 array 內時,則傳回 true,否則傳回 false

includes('i') 時,回傳 [a] -> Boolean,我們也可以視為

([a], __) -> boolean

其中 [a] 就是我們的 k__ 在此不是 Ramda 的 __(),把它當成一個可有可無的參數 placeholder 即可,表示多一個參數也無妨,但此時 ([a], __) 對應的是 pickBy()(v, k),也就是我們的 k 對應到的是 v,會變成判斷 property value 是否包含 i,這不是我們要的。

flip(includes('i'));
// (__, [a]) -> boolean

透過 flip() 翻轉 includes('i') 的 argument 後,(__, [a]) 對應到 (v, k),也就是我們的 k 對應到 callback 的 k argument,正是我們要的 property key,因此可將 k => includes('i', k) 完全用 flip(includes('i')) 取代,順利達成 point-free。

Conclusion

  • 本文共介紹 2 種 pattern,當 callback 只有一個 argument 時,使用 __flip() 取代 callback,可視程式碼可讀性自行靈活運用
  • 當 callback 時有兩個參數時,若我們要 point-free 的 data 是第一個 argument,可以無視第二個 argument 直接 point-free,但若要 point-free 的 data 是第二個 argument 時,只好先將 data flip() 成第二個 argument,如此才能 point-free 取代 callback

Reference

Ramda, flip()
Ramda, map()
Ramda, pickBy()
Ramda, includes()
Ramda, __()