用場景去理解函式柯里化(入門篇)

RobinsonZhang發表於2019-04-10

前言

函式柯里化就是將多參簡化為單引數的一種技術方式,其最終支援的是方法的連續呼叫,每次返回新的函式,在最終符合條件或者使用完所有的傳參時終止函式呼叫。

場景例項

與其他文章不同,我在本文會重點分享一些柯里化的經典使用場景,讓你在學會這點技巧後能切實的提升程式碼的可維護性。

編寫可重用小模組程式碼

比如我們有個方法部分邏輯前置是相同的,後面的執行是因為引數不同導致結果不同的,下面是程式碼部分。

計算商品的折扣,我們需要根據不同的折扣以及商品的入參返回其實際的價格。

// before
function getPrice(price,discount){
	return price * discount;
}

let price = getPrice(500,0.1);


// after 
function getPrice(discount){
	return price =>{
  	return price * discount
  }
}
// 使用,在這種使用效果下,我們可以固定的肢解拿到百分之十折扣的函式,
//也就是針對使用0.1折扣的商品價格都可以簡化這個折扣的傳遞,從而達到簡化引數的目的
//那麼從函式的執行上來講,也比之前的效率高了,如果解析折扣的過程比較複雜
let tenDiscount = getPrice(0.1);
let price = tenDiscount(500);

let price = getPrice(0.1)(500)

複製程式碼

看上去有點雞肋,因為我們本來的寫法很簡單,使用了柯里化反而讓簡單的事情變得複雜了,這主要是因為沒有達到我們要把一個函式變成柯里化的經典場景。假如你下面的程式碼變成了下面這樣,也許你就能覺察出如果有使用柯里化就會非常方便了,因為針對第一個引數做了若干的處理,甚至可以稱為一個演算法或者完整的邏輯判斷流程,那麼如果有多個引數呼叫都涉及這個方法的呼叫,同一個引數的這部分邏輯是相同可以共用跳過的。codepen連線:連結

// complexed fun 
function getPriceComplex(price,discount){
  let actualDiscount = 1;
  if(discount > 0.8 ) {
  	actualDiscount = 0.8;
  } else if(discount > 0.5){
  	actualDiscount = 0.5;
  } else {
    actualDiscount = 0.1;
  }
  let actualPrice = price - price % 100 ;
	return actualPrice * actualDiscount;
}

// complexed fun better
function getPriceComplexBetter(discount){
  let actualDiscount = 1;
  if(discount > 0.8 ) {
  	actualDiscount = 0.8;
  } else if(discount > 0.5){
  	actualDiscount = 0.5;
  } else {
    actualDiscount = 0.1;
  }
  return price => {
  	 let actualPrice = price - price % 100 ;
			return actualPrice * actualDiscount;
  }
}


console.log(getPriceComplex(500,0.9))
let exp1 = getPriceComplexCp(0.9);
console.log(exp1);
/** price => {
    let actualPrice = price - price % 100;
    return actualPrice * actualDiscount;
}*/
// 相同的輸入引數時 可以快取下之前程式碼邏輯的執行結果 實現模組的可重用,如果你之前的邏輯是一個純函式
console.log(exp1(500))// 400
console.log(exp1(400))// 320


// get real discount 
// 當你針對第一個引數的邏輯較為複雜時,出於可維護角度,建議如此 ;
// 當你另外一個邏輯也是基於這個返回結果時,出於重用角度,建議如此
function getActualDiscount(discount){
   let actualDiscount = 1;
  if(discount > 0.8 ) {
  	actualDiscount = 0.8;
  } else if(discount > 0.5){
  	actualDiscount = 0.5;
  } else {
    actualDiscount = 0.1;
  }
  return actualDiscount;
}
// complexed fun best
function getPriceComplexBest(discount){
  let actualDiscount =getActualDiscount(discount);
  return price => {
  	 let actualPrice = price - price % 100 ;
			return actualPrice * actualDiscount;
  }
}

複製程式碼

總結,無論如何,我們使用某種技巧或者封裝或者其他,都是為了讓程式碼更可用,原先複雜不可測試、不可理解的程式碼變得更有調理,更節省效能的角度出發的,當你的思維方式中有這種的時候,你就不會覺得是為了形式而使用,而是你的編碼習慣或者風格就是如此。

簡單改造普通函式為柯里

假如我們需要把一個原來非柯里的函式如何快速改造,在不影響原來主要程式碼邏輯的情況下,想下我們程式碼可能如何寫?

// 只考慮兩個引數
function add(a,b){
 return a + b
}

// 但如果你是用柯里化的方式:兩個引數的時候 ,但這樣對原始碼變動非常大,對於一些複雜的邏輯,這基本不可能
function curryAdd(...args){
  return (...newArgs) => {
  	return anoNumber * number;
  };
}

// 我們寫一個通用的柯里化函式的方式,經過這個函式的轉換,我們可以將呼叫方式簡化
function curry = (fn,...args){
	return (..._args)=>{
  	return fn(...args, ..._arg);
  }
}

let curryAdd = curry(add,10);
let curryAdd2 = curryAdd(11)
複製程式碼

不定引數的累加

一個比較經典的練手題,把下面的程式碼用柯里化的方式實現,其難點簡單分析如下:如果你沒有了解過柯里化,可能覺得基本無法完成。

1 動態入參個數,這個也許還可以通過arguments迴圈完成
2 每次都能接受新的引數繼續累加,這必須是返回新函式並帶有之前的結果,要求是具有柯里化特點
3 每次不在追加引數時,需要能得到的值,這個需要你瞭解toString方法來改變結果值

實現一個add方法,使計算結果能夠滿足如下預期: add(1)(2)(3) = 6

add(1, 2, 3)(4) = 10

add(1)(2)(3)(4)(5) = 15

function add() {
    // 第一次執行時,定義一個陣列專門用來儲存所有的引數
    var _args = [].slice.call(arguments);
    // 在內部宣告一個函式,利用閉包的特性儲存_args並收集所有的引數值,執行時已經收集所有引數為陣列
    var adder = function () {
        var _adder = function() {
          // 執行收集動作,每次傳入的引數都累加到原引數
            [].push.apply(_args, [].slice.call(arguments));
            return _adder;
        };
        // 利用隱式轉換的特性,當最後執行時隱式轉換,並計算最終的值返回
        _adder.toString = function () {
            return _args.reduce(function (a, b) {
                return a + b;
            });
        }
        return _adder;
    }
    return adder(_args);
}
複製程式碼

備註:codepen中的console.log方法被重寫,會有報錯的問題,你可以直接通過瀏覽器的console控制檯除錯這個方法。

部分引數應用

部分引數應用是指有些場景是希望固定傳遞多個引數,來得到其固定的函式,然後基於這個函式去執行程式碼。類似於第一個例子中的一個折扣引數得出折扣演算法的使用。我們將第一個例子再複雜化一些。就會變成這樣的。

function getActualDiscount(custoemrLevel,discount){
	
}
function getPriceComplex (custoemrLevel,discount){
	let actualDiscount = getActualDiscount(custoemrLevel,discount);
  return price=>{
  	return price * actualDiscount;
  }
}
// 等級一的折扣策略 
let strategyLev1WithOnepoint = getPriceComplex('lev1',0.1) ;
let actualPrice = strategyLev1WithOnepoint(500);
複製程式碼

參考文章

相關文章