柯里化-Ramda源碼中的實現

知識點總結

  1. 瞭解柯里化
  2. 閉包的應用

柯里化的簡單實現

一個參數的函數柯里化實現

function _curry1(fn) {
	return function f1(a) {
		if (arguments.length === 0) {
			return f1;
		} else {
			return fn.apply(this, arguments);
		}
	};
}

const curryMathAbs = _curry1(Math.abs);
console.log(curryMathAbs(-3)); // 3
console.log(curryMathAbs()(-3)); // 3
複製代碼

Math.abs是個只有一個入參的函數。將其柯里化,獲得函數curryMathAbs,只需判斷調用curryMathAbs給它傳遞了一個參數仍是兩個參數,傳遞一個參數時,就返回f1,傳遞兩個參數時,調用fn,也就是原函數Math.abs。緩存

兩個參數的函數柯里化實現

function _curry2(fn) {
	return function f2(a, b) {
		switch (arguments.length) {
			case 0:
				return f2;
			case 1:
				return _curry1(function(_b) { return fn(a, _b); });
			default:
				return fn(a, b);
		}
	};
}
const curryMathatan2 = _curry2(Math.atan2);
console.log(curryMathatan2(90)(15)); // 1.4056476493802699

複製代碼

Math.atan2是個有兩個入參的求正切值的函數。將其柯里化,獲得函數curryMathatan2,這時候要判斷給其傳入的時0個參數,1個參數,仍是2個參數。傳入0個參數,返回f2,傳入1個參數,返回_curry1的一個匿名函數,用來等待接受下一個參數,傳入兩個參數,直接調用原函數Math.atan2。bash

多個參數的函數柯里化實現

顯然咱們一直這樣寫下去真的很呆,須要一個通用的柯里化方法。閉包

function _curryN(length, args, fn){
	return function(){
		const newArgs = args.concat(Array.prototype.slice.call(arguments));
		
		return newArgs.length < length
			? _curryN.call(this, fn.length, newArgs, fn)
			: fn.apply(this,newArgs);
	}
}
function curry(fn){
	return _curryN(fn.length, [], fn);
}

const curryMathatan2 = curry(Math.atan2);
console.log(curryMathatan2(90)(15)); // 1.4056476493802699
複製代碼

經過若是傳入的參數個數,小於原函數的參數個數,就返回_curryN,並緩存住已經傳入的參數。實現了任意個參數的函數柯里化。app

能夠傳入佔位符的柯里化函數實現

加入咱們有一個函數f,擁有3個入參,即f(1,2,3)。將其柯里化,g = curry(f)。那麼咱們能夠這樣使用g。g(1)(2)(3)、g(1)(2, 3)、g(1, 2)(3)等。咱們有這樣一個需求,先傳入第二個參數,再傳入第一個參數,再傳入第三個參數,能夠嗎?函數

能夠,咱們能夠使用一個佔位符對象,告訴g這個參數暫時不傳值。性能

g = curry(f)

g(1)(2)(3)

也能夠

g2 = g(_)(2)(3)
g2(1)

複製代碼

這裏的_就是咱們的一個佔位符對象,告訴g函數先不傳第一個參數,後面再傳。ui

那咱們就先實現一個佔位符對象,和一個判斷它是佔位符的方法。this

const _ = {'@@functional/placeholder': true};

function _isPlaceholder(a) {
	return a != null &&
		typeof a === 'object' &&
		a['@@functional/placeholder'] === true;
}
複製代碼

支持佔位符的一個參數的函數柯里化實現

function _curry1(fn) {
	return function f1(a) {
		if (arguments.length === 0 || _isPlaceholder(a)) {
			return f1;
		} else {
			return fn.apply(this, arguments);
		}
	};
}
複製代碼

邏輯裏多了一個含佔位符的判斷。spa

支持佔位符的兩個參數的函數柯里化實現

function _curry2(fn) {
  return function f2(a, b) {
    switch (arguments.length) {
      case 0:
        return f2;
      case 1:
        return _isPlaceholder(a)
          ? f2
          : _curry1(function(_b) { return fn(a, _b); });
      default:
        return _isPlaceholder(a) && _isPlaceholder(b)
          ? f2
          : _isPlaceholder(a)
            ? _curry1(function(_a) { return fn(_a, b); })
            : _isPlaceholder(b)
              ? _curry1(function(_b) { return fn(a, _b); })
              : fn(a, b);
    }
  };
}
複製代碼

能夠看到由於要判斷佔位符的邏輯,使代碼的邏輯判斷多了些。prototype

支持佔位符的多個參數的函數柯里化實現

_curryN(length, received, fn) {
  return function() {
    var combined = [];
    var argsIdx = 0;
    var left = length;
    var combinedIdx = 0;
    while (combinedIdx < received.length || argsIdx < arguments.length) {
      var result;
      if (combinedIdx < received.length &&
          (!_isPlaceholder(received[combinedIdx]) ||
           argsIdx >= arguments.length)) {
        result = received[combinedIdx];
      } else {
        result = arguments[argsIdx];
        argsIdx += 1;
      }
      combined[combinedIdx] = result;
      if (!_isPlaceholder(result)) {
        left -= 1;
      }
      combinedIdx += 1;
    }
    return left <= 0
      ? fn.apply(this, combined)
      : _arity(left, _curryN(length, combined, fn));
  };
}

function curry(fn){
	return _curryN(fn.length, [], fn);
}

複製代碼

由於佔位符,致使判斷入參個數及緩存它們的邏輯複雜了許多,須要花一點時間去理解了。

以上就是Ramda源碼中對於柯里化的實現了。

一些細節

Ramda對全部本身支持的方法,都進行了柯里化

咱們看一下add方法的源碼:

var add = _curry2(function add(a, b) {
  return Number(a) + Number(b);
});
export default add;
複製代碼

add方法是一個有兩個參數的方法,因此使用_curry2進行柯里化,這裏沒有使用curryN,我理解是在細節上對性能的追求吧。

並且Ramda的大部分方法,都是三個參數之內的,不多使用curryN來對方法進行柯里化,因此在Ramda的內部方法中,咱們能夠看到_curry1, _curry2, _curry3, _curryN都存在。

image

參數固定

我注意到curryN方法的源碼是這樣的

function curryN(length, fn) {
  if (length === 1) {
    return _curry1(fn);
  }
  return _arity(length, _curryN(length, [], fn));
}
複製代碼

這裏面調用了_arity, _arity又是什麼樣呢?

function _arity(n, fn) {
  /* eslint-disable no-unused-vars */
  switch (n) {
    case 0: return function() { return fn.apply(this, arguments); };
    case 1: return function(a0) { return fn.apply(this, arguments); };
    case 2: return function(a0, a1) { return fn.apply(this, arguments); };
    case 3: return function(a0, a1, a2) { return fn.apply(this, arguments); };
    case 4: return function(a0, a1, a2, a3) { return fn.apply(this, arguments); };
    case 5: return function(a0, a1, a2, a3, a4) { return fn.apply(this, arguments); };
    case 6: return function(a0, a1, a2, a3, a4, a5) { return fn.apply(this, arguments); };
    case 7: return function(a0, a1, a2, a3, a4, a5, a6) { return fn.apply(this, arguments); };
    case 8: return function(a0, a1, a2, a3, a4, a5, a6, a7) { return fn.apply(this, arguments); };
    case 9: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8) { return fn.apply(this, arguments); };
    case 10: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) { return fn.apply(this, arguments); };
    default: throw new Error('First argument to _arity must be a non-negative integer no greater than ten');
  }
}
複製代碼

arity是一個讓參數固定個數的函數。它的好處在於,咱們能知道執行了部分參數的函數,還須要幾個參數。

const sumArgs = (...args) => R.sum(args);

const curriedAddFourNumbers = R.curryN(4, sumArgs);
const f = curriedAddFourNumbers(1, 2);

console.log('curriedAddFourNumbers', curriedAddFourNumbers);
// curriedAddFourNumbers ƒ (a0, a1, a2, a3) {
//        return fn.apply(this, arguments);
//      }
console.log('f', f);
// f ƒ (a0, a1) {
//        return fn.apply(this, arguments);
//      }
複製代碼

注意f這個函數的打印結果,是一個還須要兩個參數的函數,這就是arity函數的功勞,讓咱們知道執行了部分參數的curriedAddFourNumbers,還須要2個參數,就能夠得出結果。

相關文章
相關標籤/搜索