iT邦幫忙

2022 iThome 鐵人賽

DAY 12
2

前言

因為 bind() 這個函式不只可以用來綁定 this,也可以做到像 Currying(柯里化)的效果,所以獨立一個篇章來說明,同時也會介紹柯里化。

不過在那之前,我們來複習一下 Curring 以及和它十分相似的 Partial Application。


Curring(柯里化)介紹

將一個可傳入多個參數的函式拆分成多個連續的函式,其中的每個函數接受單一參數,並回傳另一個函數接受下一個參數。

舉個例子,這是還沒有柯里化的函式

const addNums = (a, b, c) => a + b + c;

console.log(addNums(2, 3, 5)) // 10

而這是柯里化後的函式

const addCurry = (a) => (b) => (c) => a + b + c;

console.log(addCurry(2)(3)(5)) // 10

Partial Application(或稱偏函數)

中文看過有人把它翻做偏函數,所以這篇都以這個名稱稱呼。

偏函數的概念就是將一個擁有多個參數的函式拆分成多個較少參數的函式,讓函式一次只能調用局部的參數,以減少參數個數。

偏函數簡單說就是某一個函式會先固定一些參數的值,然後回傳一個函式,在新函式中再加入其他的參數。

const addPartial = (a, b) => (c, d, e) => a + b + c + d + e;

const partial = addPartial(1, 2);
const result = partial(3, 4, 5);
console.log(result); // 15

這兩個東西十分的像,主要差異就是在參數個數,柯里化每次呼叫拆分後的函式都是傳入一個參數,偏函數則為一個至多個,相同的地方則是透過閉包的機制去儲存變數。


bind() 使用於柯里化 & 偏函數

接著我們來看透過 bind() 達成柯里化的範例:

剛剛的柯里化的範例我們可以這樣用它

const addCurry = (a) => (b) => (c) => a + b + c;

const addByDefaultTwo = addCurry(2);

const addAgain = addByDefaultTwo(4);
console.log(addAgain(5)); // 11

const addAgain2 = addByDefaultTwo(6);
console.log(addAgain2(7)); // 15

像這個情況,addByDefaultTwo 儲存了預設的 2 當作其中一個參數的值,未來如果有多個函式都需要傳入 2 當作參數,那 addByDefaultTwo 就可以拿來重用。

bind() 在這邊也可以創造同樣的效果出來,像以下的這個範例,addNums 雖然不是經過柯里化的函式,但透過 bind() 也可以創造出一次傳一個參數的效果,而且 addByDefaultTwo 和上個範例一樣,也有儲存參數的效果。

另外要在 bind() 傳入多個參數,做到 Partial Application 當然也是可以喔~

const addNums = (a, b, c) => a + b + c;

const addByDefaultTwo = addNums.bind(this, 2);

const addAgain = addByDefaultTwo.bind(this, 4);
console.log(addAgain(5)); // 11

const addAgain2 = addByDefaultTwo.bind(this, 6);
console.log(addAgain2(7)); // 15

另一個 Partial Application + bind() 的範例:

function askPassword(ok, fail) {
  let password = prompt("Password?", '');
  if (password == "rockstar") ok();
  else fail();
}

let user = {
  name: 'John',

  login(result) {
    alert( this.name + (result ? ' logged in' : ' failed to log in') );
  }
};

askPassword(user.login.bind(user, true), user.login.bind(user, false));
// 也等同於
// askPassword(() => user.login(true), () => user.login(false));

柯里化/偏函數優點

由上面的範例可以知道透過柯里化可以避免去呼叫相同參數的函式,不同抽象層級的函式,只提供特定參數做處理。

若原本有很多個參數的函式透過拆分也可以讓程式碼更乾淨、重用性更高。

另一個優點是容易讓多個函式進行組合(Composition),牽涉到比較多的 Functional Programming 的觀念,之後有機會再寫文章來分享。


柯里化範例

以下舉一些和柯里化相關的範例和問題:

範例1.

假如有這樣的一筆資料:

const members = [
  {
    id: 1,
    name: 'Steve',
    age: 25,
    memberLevel: 'sliver',
  },
  {
    id: 2,
    name: 'John',
    age: 20,
    memberLevel: 'bronze',
  },
  {
    id: 3,
    name: 'Jerry',
    age: 35,
    memberLevel: 'gold',
  },
  {
    id: 4,
    name: 'Tom',
    age: 22,
    memberLevel: 'gold',
  },
  {
    id: 5,
    name: 'Alice',
    age: 26,
    memberLevel: 'bronze',
  },
];

假如要過濾青銅級的會員的話,可以使用:

const advancedMembers = members.filter(item => item.memberLevel !== 'bronze');`

如果要為函式增加重用性,可以改成以下這樣,就可以傳入不同等級了。

const filterLevel = (data, level) => data.filter(item => item.memberLevel !== level);

但是上面的函式只能針對 memberLevel 進行過濾,如果要過濾不同屬性的特定值的話,就可以使用柯里化處理:

const filter = filterRole => data => data.filter(filterRole);

const filterByProperty = propertyName => propertyValue => filter(item => item[propertyName] !== propertyValue);

const filterByName = filterByProperty('name');
const filterByNameJohn = filterByName('John');

console.log(filterByNameJohn(members));

const filterByLevel = filterByProperty('memberLevel');
const filterByLevelGold = filterByLevel('gold');

console.log(filterByLevelGold(members));

範例2.

這類題目是要計算一個 n 次呼叫連加的函式,要讀者計算所有傳入參數的總和。以這題來說,要計算 add(4)(3)(4)(0)(3)() 的總和。

function add(x){
  return function (y) {
    if (y || y === 0) return add(x + y);
    return x;
  }
}

console.log(add(4)(3)(4)(0)(3)()); // 14

思考的點就是一定會不斷回傳函式,因為從 console.log 看 add 函式會被呼叫好幾次,但還是需要有一個中止的條件,那就是當接下來要呼叫的參數不存在時,就停止呼叫,直接 return x。

其他解法

這裡也可以應用 valueOf(),valueOf() 也可以用 toString() 代替,但 valueOf() 會更適合所以用它。

function add(x){
  let sum = x;

  function resultFn(y){
    sum += y;
      return resultFn;
  }

  resultFn.valueOf = function(){
    return sum;
  };
  return resultFn;
}

範例3.

這題參考 Advanced curry implementation,給以下內容,實作 curry 函式,注意 curry 函式傳入的函式,其接收參數必須是固定的。

function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

alert(curriedSum(1, 2, 3)); // 6, still callable normally
alert(curriedSum(1)(2,3)); // 6, currying of 1st arg
alert(curriedSum(1)(2)(3)); // 6, full currying

解答:

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) return func(...args);
    return (...args2) => curried(...args, ...args2);
  };
}

if 判斷式的邏輯是如果 args 參數累計和 func 所傳入的參數一樣或是大於 func 的參數時,就呼叫 func 並帶入所有累積的參數,否則就繼續呼叫 curried 函式去對 func 函式傳入的參數進行 partial。

試著思考以下範例,假設 f 不只接收 2 個參數,那就會看到更多了 return function...,此過程就是在做 partial,而直到所有的參數都 partial 後,就可以回傳 f 和其所有參數。

// 兩個
function curry(f) {
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

// 四個
function curry(f) {
  return function(a) {
    return function(b) {
        return function(c) {
            return function(d) {
                return f(a, b, c, d);
            };
        };
    };
  };
}

func.length 其實是 JS 函式的一個屬性 Function: length,代表一個函式的傳入參數數量。

這篇就到這邊結束啦~明天將會進入重要的 JS 原型繼承章節。


參考資料 & 推薦閱讀

Currying

Currying in Javascript and its practical usage

Function binding

https://youtu.be/k5TC9i5HonI

https://youtu.be/I4MebkHvj8g


上一篇
Day11-bind() 函式介紹 & 實作
下一篇
Day13-圖解原型繼承與原型鏈
系列文
強化 JavaScript 之 - 程式語感是可以磨練成就的30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
harry xie
iT邦研究生 1 級 ‧ 2023-11-01 11:30:28

其他 Partial application 範例

重構前:

const getApiURL = (apiHostname, resourceName, resourceId) => {
  return `https://${apiHostname}/api/${resourceName}/${resourceId}`
}

const getUserURL = userId => {
  return getApiURL('localhost:3000', 'users', userId)
}

const getOrderURL = orderId => {
  return getApiURL('localhost:3000', 'orders', orderId)
}

const getProductURL = productId => {
  return getApiURL('localhost:3000', 'products', productId)
}

重構後:

const partial = (fn, ...argsToApply) => {
  return (...restArgsToApply) => {
    return fn(...argsToApply, ...restArgsToApply)
  }
}

const getApiURL = (apiHostname, resourceName, resourceId) => {
  return `https://${apiHostname}/api/${resourceName}/${resourceId}`
}

const getResourceURL = partial(getApiURL, 'localhost:3000')

const getUserURL = partial(getResourceURL, 'users')
const getOrderURL = partial(getResourceURL, 'orders')
const getProductURL = partial(getResourceURL, 'products')

我要留言

立即登入留言