00-接上篇
在上一篇中主要记录了函数柯里化的基本概念和手动实现,当然如果在每一个项目中都手动实现一遍柯里化显然是不方便的。其次,函数柯里化在函数式编程中具体有哪些应用模式呢?本篇将主要讲述柯里化的使用和函数式编程的应用。
01-Lodash中的柯里化功能
lodash
是一个一致性、模块化、高性能的 JavaScript 实用工具库。在其内部将我们生产和开发中的许多常用功能抽象成了各种函数以方便调用(虽然他们不一定都是纯函数),其中便提供了curry
和curryRight
方法,这一方法可以帮助我们将函数柯里化。其中curry
方法与我们自己实现的函数使用方法差异不多,如下:
var abc = function(a, b, c) {
return [a, b, c];
};
var curried = _.curry(abc);
curried(1)(2)(3);
// => [1, 2, 3]
curried(1, 2)(3);
// => [1, 2, 3]
curried(1, 2, 3);
// => [1, 2, 3]
// Curried with placeholders.
curried(1)(_, 3)(2);
// => [1, 2, 3]
而curryRight
除了 会生成柯里化之后的函数之外,其传递参数的顺序是由后向前。
02-初步总结柯里化
至此,单纯关于柯里化的内容已经完毕,柯里化的优点和作用已然明了:
柯里化可以通过闭包的方式实现生成一个“缓存了一部分参数的函数”从而使我们能够无压力的抽象细粒度的函数而不必担心传入的参数过多的不便。
但是柯里化显然不止是开发上多传一个或者少传一个参数的问题,柯里化的核心其实是将一个多元函数转换成一个一元函数,而为什么要组合成一元函数呢?答案是:函数组合
03-函数组合
首先假设我们需要设计这样一个函数,这个函数的目的是获取数组的最后一个元素并且转换为大写字母,如果应用我们之前所涉及的柯里化和纯函数,我们理所当然的会写出这样的代码:
_.toUpper(_.first(_.reverse(array)))
这种代码虽然可以正常并且顺利的获得我们想要的结果,但是这种“洋葱代码”的层层嵌套如果层级太多就会不利于代码的编写和维护。所以在这种情况我们就不再适合嵌套使用,而是使用“函数组合”:
函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
函数组合的思想是一种由从内到外编写格式到依次执行思路的转变,他有点像pipeline,但是完全不同,不过这并不能阻止你将他想像成一个管道:
如果说一个纯函数是一个完整的管道,接受一个输入,并提供一个输出,就像:
那么使用纯函数的函数组合就像是把一个复杂的多个管道转变成一个个连接在一起的细分小管道:
到这之前的铺垫就都讲得通了,之所以要将函数进行柯里化,是因为需要将多元函数转变为一元函数。而需要一元纯函数的原因,就是在具体组合应用的时候方便进行函数组合。函数组合思想的便利之处就在于,我们可以放心的尽可能细粒度的抽象函数而不必担心调用时过于复杂的问题。我们在应用的时候只要拿出足够细粒度的函数“积木”进行进一步的功能通过组合函数进行整合即可(需要注意的是组合函数的执行顺序是从右到左)。如:
// 组合函数
function compose (f, g) {
return function (x) {
return f(g(x))
}
}
function first (arr) {
return arr[0]
}
function reverse (arr) {
return arr.reverse()
}
// 从右到左运行
let last = compose(first, reverse)
console.log(last([1, 2, 3, 4]))
看过代码,相信“从右到左”这个似乎很反常规的规则的原因也已经明了了,虽然我们传参的顺序是(f, g)
,但是只要想像一下内部的“洋葱代码”,就可得知其实洋葱的最内部是”g“,是内部的函数先执行。
而这样的解决方式显然还是不够“优雅”,你可能会发现其实只是把洋葱代码换了个地方写了而已,而且这种函数组合只能处理两个函数的情况。如果我们需要使用多个函数的组合又该怎么办呢?Lodash为我们提供了解决方案flow()
和flowRight()
,其中flowRight便是从右到左执行的组合方式。这种方式在实际应用中会使用的更多一点(因为他能更好的帮助我们去想像其中的洋葱函数???)。如:
const _ = require('lodash')
const toUpper = s => s.toUpperCase()
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const f = _.flowRight(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))
可以看出我们很优雅的将三个细粒度的纯函数进行了组合形成了一个全新的函数f()
,比洋葱代码要好维护得多了。
以下是面试专用手写一切时间!!!
在面试的的时候如果面试官问起函数组合是如何实现的,我们可以给出这样一个思路:
-
获取传入所有参数的伪数组
-
将其反转(因为我们需要从右向左依次执行)
-
依次组合函数,并将前一个函数的返回值作为后一个函数的参数
-
返回生成的函数
如果面试官问:那么用代码如何实现呢,我们便可以仅用一行代码展现自己优秀的编程水平(并不),如:
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) =>
fn(acc), value)
在这行代码中我们依然使用了…参数的这样的剩余参数用法以获取所有的参数构成的伪数组。之后我们简单的将其简单的reverse()
。然后调用了reduce()
方法。(这里值得好好说一下)
reduce
函数是一个累加器,它接受一个函数作为参数,并且返回数组中每一个元素通过这个函数进行累加之后的值。这样说可能比较抽象,我们可以用数组求和来举个栗子。
var numbers = [65, 44, 12, 4];
function getSum(total, num) {
return total + num;
}
function myFunction(item) {
document.getElementById("demo").innerHTML = numbers.reduce(getSum);
}
reduce
函数会将累加中间结果作为第一个参数,数组项作为第二个参数传入提供的函数,从而返回最终结果。在这个案例中我们会对这个数组中的每一项进行求和。
回到我们手写的compose
函数,我们的函数接受一系列参数,返回的内容是一个接受一个value
参数的函数,函数的内部会累加执行当前函数并且将之前函数的结果作为参数,当然还会传入我们传入的value作为初始参数。翻译成更好理解的方式就是:
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) =>
fn(acc), value)
const compose = (...fns) => {
return value => {
return fns.reverse().reduce((acc, fn)=>{return fn(acc)}, value)
}
}
你学废了吗?
04-调试组合函数
调试组合函数…就是在调用的一串函数中间插一个柯里化过的额外函数,然后将接收到的中间参数原样返回传递下去就可以了…(其实可能不如打个断点?)
示例代码
const _ = require('lodash')
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, array) => _.join(array, sep))
const map = _.curry((fn, array) => _.map(array, fn))
const f = _.flowRight(join('-'), trace('map 之后'), map(_.toLower),
trace('split 之后'), split(' '))
console.log(f('NEVER SAY DIE'))
05-Point free
其实前面几千字的铺垫都是为了这一点…
Point free 编程顾名思义就是他可以省去一些东西,而被free的point是什么呢?你可以这样理解:
如果我们清楚的知道一个公式的计算过程,那么我们即使不提供任何数据,就可以表达出这个公式,并且确信他能达到预期的结果。
而这就需要纯函数,柯里化,函数组合等概念作为前提:
-
如果一个函数会产生副作用,那么就无法确定预期结果(是否为纯函数)
-
如果一个函数是多元函数,那么就必须额外提供参数(不需要提供任何数据)
-
如果一个函数可以被组合,那么他就可以用来表示程序的计算过程(表达出一个公式)
PointFree这一特点通常用来封装一个底层函数,这样我们无需了解其内部的运作流程,只要调用这个函数就可以了,比如:
// Hello World => hello_world
function f (word) {
return word.toLowerCase().replace(/\s+/g, '_');
}
// Point Free
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World'))
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(join('. '),
fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' '))
console.log(firstLetterToUpper('world wild web'))
// => W. W. W
06-函数式编程相关库
lodash/fp
-
lodash 的 fp 模块提供了实用的对函数式编程友好的方法
-
提供了不可变 auto-curried iteratee-first data-last 的方法
(意思就是所有的函数都已经柯里化过,如果同时传参后传数据)
// lodash 模块
const _ = require('lodash')
_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C']
_.map(['a', 'b', 'c'])
// => ['a', 'b', 'c']
_.split('Hello World', ' ')
// lodash/fp 模块
const fp = require('lodash/fp')
fp.map(fp.toUpper, ['a', 'b', 'c'])
fp.map(fp.toUpper)(['a', 'b', 'c'])
fp.split(' ', 'Hello World')
fp.split(' ')('Hello World')
//-------------------------------------
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE'))
本系列/专栏为拉勾教育-大前端高薪训练营学习笔记,内容为本系列课程的讲授内容、亮点题目分析、重点难点的总结、以及个人的体会。个人感觉拉勾教育比体验过的其他教育平台要更好一点。老师讲授的内容比较全面,相对于自学可以节省很多不必要的走弯路的时间,可以更快的使自己在技术上系统的有所提高。同时随堂测的题目也很用代表性,老师跟进解答很快,推荐和我一样在自学路上遇到瓶颈或者找不到进一步学习方向的同学尝试一下。