Libon

Higher Order Function / Currying / Partial-Function

2023/06/10 高阶函数/柯里化函数及偏函数的概念 #JavaScript

ToC

高阶函数

高阶函数并不是 JS 独有的概念,在其他语言中也能发现它的影子。高阶函数的核心概念就是将函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。举个详细的例子:

  function test() {}  // 函数可以直接声明
  var test1 = function() {} // 函数可以赋值给变量
  // 函数参数接收变量
  function test2(arg) {}
  // 那么函数、也可以接受变量作为参数
  test2(test1)

JS 中内置的高阶函数有很多,比如 setTimeout setInterval [].map() [].reduce() [].filter ,这里引申出一个问题,高阶函数的好处是什么?

实际上高阶函数最大的好处是抽象函数,便于扩展。将函数由相互依赖的程序体抽象成完全独立的函数体。

函数柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

它在实际使用效果是这样的:

function curry(fn) {}

function add(a,b,c,d) {
	return a + b + c + d
}

// 正常调用 add
add(1,2,3,4)

// 使用柯里化
var curryAdd = curry(add)
curryAdd(1) // 参数个数不足,返回一个新的函数
curryAdd(2) // 参数个数不足,返回一个新的函数
curryAdd(3) // 参数个数不足,返回一个新的函数
curryAdd(4) // 返回最终的结果

// 还可以第一次就把前三个参数传好
var curryAdd = curry(add, 1, 2, 3)
curryAdd(4) // 返回最终结果

// 或者时不定参数个数
var curryAdd = curry(add)
var curryAdd2 = curryAdd(1, 2)
var curryAdd3 = curryAdd2(3)

curryAdd3(4) // 返回最终结果

初看这种效果好像没啥用,但是仔细想想,如果一个函数有多个参数,而我前几个参数的值是固定的,已经确认好结果的,如果不用柯里化函数,那每次调用的时候都需要传入完成的参数,但是如果使用了柯里化函数,并且将那些已确定的参数传入,那最终在调用的时候,就只需要传入每次需要变化的那个参数即可,这才是柯里化函数真正的用途。

curry 实现

柯里化的基本实现可以简化成以下版本的代码:

// 第一个函数表示需要将哪个函数转换为柯里化函数
// 从第二个参数开始,往后所有的参数都是传递给第一个函数参数的
function curry(fn, ...args) {
	// 返回一个新的函数,接收剩下的函数
	return function (..._args) {
		const params = args.concat(_args)

		return fn.apply(this, params)
	}
}

// 使用
var curryAdd = curry(add, 1, 2)
console.log(curryAdd(3, 4)) // 10

但这只是实现了最基本的功能,因为它还没有对参数个数进行判断,柯里化函数的特点就是在参数个数不满足函数要求之前返回的东西都是一个函数,而只有满足了参数个数之后,得到的才是返回的结果。以下是改良版:

function curry(fn, ...args) {
	// 如果第一次执行时参数个数足够则直接函数函数调用的结果
	if (args.length >= fn.length) {
		return () => fn(...args)
	}

	// 如果参数不够则返回一个新的函数,将上次执行和下一次执行的参数收集起来
	return (...params) => curry(fn, [...args, ...params])
}
// 测试一下
function add(a, b, c, d) {
  return a + b + c + d
}

console.log(curry(add))               // [Function (anonymous)]
console.log(curry(add, 1))            // [Function (anonymous)]
console.log(curry(add, 1, 2))         // [Function (anonymous)]
console.log(curry(add, 1, 2, 3))      // [Function (anonymous)]
console.log(curry(add, 1, 2, 3, 4)()) // 10(第一次就把所有参数给设置好,但依然需要返回一个函数,因为 curry 的本质上就是返回一个接收任意参数个数的函数)
console.log(curry(add)(1)(2)(3)(4))   // 10

偏函数

在了解偏函数之前,我们先了解一下什么是函数的元。有一个函数有两个参数,那么它就叫二元函数。这里的元就表示参数的个数。而减少函数的个数就可以叫做降元。

在计算机科学中,偏函数叫做部分应用、局部应用。指固定的一个函数的一些参数,然后产生另一个更小元的函数。

function add(a, b, c) {
	return a + b + c
}

var add1 = partial(add, 1)

add1(2, 3)

是不是似曾相识?它好像和那个柯里化有点相似啊

与柯里化有什么区别?

偏函数与柯里化在语法上看不出什么区别,但他们的目的是不一样的。

柯里化的目的是返回一个接收任意参数个数的函数来实现最终被包装函数的调用,而偏函数则是为了简化函数参数,固定函数参数中的部分参数,使得最终调用时更简单,像这样:

// 比如说有一个函数,它会做很多事情,同时也包括将传入的参数转换成对应进制的值
function parseNumber(value, radix) {
	// ...do something
	const number = parseInt(value, radix)
}

// 正常情况下调用:
parseNumber('1', 10)
parseNumber('3', 10)
parseNumber('10', 10)

// 而使用偏函数包装以后
const newParseNumber = partial(parseNumber, 10)
newParseNumber('1')
newParseNumber('3')
newParseNumber('10')

而其实最简单的偏函数使用一个 bind 就能实现:

function add(a, b, c, d) {
	return a + b + c + d
}

const newAdd = add.bind(null, 1, 2)
console.log(newAdd(3, 4))

具体实现

以上确实能实现,但是它使用需要传入第一个参数作为指向,那我们可以在 Function.prototype 上增加一个方法,替我们去做这件事

Function.prototype.partial = function(...args) {
	const _self = this

	return function (...params) {
		return _self.apply(this, [...args, ...params])
	}
}

const newAdd = add.partial(1, 2)

newAdd(3, 4) // 10

我们可以试试用这个来实现一个实例

function formatSentence(time, opt) {
	return time + ' ' + opt.user_class + ' ' + opt.name + ': ' + opt.sentence
}

const outPutSentence = formatSentence.partial(new Date().getHours() + ':' + new Date().getMinutes())

console.log(outPutSentence({
	user_class: '管理员',
	name: '测试',
	sentence: '欢迎大家'
}))

以上。

CD ..