函数式编程中的数组问题

Posted WebHub

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了函数式编程中的数组问题相关的知识,希望对你有一定的参考价值。

这里只传授最高端的编程技巧...


好久没讲技术了,先回忆一下啥是函数式编程(FP)吧,比如FP要求使用表达式,不允许出现语句,这样更接近自然语言。




表达式取代经典语句


什么叫语句呢?学校编程课本上教的变量声明语句,循环语句,条件判断语句,枚举语句,这些都是语句,也就是说我们再熟悉不过的if/else语句,for/while循环,switch以及try/catch都不给用了!


函数式编程中的数组问题

没有这些语句还编个P程啊?我当时也有一种“这些年编程白学了”的冲动,虽然官方说每一种语句都可以用对应的表达式来替代,比如在javascript领域,变量声明省略掉关键词后就变成了表达式:


  • 变量声明语句


// 变量声明语句+赋值let test = 123;
// 变量申明+赋值表达式test = 123;


因为变量总是属于当前函数的变量对象(variable object),声明变量等同于给对象添加属性,所以变量申明表达式返回赋的值或者undefined。


  • if/else语句


函数式替换if/else语句也很简单,我们本来就有条件运算符(… ? … : …)可用:


// 条件语句if(convention){}else {}
// 条件表达式convention ? expression1 : expression2;


  • switch语句


switch语句的话可以用js散列表来模拟,也就是对象:


// 状态枚举语句switch (expression) { case value1:    break; case value2:    break;  default:    break;}
// 字典表达式({  value1(){},  value2(){},})[expression] || default();


  • try&catch语句


至于try/catch/finally可以将同步流包裹进promise,再给他监听一个catch方法:


// 异常处理语句try{ // 代码块}catch(err){ }finally{}
// 异常处理表达式new Promise((res,rej)=>{  // 代码块}).catch(err=>{ }).finally(()=>{})


以上这些表达式都完美替换了经典语句,但是我在“如何取代循环语句”问题上思考了很久,循环语句不同于上面几种,循环问题是最复杂的,光语句语法就有for和while等好几种,如何取代这些傻吊语句成了一个问题。下面我来一一讨论一下,表达式是否能够完美的替换循环语句。


数组问题


Array对象(数组或者叫列表)是JavaScript里最重要的一个类,也是原型链上方法最多的一个。事实上JS里一切对象都是(散)列表。首先,所有循环都要使用数组,因为数组的长度(n)是衡量循环的时间复杂度的标准,通常循环一遍的复杂度就是O(n)。


  • 循环遍历


我们最常见的循环就是遍历一个数组,那直接可以利用数组的forEach方法来遍历:


// 遍历数组语句for(let i=0; i<list.length; i++){}
// 遍历数组方法list.forEach(item=>{})


  • 指定循环次数


for循环语句中经常出现需要指定循环的次数而没有数组,我们可以通过构造一个定长数组来遍历:


// 指定次数循环语句for(let i=0; i<n; i++){}
// 指定次数循环表达式Array(n).fill(true).forEach(()=>{})


  • continue中断本次迭代


continue关键词的作用是提前结束本次迭代进程,赶紧进入下一次迭代。在函数式数组的遍历中只要使用return结束当前回调的执行就行啦。


// continue语句while (expression) {   if (condition) { continue;   }}
// 用return结束当前迭代函数list.forEach(()=>{ if (condition) { return; }})


  • break结束循环


和continue不同,break关键词会结束整个循环,forEach传的回调函数永远会执行列表的长度遍,所以forEach没用,同理map和filter等一系列数组遍历方法都不能用。可喜的是,数组有一些“可中断的遍历方法”,比如find方法本意是寻找一个数组元素,找到后就可以中断遍历;比如some方法本意是是否有“一些”元素符合回调条件,遍历时一旦匹配到一个就会停止向下匹配;比如every方法本意是是否“所有”元素都符合回调条件,遍历时只要发现1个元素不符合就会停止向下匹配。所以函数式编程中有3个数组方法可以实现循环的break。


// 传统break语句for(let item of list){  if(condition)break;}
// 函数式break// findlist.find(item=>{  if(condition)return true;})// somelist.some(item=>{  if(condition)return true;})// everylist.some(item=>{  if(condition)return false;})


  • 无限循环


取代无限循环语句只要递归调用自己就好啦~


// 无限循环语句while(true){}
// 无限循环表达式(function loop(){ loop();})();


  • 异步循环(划重点)


异步循环是最难的模拟的一个。假如我们有一个异步任务列表asyncTasks,想要串行执行而不是并行执行,也就是一个接着一个运行,如果想要并行执行任务非常简单,只要Promise.all(asyncTasks)就行了,但能不能实现一个Promise.sequential呢?如果任务数量确定可以直接.then().then()...来链式调用,但如果数量是动态的就得用循环了。首先模拟一个tasks列表,其中每个元素都是async函数,即返回promise的函数:


tasks = [2000, 1000, 3000].map(time => async () => { await new Promise(res => setTimeout(res, time)); console.log(time);})


使用循环语句来顺序执行非常舒适,但如果你尝试使用forEach来遍历就会出现问题:


// 异步链用循环语句+await非常合适for(task of tasks){ await task();}
// 但是这样你会发现,若干个异步任务并发执行了!tasks.forEach(async (task)=>{  await task();})


使用forEach,回调函数虽然是异步的,但是这个回调函数在一瞬间被并发执行了n次,每一次之间没有等待,导致串行失败。追根揭底,forEach无法顺序执行异步任务的原因是,回调函数每次执行完全独立,没有关联。贯穿Array原型链上几十种遍历方法中,似乎只有reduce和sort等寥寥几个方法可以实现前后关联。我们来模拟一个吧,利用reduce来polyfill一个Promise.sequential方法。


Promise.concurrent = Promise.all;Promise.sequential = tasks => tasks.reduce(async (chain, nextTask) => { await chain; return nextTask();}, Promise.resolve());
Promise.sequential(tasks) .then(()=>console.log('finished'));// 依次打印2000,1000,3000,'finished'


老衲的解释:这里利用reduce将一系列promise串了起来,合成了一个大的promise,本质上仍然是通过.then将一个个promise链起来。注意,在async函数中即使return了一个promise.resolve(123),函数返回值将是另一个promise,只是解析值都是123。


经过本文的分析,所有的JavaScript语句,无论是声明,条件,枚举,循环还是流程控制语句,统统可以用函数表达式来替换,让JS成为第一个只由表达式组成的通用编程语言。如果认为我有遗漏的地方或者说还有哪些语句是不可取代的,欢迎在底下留言评论。




参考


  • https://css-tricks.com/why-using-reduce-to-sequentially-resolve-promises-works/

  • https://stackoverflow.com/questions/24586110/resolve-promises-one-after-another-i-e-in-sequence

  • https://jakearchibald.com/2017/await-vs-return-vs-return-await/

  • https://jimmy.blog.csdn.net/article/details/91038735


(完)



【日记】

看看本文的参考链接,可以发现外网站点都习惯于将文章的标题放在url上作为文章ID,这种习惯的好处就是可以从url上直接读出内容的主题,而我们的站点url很多都是一个个文章编号。不得不说,这些专业论坛的文章的不仅质量高,url的设计也很有语义。

以上是关于函数式编程中的数组问题的主要内容,如果未能解决你的问题,请参考以下文章

函数式编程 JS 中的数组赋值

函数式编程中的数组问题

函数式编程/命令式编程

JS函数式编程

函数式编程

VSCode自定义代码片段—— 数组的响应式方法