點燈坊

學而時習之,不亦悅乎

使用 call() 動態產生 Converging Function

Sam Xiao's Avatar 2019-07-07

Ramda 的 call() 乍看很不起眼,但若搭配 converge() 之後,就能動態產生 Converging Function。

Version

macOS Mojave 10.14.5
VS Code 1.32.3
Quokka 1.0.195
Ramda 0.26.1

call()

import { call, add } from 'ramda';

call(add, 1, 2); // ?

最簡單的 call(),第一個 argument 為 function,第二個 argument 之後為該 function 的 argument list,相當於執行 add(1, 2)

call()
(*… → a),*… → a
執行第一個 argument 的 function,並將之後 argument 當作該 function 的 argument list

(*… → a):任意 function

*…:任意 argument list

a:回傳 function 執行的結果

call000

Function

let product = {
  discount: 5000,
  price: 30000
};

// calPrice :: (Number, Number) -> Number
let calPrice = (discount, price) => price - discount - 1000;

// finalPrice :: Number -> Number
let finalPrice = product => calPrice(product.discount, product.price);

finalPrice(product); // ?

calPrice() 傳入 discountprice argument,會計算出折扣後價錢。

fn() 則傳入 product object,再呼叫 calPrice()

call003

Point-free

import { converge, prop } from 'ramda';

let product = {
  discount: 5000,
  price: 30000
};

// calPrice :: (Number, Number) -> Number
let calPrice = (discount, price) => price - discount - 1000;

// finalPrice :: Number -> Number
let finalPrice = converge(
  calPrice, [
    prop('discount'),
    prop('price')
  ]
);

finalPrice(product); // ?

首先將 finalPrice() point-free。

11 行

// finalPrice :: Number -> Number
let finalPrice = converge(
  calPrice, [
    prop('discount'),
    prop('price')
  ]
);

finalPrice() 的 argument 為 product object,包含 discountprice property,分別傳入 折扣價錢

最後要執行的是 calPrice(),分別有 discountprice 兩個 argument。

由於傳入 finalPrice() 的是 object,因此勢必要先使用 prop() 拆解後才能傳給 calPrice() 執行。

這正是使用 finalPrice() 時機,將 calPrice() 當作 converging function,將 prop() 當作 branching function。

就功能而言沒問題,唯 calPrice() 並非 point-free,是否有繼續優化空間呢 ?

call001

import { converge, prop, call, add, pipe, subtract, flip, compose } from 'ramda';

let product = {
  discount: 5000,
  price: 30000
};

// calPrice :: (Number, Number) -> Number
let calPrice = pipe(
  add(1000),
  flip(subtract)
);

// getDiscount :: Object -> Number
let getDiscount = compose(calPrice, prop('discount'));

// getPrice :: Object -> Number
let getPrice = prop('price');

// finalPrice :: Number -> Number
let finalPrice = converge(
  call, [
    getDiscount,
    getPrice
  ]
);

finalPrice(product); // ?

由於 calPrice() 有兩個 argument:discountprice,基於 currying 特性,可將 calPrice() 視為由 discount 產生的 function,其 argument 只剩下 price

第 8 行

// calPrice :: (Number, Number) -> Number
let calPrice = pipe(
  add(1000),
  flip(subtract)
);

calPrice() 最直覺會想用 pipe(),多減 1000 元就相當於 discount 多加 1000 元,所以先套用 add(1000)discount1000,再傳到下一個 subtract()

subtract() 第一個 argument 為 被減數,而 add(1000)減數,因此必須使用 flip()subtract() 的 signature 反轉後才能使用。

我們發現 calPrice() 已經 point-free 了。

21 行

// finalPrice :: Number -> Number
let finalPrice = converge(
  call, [
    getDiscount,
    getPrice
  ]
);

由於 calPrice()discount 產生,因此 converge() 的 converging function 就不能再是 calPrice(),而要改用 call(),如此才會執行第一個 branching function。

第一個 branching function getDiscount() 以取得 discount 為目標。

第二個 branching function getPrice() 以取得 price 為目標,再傳入第一個 branching function 執行,這是因為 call() 的緣故。

14 行

// getDiscount :: Object -> Number
let getDiscount = compose(calPrice, prop('discount'));

從 object 取得 discount property,然後透過 calPrice() 計算。

17 行

// getPrice :: Object -> Number
let getPrice = prop('price');

從 object 取得 price property。

call002

Conclusion

  • 當 converging function 並非固定,而是由其他 function 動態產生時,可搭配 call(),使其在 branching function 先動態產生 function,再由 call() 呼叫執行之

Reference

Ramda, call()
Ramda, add()
Ramda, converge()
Ramda, add()
Ramda, pipe()
Ramda, subtract()
Ramda, flip()