用大白話介紹柯里化函式

小黎也發表於2019-04-14

最近在學習函數語言程式設計,看到柯里化函式這個東東,原以為是個新的概念,沒想到一查,竟然老早就存在了,且已經成熟運用多年,深感慚愧啊,我到現在都還沒接觸過。

官方解釋

先是檢視了柯里化的介紹

在電腦科學中,柯里化(Currying)是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數且返回結果的新函式的技術。

這句話聽起來有點拗口啊,反正我沒能完全理解,不過沒關係,我們可以檢視網上大牛們的解釋,經過一頓搜尋,算是屢清楚了頭緒。

我的理解

我們先來看個栗子,這是一個求和的函式,求4個引數的和,那麼我們在使用這個函式的時候,就要先知道每個引數的具體的值,然後才能呼叫這個函式。

function add(a, b, c, d) {
    return a + b + c + d;
};
console.log(add(2,3,4,5))  // 14
複製程式碼

而假如,我們想讓呼叫更加靈活一些,比如改成下面這種呼叫方式

console.log(add(2)(3)(4)(5))  // 14
複製程式碼

可以看到,這種呼叫方式有個好處,不需要等待所有引數明確後在執行具體方法,當在引數不滿足的情況下,每次呼叫返回的是函式,比如 add(2) 返回的是一個函式,返回的函式不斷的接受新的引數,直到滿足預定好的4個引數時,便輸出結果。 而 add(2,3,4,5) => add(2)(3)(4)(5) 這一個過程便是函式柯里化的過程。這樣子解釋,不知道大夥好理解不?

talk is cheap , show me the code,ok 那我們看下具體的實現,先寫一個函式將 add() 轉換成柯里化函式

/**
 * 轉換函式
 * @param {*} fn 目標函式
 * @param  {...any} args 其他引數
 */
const createCurry = (fn, ...args) => {
    // 獲取目標函式的引數個數
    let length = fn.length;
    return (...rest) => {
        // 將已有的引數和新的引數合併
        let allArgs = args.slice(0);
        allArgs.push(...rest);
        // 若引數個數已經滿足目標函式的引數要求,則執行目標函式。否則繼續返回轉換函式
        if (allArgs.length < length) {
            return createCurry.call(this, fn, ...allArgs)
        } else {
            return fn.apply(this, allArgs)
        }

    }
}
function add(a, b, c, d) {
    return a + b + c + d;
};
複製程式碼

使用如下:

const curryAdd = createCurry(add,2);
const sum = curryAdd(3)(4)(5);    

console.log(sum) // 14
複製程式碼

轉換柯里化函式有個關鍵的點,那就是要明確觸發條件,比如說上面的栗子中,我們的觸發條件是引數的個數,根據引數的個數來區分返回的是函式還是具體的結果。

柯里化函式的特點

通過上面的栗子我們可以看出柯里化函式有這麼幾個特點:

  1. 引數複用
  2. 業務解耦,呼叫時機靈活
  3. 延遲執行,部分求值

繼續舉栗子

光說理論太枯燥,那麼我們把上面的栗子在稍微擴充套件下,把add函式改成不定引數,就是說,我可以傳n個引數,求這n個引數的和,呼叫如下

const curryAdd = createCurry(add);
const sum = curryAdd(3)(4)(5); // 12  
const sum = curryAdd(3)(4)(5)(6); // 18  
複製程式碼

OK,首先我們要對add函式做下修改

/**
 * 不定引數求和
 * @param  {...any} arg 
 */
function add(...arg){
    return arg.reduce((result,value)=>{ return result += value },0)
}
複製程式碼

那麼這個函式使用就變成 add(1,2,3),但我們的目標是這樣的 add(1)(2)(3) ,在寫轉換函式之前,我們先要確定我們的觸發條件,我想到的條件是:當新傳入的引數個數為 0 的時候,就是沒有引數,就執行目標函式,否則返回函式。那麼柯里化函式的呼叫方式需要稍微改成如下

add(1)(2)(3)();
複製程式碼

具體的轉換函式和呼叫如下

const createCurry = (fn, ...args) => {
    return (...rest) => {
        let allArgs = args.slice(0);
        allArgs.push(...rest)
        // 觸發條件
        if (rest.length !== 0) {
            return createCurry.call(this, fn, ...allArgs)
        } else {
            return fn.apply(this, allArgs)
        }

    }
}
const curryAdd = createCurry1(add);
console.log(curryAdd(1)(2)(3)(4)()) // 10
複製程式碼

柯里化到底是什麼?

通過上面兩個小栗子,大夥應該有個大致概念了,所以柯里化是一種設計思想,主動掌握了函式的控制權,根據我們的需要,設定相應的觸發機制。有句話,讓程式碼編寫程式碼,柯里化算是有那麼點意思吧,函式生成函式,函式去呼叫函式,我們只要編寫原子性的函式和設定好條件。

再來兩個栗子

陣列處理

寫個通用的陣列處理柯里化函式,currying的第一個引數是目標函式,currying直接返回函式,沒有特定的觸發條件,在使用mapSQ([1, 2, 3, 4, 5]) 會將陣列和currying的第二個引數一起傳給目標函式,即currying第一個引數,詳細如下:

function currying(fn) {
    var slice = Array.prototype.slice,
        __args = slice.call(arguments, 1);
    return function () {
        var __inargs = slice.call(arguments);
        return fn.apply(null, __args.concat(__inargs));
    };
}

function square(i) {
    return i * i;
}
function double(i) {
    return i *= 2;
}

function map(handeler, list) {
    return list.map((value) => {
        return handeler(value)
    });
}

var mapSQ = currying(map, square); // 平方
mapSQ([1, 2, 3, 4, 5]); //[1, 4, 9, 16, 25]

var mapTwo = currying(map, double); // 兩倍
mapSQ([1, 2, 3, 4, 5]); //[2,4,6,8,10]

複製程式碼

以引數長度為條件的轉換函式

這個栗子是在 30secondsofcode 上看到,我覺得寫得非常精簡直觀,給大家分享下

const curry = (fn, arity = fn.length, ...args) => arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);

curry(Math.pow)(2)(10); // 1024
curry(Math.min, 3)(10)(50)(2); // 2
複製程式碼

官方寫成了一行,閱讀可能不太方便,我改成通俗版,加了些註釋

/**
 * 生成柯里化函式,以引數長度達標作為觸發條件
 * @param {*} fn 目標函式
 * @param {*} arity 目標函式引數個數
 * @param  {...any} args 呼叫傳入的引數
 */
const curry = (fn, arity = fn.length, ...args) => {
    if (arity <= args.length) {
        return fn(...args)
    } else {
        return curry.bind(null, fn, arity, ...args);
    }
}
複製程式碼

小結

函式柯里化可以給我們帶來很多想象,可以將耦合的業務邏輯拆解,使得函式程式設計更加純粹。不過我個人覺得柯里化函式要是太複雜,對大大降低程式碼的可閱讀性和可維護性,所以柯里化雖然看著高大上,但還是不能濫用。

參考資料

baike.baidu.com/item/柯里化

segmentfault.com/a/119000001…

相關文章