JavaScript函数式编程,真香之组合(一)

JavaScript函数式编程,真香之熟悉函数式编程(一)

该系列文章不是针对前端新手,须要有一定的编程履历,而且相识 JavaScript 内里作用域,闭包等观点

组合函数

组合是一种为软件的行动,举行清晰建模的一种简朴、文雅而富于表现力的体式格局。经由历程组合小的、肯定性的函数,来建立更大的软件组件和功用的历程,会天生更轻易构造、邃晓、调试、扩大、测试和保护的软件。

关于组合,我以为是函数式编程内里最精华的处所之一,所以我如饥似渴的把这个观点拿出来先引见,由于在全部进修函数式编程里,所碰到的基本上都是以组合的体式格局来编写代码,这也是转变你从一个面向对象,或许构造化编程头脑的一个症结点。

我这里也不去证实组合比继承好,也不说组合的体式格局写代码有多好,我愿望你看了这篇文章能晓得以组合的体式格局去笼统代码,这会扩大你的视野,在你想重构你的代码,或许想写出更易于保护的代码的时刻,供应一种思绪。

组合的观点是异常直观的,并非函数式编程独占的,在我们生涯中或许前端开辟中到处可见。

比方我们如今盛行的 SPA (单页面运用),都邑有组件的观点,为何要有组件的观点呢,由于它的目标就是想让你把一些通用的功用或许元素组合笼统成可重用的组件,就算不通用,你在构建一个庞杂页面的时刻也能够拆分红一个个具有简朴功用的组件,然后再组合成你满足种种需求的页面。

实在我们函数式编程内里的组合也是类似,函数组合就是一种将已被剖析的简朴使命构造成庞杂的团体历程

如今我们有如许一个需求:给你一个字符串,将这个字符串转化成大写,然后逆序。

你能够会这么写。

// 例 1.1

var str = 'function program'

// 一行代码搞定
function oneLine(str) {
    var res = str.toUpperCase().split('').reverse().join('')
    return res;
}

// 或许 按请求一步一步来,先转成大写,然后逆序
function multiLine(str) {
    var upperStr = str.toUpperCase()
    var res = upperStr.split('').reverse().join('')
    return res;
}

console.log(oneLine(str)) // MARGORP NOITCNUF
console.log(multiLine(str)) // MARGORP NOITCNUF

能够看到这里你并没有以为有什么不对的,然则如今产物又突发奇想,改了下需求,把字符串大写以后,把每一个字符拆开以后组装成一个数组,比方 ’aaa‘ 终究会变成 [A, A, A]。

那末这个时刻我们就须要变动我们之前我们封装的函数。这就修正了之前封装的代码,实在在设想情势内里就是损坏了开闭准绳。

那末我们假如把最最先的需求代码写成这个模样,以函数式编程的体式格局来写。

// 例 1.2

var str = 'function program'

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

var toUpperAndReverse = 组合(stringReverse, stringToUpper)
var res = toUpperAndReverse(str)

那末当我们需求变化的时刻,我们基本不须要修正之前封装过的东西。

// 例 2

var str = 'function program'

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

// var toUpperAndReverse = 组合(stringReverse, stringToUpper)
// var res = toUpperAndReverse(str)

function stringToArray(str) {
    return str.split('')
}

var toUpperAndArray = 组合(stringReverse, stringToUpper)
toUpperAndArray(str)

能够看到当变动需求的时刻,我们没有突破之前封装的代码,只是新增了函数功用,然后把函数举行重新组合。

这里能够会有人说,需求修正,一定要变动代码呀,你这不是也删除了之前的代码么,也不是算损坏了开闭准绳么。我这里声明一下,开闭准绳是指一个软件实体如类、模块和函数应当对扩大开放,对修正封闭。是针对我们封装,笼统出来的代码,而是挪用逻辑。所以如许写并不算损坏开闭准绳。

倏忽产物又灵光一闪,又想改一下需求,把字符串大写以后,再翻转,再转成数组。

假如你依据之前的思索,没有举行笼统,你一定心思一万只草泥马在奔驰,然则假如你笼统了,你完整能够不慌。

// 例 3

var str = 'function program'

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

function stringToArray(str) {
    return str.split('')
}

var strUpperAndReverseAndArray = 组合(stringToArray, stringReverse, stringToUpper)
strUpperAndReverseAndArray(str)

发明并没有替换你之前封装的代码,只是替换了函数的组合体式格局。能够看到,组合的体式格局是真的就是笼统单一功用的函数,然后再组成庞杂功用。这类体式格局既磨炼了你的笼统才能,也给保护带来庞大的轻易。

然则上面的组合我只是用汉字来替代的,我们应当怎样去完成这个组合呢。起首我们能够晓得,这是一个函数,同时参数也是函数,返回值也是函数。

我们看到例 2, 怎样将两个函数举行组合呢,依据上面说的,参数和返回值都是函数,那末我们能够肯定函数的基本构造以下(顺便把组合换成英文的 compose)。

function twoFuntionCompose(fn1, fn2) {
    return function() {
        // code
    }
}

我们再思索一下,假如我们不必 compose 这个函数,在例 2 中怎样将两个函数合成呢,我们是不是是也能够这么做来到达组合的目标。

var res = stringReverse(stringToUpper(str))

那末依据这个逻辑是不是是我们就能够写出 twoFuntonCompose 的完成了,就是

function twoFuntonCompose(fn1, fn2) {
    return function(arg) {
        return fn1(fn2(arg))
    }
}

同理我们也能够写出三个函数的组合函数,四个函数的组合函数,不过就是一向嵌套多层嘛,变成:

function multiFuntionCompose(fn1, fn2, .., fnn) {
    return function(arg) {
        return fnn(...(fn1(fn2(arg))))
    }
}

这类恶心的体式格局很显然不是我们程序员应当做的,然后我们也能够看到一些规律,不过就是把前一个函数的返回值作为后一个返回值的参数,当直接到末了一个函数的时刻,就返回。

所以依据一般的头脑就会这么写。

function aCompose(...args) {
    let length = args.length
    let count = length - 1
    let result
    return function f1 (...arg1) {
        result = args[count].apply(this, arg1)
        if (count <= 0) {
          count = length - 1
          return result
        }
        count--
        return f1.call(null, result)
    }
}

如许写没题目,underscore 也是这么写的,不过内里另有许多硬朗性的处置惩罚,中心也许就是如许。

然则作为一个函数式爱好者,只管照样以函数式的体式格局去思索,所以就用 reduceRight 写出以下代码。

function compose(...args) {
    return (result) => {
        return args.reduceRight((result, fn) => {
          return fn(result)
        }, result)
  }
}

固然关于 compose 的完成另有许多种体式格局,在这篇完成 compose 的五种思绪中还给出了别的脑洞大开的完成体式格局,在我看这篇文章之前,别的三种我是没想到的,不过觉得也不是太有效,然则能够扩大我们的思绪,有兴致的同砚能够看一看。

注重:要传给 compose 函数是有范例的,起首函数的实行是从末了一个参数最先实行,一向实行到第一个,而且关于传给 compose 作为参数的函数也是有请求的,必需只要一个形参,而且函数的返回值是下一个函数的实参。

关于 compose 从末了一个函数最先求值的体式格局假如你不是很顺应的话,你能够经由历程 pipe 函数来从左到右的体式格局。

function pipe(...args) {
     return (result) => {
        return args.reduce((result, fn) => {
          return fn(result)
        }, result)
  }
}

完成跟 compose 差不多,只是把参数的遍历体式格局从右到左(reduceRight)改成从左到右(reduce)。

之前是不是是看过许多文章写过怎样完成 compose,或许柯里化,部份运用等函数,然则你能够不晓得是用来干啥的,也没用过,所以记了又忘,忘了又记,看了这篇文章以后我愿望这些你都能够轻松完成。后面会继承讲到柯里化和部份运用的完成。

point-free

在函数式编程的天下中,有如许一种很盛行的编程作风。这类作风被称为 tacit programming,也被称作为 point-free,point 示意的就是形参,意义也许就是没有形参的编程作风。

// 这就是有参的,由于 word 这个形参
var snakeCase = word => word.toLowerCase().replace(/\s+/ig, '_');

// 这是 pointfree,没有任何形参
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

有参的函数的目标是取得一个数据,而 pointfree 的函数的目标是取得另一个函数。

那这 pointfree 有什么用? 它能够让我们把注重力集合在函数上,参数定名的贫苦一定是省了,代码也更简约文雅。 须要注重的是,一个 pointfree 的函数多是由浩瀚非 pointfree 的函数组成的,也就是说底层的基本函数大都是有参的,pointfree 体如今用基本函数组合而成的高等函数上,这些高等函数每每能够作为我们的营业函数,经由历程组合差别的基本函数组成我们的复制的营业逻辑。

能够说 pointfree 使我们的编程看起来更美,更具有声明式,这类作风算是函数式编程内里的一种寻求,一种规范,我们能够只管的写成 pointfree,然则不要过分的运用,任何情势的过分运用都是不对的。

别的能够看到经由历程 compose 组合而成的基本函数都是只要一个参数的,然则每每我们的基本函数参数极能够不止一个,这个时刻就会用到一个奇异的函数(柯里化函数)。

柯里化

在维基百科内里是这么定义柯里化的:

在盘算机科学,
柯里化(英语:Currying),又译为
卡瑞化
加里化,是把吸收多个
参数
函数变换成
吸收一个单一参数(最初函数的第一个参数)的函数,而且返回吸收余下的参数而且
返回效果的新函数的手艺。

在定义中猎取两个比较重要的信息:

  • 吸收一个单一参数
  • 返回效果是函数

这两个要点不是 compose 函数参数的请求么,而且能够将多个参数的函数转换成吸收单一参数的函数,岂不是能够处理我们再上面提到的基本函数假如是多个参数不能用的题目,所以这就很清晰了柯里化函数的作用了。

柯里化函数能够使我们更好的去寻求 pointfree,让我们代码写得更幽美!

接下来我们详细看一个例子来邃晓柯里化吧:

比方你有一间士多店而且你想给你优惠的主顾给个 10% 的折扣(即打九折):

function discount(price, discount) {
    return price * discount
}

当一名优惠的主顾买了一间代价$500的物品,你给他打折:

const price = discount(500, 0.10); // $50 

你能够预感,从长远来看,我们会发明本身天天都在盘算 10% 的折扣:

const price = discount(1500,0.10); // $150
const price = discount(2000,0.10); // $200
// ... 等等许多

我们能够将 discount 函数柯里化,如许我们就不必老是每次增添这 0.01 的折扣。

// 这个就是一个柯里化函数,将原本两个参数的 discount ,转化为每次吸收单个参数完成求职
function discountCurry(discount) {
    return (price) => {
        return price * discount;
    }
}
const tenPercentDiscount = discountCurry(0.1);

如今,我们能够只盘算你的主顾买的物品都价钱了:

tenPercentDiscount(500); // $50

同样地,有些优惠主顾比一些优惠主顾更重要-让我们称之为超等客户。而且我们想给这些超等客户供应20%的折扣。
能够运用我们的柯里化的discount函数:

const twentyPercentDiscount = discountCurry(0.2);

我们经由历程这个柯里化的 discount 函数折扣调为 0.2(即20%),给我们的超等客户设置了一个新的函数。
返回的函数 twentyPercentDiscount 将用于盘算我们的超等客户的折扣:

twentyPercentDiscount(500); // 100

我置信经由历程上面的 discountCurry 你已对柯里化有点觉得了,这篇文章是谈的柯里化在函数式编程内里的运用,所以我们再来看看在函数式内里怎样运用。

如今我们有这么一个需求:给定的一个字符串,先翻转,然后转大写,找是不是有TAOWENG,假如有那末就输出 yes,不然就输出 no。

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

function find(str, targetStr) {
    return str.includes(targetStr)
}

function judge(is) {
    console.log(is ? 'yes' : 'no')
}

我们很轻易就写出了这四个函数,前面两个是上面就已写过的,然后 find 函数也很简朴,如今我们想经由历程 compose 的体式格局来完成 pointfree,然则我们的 find 函数要吸收两个参数,不相符 compose 参数的划定,这个时刻我们像前面一个例子一样,把 find 函数柯里化一下,然后再举行组合:

// 柯里化 find 函数
function findCurry(targetStr) {
    return str => str.includes(targetStr)
}

const findTaoweng = findCurry('TAOWENG')

const result = compose(judge, findTaoweng, stringReverse, stringToUpper)

看到这里是不是是能够看到柯里化在到达 pointfree 是异常的有效,较少参数,一步一步的完成我们的组合。

然则经由历程上面那种体式格局柯里化须要去修正之前封装好的函数,这也是损坏了开闭准绳,而且关于一些基本函数去把源码修正了,其他处所用了能够就会有题目,所以我们应当写一个函数来手动柯里化。

依据定义之前对柯里化的定义,以及前面两个柯里化函数,我们能够写一个二元(参数个数为 2)的通用柯里化函数:

function twoCurry(fn) {
    return function(firstArg) { // 第一次挪用取得第一个参数
        return function(secondArg) { // 第二次挪用取得第二个参数
            return fn(firstArg, secondArg) // 将两个参数运用到函数 fn 上
        }
    }
}

所以上面的 findCurry 就能够经由历程 twoCurry 来取得:

const findCurry = twoCurry(find)

如许我们就能够不变动封装好的函数,也能够运用柯里化,然后举行函数组合。不过我们这里只完成了二元函数的柯里化,假如三元,四元是不是是我们又要要写三元柯里化函数,四元柯里化函数呢,实在我们能够写一个通用的 n 元柯里化。

function currying(fn, ...args) {
    if (args.length >= fn.length) {
        return fn(...args)
    }
    return function (...args2) {
        return currying(fn, ...args, ...args2)
    }
}

我这里采纳的是递归的思绪,当猎取的参数个数大于或许即是 fn 的参数个数的时刻,就证实参数已猎取终了,所以直接实行 fn 了,假如没有猎取完,就继承递归猎取参数。

能够看到实在一个通用的柯里化函数中心头脑是异常的简朴,代码也异常简约,而且还支撑在一次挪用的时刻能够传多个参数(然则这类通报多个参数跟柯里化的定义不是很合,所以能够作为一种柯里化的变种)。

我这里重点不是讲柯里化的完成,所以没有写得很硬朗,更壮大的柯里化函数可见羽讶的:
JavaScript专题之函数柯里化

部份运用

部份运用是一种经由历程将函数的不可变参数子集,初始化为固定值来建立更小元数函数的操纵。简朴来讲,假如存在一个具有五个参数的函数,给出三个参数后,就会取得一个、两个参数的函数。

看到上面的定义能够你会以为这跟柯里化很类似,都是用来收缩函数参数的长度,所以假如邃晓了柯里化,邃晓部份运用是异常的简朴:

function debug(type, firstArg, secondArg) {
    if(type === 'log') {
        console.log(firstArg, secondArg)
    } else if(type === 'info') {
        console.info(firstArg, secondArg)
    } else if(type === 'warn') {
        console.warn(firstArg, secondArg)
    } else {
        console.error(firstArg, secondArg)
    }
}

const logDebug = 部份运用(debug, 'log')
const infoDebug = 部份运用(debug, 'info')
const warnDebug = 部份运用(debug, 'warn')
const errDebug = 部份运用(debug, 'error')

logDebug('log:', '测试部份运用')
infoDebug('info:', '测试部份运用')
warnDebug('warn:', '测试部份运用')
errDebug('error:', '测试部份运用')

debug要领封装了我们日常平凡用 console 对象调试的时刻种种要领,原本是要传三个参数,我们经由历程部份运用的封装以后,我们只须要依据须要挪用差别的要领,传必需的参数就能够了。

我这个例子能够你会以为没必要这么封装,基本没有削减什么工作量,然则假如我们在 debug 的时刻不仅是要打印到控制台,还要把调试信息保存到数据库,或许做点其他的,那是不是是这个封装就有效了。

由于部份运用也能够削减参数,所以他在我们举行编写组合函数的时刻也占有一席之地,而且能够更快通报须要的参数,留下为了 compose 通报的参数,这里是跟柯里化比较,由于柯里化依据定义的话,一次函数挪用只能传一个参数,假如有四五个参数就须要:

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

// 运用柯里化体式格局来使 add 转化为一个一元函数
let addPreThreeCurry = currying(add)(1)(2)(3)
addPreThree(4) // 10

这类一连挪用(这里所说的柯里化是依据定义的柯里化,而不是我们写的柯里化变种),然则用部份运用就能够:

// 运用部份运用的体式格局使 add 转化为一个一元函数
const addPreThreePartial = 部份运用(add, 1, 2, 3)
addPreThree(4) // 10

既然我们如今已邃晓了部份运用这个函数的作用了,那末照样来完成一个吧,真的是异常的简朴:

// 通用的部份运用函数的中心完成
function partial(fn, ...args) {
    return (..._arg) => {
        return fn(...args, ..._arg);
    }
}

别的不晓得你有无发明,这个部份运用跟 JavaScript 内里的 bind 函数很类似,都是把第一次穿进去的参数经由历程闭包存在函数里,比及再次挪用的时刻再把别的的参数传给函数,只是部份运用不必指定 this,所以也能够用 bind 来完成一个部份运用函数。

// 通用的部份运用函数的中心完成
function partial(fn, ...args) {
    return fn.bind(null, ...args)
}

别的能够看到实际上柯里化和部份运用确切很类似,所以这两种手艺很轻易被殽杂。它们重要的区分在于参数通报的内部机制与控制:

  • 柯里化在每次散布挪用时都邑天生嵌套的一元函数。在底层 ,函数的终究效果是由这些一元函数逐渐组合发生的。同时,curry 的变体许可同时通报一部份参数。因而,能够完整控制函数求值的时候与体式格局
  • 部份运用将函数的参数与一些预设值绑定(赋值),从而发生一个具有更少参数的新函数。改函数的闭包中包含了这些已赋值的参数,在以后的挪用中被完整求值。

总结

在这篇文章里我重点想引见的是函数以组合的体式格局来完成我们的需求,别的引见了一种函数式编程作风:pointfree,让我们在函数式编程内里有了一个最好实践,只管写成 pointfree 情势(只管,不是都要),然后引见了经由历程柯里化或许部份运用来削减函数参数,相符 compose 或许 pipe 的参数请求。

所以这类文章的重点是邃晓我们怎样去组合函数,怎样去笼统庞杂的函数为颗粒度更小,功用单一的函数。这将使我们的代码更轻易保护,更具声明式的特性。

关于这篇文章内里提到的其他观点:闭包、作用域,然后柯里化的其他用处我愿望是在番外篇内里更深切的去邃晓,而这篇文章重要控制函数组合就好了。

参考文章

文章首发于本身的个人网站桃园,别的也能够在 github blog 上找到。

假如有兴致,也能够关注我的个人民众号:「前端桃园」

《JavaScript函数式编程,真香之组合(一)》

    原文作者:leexiaoran
    原文地址: https://segmentfault.com/a/1190000018232767
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞