將參數先經過 Branching Function 處理,再交給 Converging Function 執行

既然談到了 useWith(),就不得不談談它的孿生兄弟:converge()useWith() 是 array 有幾個 function,則新 function 就有幾個參數;而 converge() 是新 function 的參數個數由 array 內 function 參數個數決定。

Version


WebStorm 2018.3.3
Quokka 1.0.136
Ramda 0.26.1

Converge()


converge()
((x1, x2, …) → z) → [((a, b, …) → x1), ((a, b, …) → x2), …] → (a → b → … → z)
建立一個新 funtion,其 arity 由 array 內 function 的最大 arity 決定
會先經過 branching function 運算,再將結果傳給 converging function 執行

((x1, x2, …) → z):converging function,最後要收斂執行的 function

[((a, b, …) → x1), ((a, b, …) → x2), …]:為一 array,其中 ((a, b, …) → x1) 稱為 branching function,converging function 的 arity 會與 array 內 branching function 數量相等,新 function 的所有參數會先經過各 branching function 執行,結果才會傳給 converging function 執行

(a → b → … → z):回傳新 function,且是 curried function,其 arity 由 branching function 中最大 arity 決定

單一參數


1
2
3
4
5
6
7
8
const product = {
name: 'iPhone',
price: 30000,
discount: 5000,
};

const finalPrice = product => product.price - product.discount;
console.log(finalPrice(product));

當只有一個參數 (通常為 object),接下來要做的就是要讀取其 property,正常會使用 .,但 . 必須搭配參數,勢必無法 Point-free。

converge000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { converge, subtract, prop } from 'ramda';

const product = {
name: 'iPhone',
price: 30000,
discount: 5000,
};

const finalPrice = converge(
subtract,
[
prop('price'),
prop('discount')
]);

console.log(finalPrice(product));

從另外一個角度觀察,我們發現最終要執行 -,且 product.priceproduct.discount 都要讀取 property,且為單一參數 product,符合 converge() 格式需求。

- 在 Ramda 相當於 subtract(),而 . 在 Ramda 相當於 prop(),因此可用 converge() 改寫。

若改用 converge(),可先透過 branching function prop() 得到 property 資料,在各自傳給 subtract() 的第 1 個與第 2 個參數。

由於 converge() 會回傳 subtract() 回傳值為參數的的 function,因此可將原本 product 參數 Point-free 掉。

參數為 objectconverge() 最常見的用法,可視為 pattern 使用

converge003

converge001

多個參數


1
2
3
4
5
6
7
8
9
10
11
12
13
14
const length = (p1, p2) => {
const x2 = Math.pow(Math.abs(p1.x - p2.x), 2);
const y2 = Math.pow(Math.abs(p1.y - p2.y), 2);
const sum = x2 + y2;
return Math.sqrt(sum);
};

const fn = (x, y) => {
const p1 = {x: x - 5, y: y - 5};
const p2 = {x: x + 5, y: y + 5};
return length(p1, p2);
};

console.log(fn(1, 1))

length() 主要計算兩點之間的距離,用的是數學先平方和再開根號,但 lenght() 不是重點。

重點是 fn() 會先將傳入的 (x, y) 的 x、 y 各 -5p1,再各 +5p2,最後再將 p1p2 傳進 length()

fn() 明顯需要參數 (x, y),所以沒有 Point-free。

converge002

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

const length = (p1, p2) => {
const x2 = Math.pow(Math.abs(p1.x - p2.x), 2);
const y2 = Math.pow(Math.abs(p1.y - p2.y), 2);
const sum = x2 + y2;
return Math.sqrt(sum);
};

const shift = v => applySpec({
x: add(v),
y: add(v),
});

const fn = converge(
length,
[shift(-5), shift(5)],
);

console.log(fn(1, 1))

從另外一個角度觀察,我們發現最終要執行 length(),且 (x, y) 要先運算成為 p1p2,才將結果傳給 length(),而 (x, y)一組參數 同時產生 p1p2,符合 converge() 格式需求。

第 10 行

1
2
3
4
const shift = v => applySpec({
x: add(v),
y: add(v),
});

由於 (x, y) 要先運算成為 p1p2,先將其抽成 shift() HOF,由於回傳為 object,適合使用 Ramda 的 applySpec(),注意 spec object 有幾個 property,其新 function 的 arity 就有幾個。

applySpec()
{k: ((a, b, …, m) → v)} → ((a, b, …, m) → {k: v})
依照 spec object 建立新 object

1
2
3
4
5
6
const fn = converge(
length,
[shift(-5), shift(5)],
);

console.log(fn(1, 1))

如此就可使用 converge(),將 converging function 設定為 length(),branching function 則分別為 shift(-5)shift(5)

至於 fn() 的 arity 呢 ? 由於 applySpec() 的 spec object 有兩個 property,因此 fn() 的 arity 是 2,也就是一樣是 (x, y)

Converge004

Converge005

Conclusion


  • converge() 看似複雜,而其使用時機是當 一個參數,或 一組參數 同時被數個 function 使用,最後再將結果交給同一個 function 執行時,可使用 converge() 整合這些 function,並將最後執行的 function 成為 converging function,而使用參數的數個 function 成為 branching function
  • converge()useWith() 看似相似,但 converge() 是每個 branching function 處理所有參數,而 useWith() 是每個 function 各自處理參數
  • converge() 的新 function 只有一個參數時,就特別容易與 pipe()compose() 一起合作
  • converge() 經常搭配 一個參數,但事實上也能搭配 一組參數 使用

Reference


Ramda, converge()
Ramda, applySpec()
Ramda, subtract()
Ramda, prop()