函数柯里化 (Currying)
系列开篇
面试题
- 什么是柯里化
- 柯里化的应用场景有哪些
- 你了解函数式编程吗
- 实现一个curring
这是干什么的?
先看看其他官方的各种定义引用:
举个例子:
它是指将一个函数从可调用的 f(a, b, c)
转换为可以这样调用 f(a)(b)(c)
。柯里化不会调用函数,它只是对函数进行转换。
我们先做个简单总结
- 柯里化是一种
函数式编程的技术
。 - 只传递给函数
一部分参数来调用
它,并返回一个函数
去处理剩下的参数。 - 它不仅被用于 JavaScript,还被用于其他编程语言。
分析理解
上面我们下了个定义,但是我猜你还是对这个玩意不理解,有什么用,看分析你就明白了。
javascript中函数调用一般长这样:
let add = function(a, b) {
return a + b
}
add(1, 2) //= 3
一个函数接受一定数量的参数,然后执行后返回一个 value。当我们传入少的,或者多的参数(反正跟定义的不同数量),会造成结果不合预期(传少)或者多出来的参数被忽略的结果。
add(1) //= NaN
add(1, 2, 'ignore args') //= 3
下面我们使用curry的方式来把多参数加法变成一个个单参数调用的函数
var curry = require('curry') // 假设这个curry方法是外部引入的
// 我们把这个函数柯里化, 就是把这个函数传到 curry() 中并返回出了一个被curry的函数 sum3
var sum3 = curry(function(a, b, c) {
return a + b + c
})
// 那么接下来我们可以这样调用他们
sum3(1, 2, 3) //= 6 原来的方式当然ok
sum3(1)(2)(3) //= 6 可一个个参数调用
sum3(1)(2, 3) //= 6
sum3(1, 2)(3) //= 6 也可传入部分,再传入剩余的
sum3(1)(2, 3)
称为以偏函数(partial
)的方式调用 或者称为 Partial Application
(偏函数应用)
这是指使用一个函数并将其应用一个或多个参数,但不是全部参数,在这个过程中创建一个新函数。
这样有啥好处,哪些场景可以用
我们先空泛地说下好处,再解释
一句话:令函数有更好的可读性、灵活性 和 复用性。
其他潜在好处:
- 可以让你生成一个小型的,易于配置的函数库,而且这些函数的行为始终如一。(没有副作用的纯函数[相关概念])
- 可以让你养成良好的函数命名习惯。
用些例子解释下:
1. 参数复用,形成一些偏函数,灵活应用
例如: 我们有一个用于格式化和输出信息的日志的函数 log(level, message)
。假设长这样
- level 设置日志警告等级
'warn', 'error', 'info'
等 - message 日志内容信息
function log(level, message) {
console.log(`[${level}] ${message}`);
}
非常简单的函数, 想想平时都是这样调用是不是
if (exp) {
log('warn', 'sth... warn')
}
log('error', '...message')
...
现在柯里化看看能有啥变化
import _ from 'loadsh'
var log = _.curry(log)
// 柯里化之后,log 仍正常运行:
log("warn", "some warn"); // "[warn] some warn"
// 但是也可以以柯里化形式运行:
log("warn")("some warn"); // "[warn] some warn"
这样我们能创建更多『便捷函数』或者说偏函数
let warnLogger = log('warn');
// 使用它
warnLogger("message"); // [warn] message
warnLogger
是带有固定第一个参数的日志的偏函数,这个函数是参数固定的原来函数的部分函数。
那么我们现在调用方式
if (exp) {
warnLogger('sth... warn')
}
errLogger('...err message')
可读性是不是大大增加了,而且更灵活,因为你的这些小函数是可以互相组合的。 这里的例子因为简单,你还看不出好处有多大。尝试在复杂项目中使用,你会发现这种编程习惯会让你思路更清晰。
2. 将操作原子化,方便单元测试
简单来说,就是把各种小操作给工具函数化,增加了可读和复用性。
还又很重要的一点是,这些函数可以非常方便地进行单元测试[关联概念]。
举个例子:仅仅是个例子,请举一反三
var objects = [{ id: 1 }, { id: 2 }, { id: 3 }]
let idList = objects.map(function(item) {
return item.id
})
其实我们要做的操作就是:遍历这个对象数组并取出他们的id
那么现在我们可以用柯里化处理一波
import _ from 'loadsh'
var get = _.curry(function(prop, object) {
return object[prop]
})
// map接受一个function 是用来获取每个对象的'prop'的 这里的prop是'id'
objects.map(get('id')) //= [1, 2, 3]
我们在get函数中 真正创建的的可以部分配置的函数。 再看我们平时是不是使用这种方式多点,来创建一个方法用于获取对象数组中的id
let getIDs = function(objects) {
return objects.map(get('id'))
}
getIDs(objects) //= [1, 2, 3]
我们甚至可以进一步把 map也进行curry处理
let curriedMap = curry(function(fn, value) {
return value.map(fn)
})
var getIDs = curriedMap(get('id'))
getIDs(objects) //= [1, 2, 3]
这样的代码读上去更清楚不是吗,当你在平时工作中积累了很多原子操作处理,就像一块块积木,顺手拿来,解决问题的速度会让你惊讶的。
实际怎么写
如果你在现实项目中想用curry,建议先了解函数式编程,自然而然地使用。单用curry也建议直接用lodash (省的自己写,而且别人的也处理了this等其他需要特殊处理的部分, placeholders也是很好用的)
_.curry
当然你也可以选择自己实现一个,根据下面原理,简单实现。
原理是什么?
了解原理实现,最好先了解 闭包【关联概念(强)】的概念。这是理解下面原理实现的前提。
function myCurry(func) {
// 我们myCurry调用应该返回一个包装器 curried,令这个函数curry化
return function curried(...args) {
// curry 的使用主要看参数数量
return args.length >= func.length ?
// 如果传入的 args 长度与原始函数所定义的(func.length)相同或者更长,
// 那么只需要将调用传递给它即可。直接现在就调用,返回函数结果
func.call(this, ...args) :
// 否则的话,返回另一个包装器方法,递归地调用curried,将之前传入的参数与新的参数拼接后一起传入。
// 然后,在一个新的调用中,再次,我们将获得一个新的偏函数(如果参数不足的话),或者最终的结果。
(...rest) => {
return curried.call(this, ...args, ...rest);
};
};
}
注意每次调用参数不足返回包装器函数时,会将上一轮参数保存在词法环境中,利用闭包
的特性,进行下一轮判断。
我觉得看注释应该不用多解释了,不理解评论区留言吧。
写完可以用这个例子测试下
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = myCurry(sum);
console.log( curriedSum(1, 2, 3) ); // 6,仍然可以被正常调用
console.log( curriedSum(1)(2,3) ); // 6,对第一个参数的柯里化
console.log( curriedSum(1)(2)(3) ); // 6,全柯里化
其他
为何使用函数式编程风格
Pointfree 就是如何使用函数式编程的答案
- 这就叫做 Pointfree:不使用所要处理的值,只合成运算过程。中文可以译作"无值"风格。
- Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。这就要求,将一些常用的操作封装成函数。
例子
下面是一个字符串,请问其中最长的单词有多少个字符?
var str = 'Lorem ipsum dolor sit amet consectetur adipiscing elit';
我们先定义一些基本运算。
// 以空格分割单词
var splitBySpace = s => s.split(' ');
// 每个单词的长度
var getLength = w => w.length;
// 词的数组转换成长度的数组
var getLengthArr = arr => R.map(getLength, arr);
// 返回较大的数字
var getBiggerNumber = (a, b) => a > b ? a : b;
// 返回最大的一个数字
var findBiggestNumber =
arr => R.reduce(getBiggerNumber, 0, arr);
然后,把基本运算合成为一个函数
var getLongestWordLength = R.pipe(
splitBySpace,
getLengthArr,
findBiggestNumber
);
getLongestWordLength(str) // 11
可以看到,整个运算由三个步骤构成,每个步骤都有语义化的名称,非常的清晰。这就是 Pointfree 风格的优势。
memoization
function memoizeFunction(func) {
var cache = {};
return function() {
var key = arguments[0];
if (cache[key]) {
return cache[key];
} else {
var val = func.apply(this, arguments);
cache[key] = val;
return val;
}
};
}
var fibonacci = memoizeFunction(function(n) {
return (n === 0 || n === 1) ? n : fibonacci(n - 1) + fibonacci(n - 2);
});
console.time('start1');
fibonacci(100)
console.timeEnd('start1')
// 第二次有缓存
console.time('start2');
fibonacci(100)
console.timeEnd('start2')
缓存对计算速度提升效果明显
上面这句话给你们,同样也给我自己前进的动力。
我是摩尔,数学专业,做过互联网研发,测试,产品
致力用技术改变别人的生活,用梦想改变自己的生活
关注我,找到自己的互联网思路,踏实地打牢固自己的技术体系
点赞、关注、评论、谢谢
有问题求助可私信 1602111431@qq.com 我会尽可能帮助你
参考
- www.sitepoint.com/currying-in…
- medium.com/@kevincenni…
- hughfdjackson.com/javascript/…
- juejin.cn/post/684490…
- www.ruanyifeng.com/blog/2017/0…
- zh.javascript.info/currying-pa…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!