数组函数与最小功率法则

2020-07-20 04:05:37

1998年,万维网的发明者蒂姆·伯纳斯·李(Tim Berners-Lee)提出了最小功耗原则:

计算机科学在20世纪60年代到80年代花费了大量的精力来制造尽可能强大的语言。如今,我们必须意识到选择最有力的解决方案而不是最弱的解决方案的原因。

在语言的计算能力和确定该语言中的程序正在做什么的能力之间有一个重要的权衡。

用功能较弱的语言表达约束、关系和处理指令可以提高信息重用的灵活性:语言功能越弱,您可以使用该语言存储的数据就越多。

事实上,Berners-Lee基于以下规则选择不让HTML成为真正的语言:

我选择HTML不是一种编程语言,因为我想让不同的程序用它做不同的事情:以不同的方式表示它、提取目录、索引它,等等。

虽然最低功耗规则针对的是编程语言本身,而不是语言特性,但我认为同样的想法仍然适用。代码的功能越弱,就越容易推理。

因此,有趣的是,有人说像.filter、.map和.duce这样的函数数组函数比它们粗糙的for循环替代函数更强大。我会说相反的话:他们的力量要小得多,这才是关键所在。

毫无疑问,调用这些函数的人可能指的是它们的总体能力(例如,能够调用array.map(...).filter(...)),或通过并行处理启用的能力,或通过将回调赋值给一级函数变量所提供的能力。

但我想请大家注意,从设计上看,这些功能在单独考虑时的功率实际上是很低的。

下面是我制作的一个图表,它粗略地对常见的javascript数组函数进行了排序,从功能最强的(for循环)到功能最弱的(.some/.each)。

在解释我所说的强大是什么意思之前,我们先简要回顾一下不同方法的实际用途:

For-loop:迭代代码块,通常是为了在循环内产生副作用(比如追加到数组中)。

.forEach:迭代数组中的每个元素,并在每次迭代中对该元素执行某些操作。再说一次,通常是为了在某些时候产生副作用。

.duce:从左到右迭代数组以累加一些值,可能在开始时显式初始化,在每次迭代中,我们获取当前数组项并返回累加器的新值(直到我们在末尾返回最终值)。

.map:对于数组中的每个原始项,根据要放置在输出数组的相应索引中的原始项返回一个新项

.filter:从左到右,对于数组中的每一项,如果它满足某些条件,则将其包含在输出数组中。

.Every:如果数组中的每一项都满足某个条件,则返回true,否则返回false。

.some:如果数组中的任何项满足某个条件,则返回true,否则返回false。

这篇帖子更多的是关于选择使用哪一个,而不是解释每一个都做了什么。有关很好的参考资料,请参阅此处。

在这里,我是在模仿蒂姆·伯纳斯-李(Tim Berners-Lee)的造币法,但当我说强大的时候,我真的是指灵活的。如上所述,此功能可以满足多少个用例?具体地说,我将函数A定义为比函数B更强大,如果它可以用自己的术语实现函数B,并且还可以做函数B可以做的其他事情。*。

这意味着根据我的定义(我并不宣称它是通用的),for循环比.forEach更强大,因为您可以通过for循环实现.forEach。例如:

Const forEach=(array,callback)=>;{for(i=0;i<;array.length;i++){callback(array[i])}}for Each([1,2,3],a=>;console.log(A))>;1>;2>;3[1,2,3].forEach(a=>;console.log(A))>;1>;2。

Const Reduce=(array,callback,initialValue)=>;{let result=initialValue array.forEach((Item)=>;{result=callback(result,item)})return result}duce([1,2,3],(acc,curr)=>;acc+curr,0)>;6[1,2,3].duce((acc,curr)=>;acc+curr,0)&

值得注意的是,我们的一些定制不像ECMAScript那样处理未定义的值,但是您可以理解其中的意思。

为什么不对所有东西都使用for循环呢?这样,我们只需要记住一种迭代数组项的方法。这与你不使用手榴弹杀死蚊子的原因是一样的:手榴弹是非法的,黑市商品被标上标签,以补贴小贩承担的风险。

说实在的:选择功能最弱的工具有很多原因,但对我来说,最重要的两个原因是:1)减少出错的机会2)容易被别人理解。

对于仍能完成工作的作业来说,功能最弱的工具就是出错几率最小的工具。考虑这样一种情况,我有一个数字数组,并且希望返回数组中每项加倍的结果:

Const myArray=[1,2,3]//with`.map`result tWithMap=myArray.map(Item=>;Item*2)>;[2,4,6]//For(i=0;i<;myArray.length-1;i++){result tWithLoop.ush(array[i]*2)}ResultWithLoop>;[2,4,6]for(i=0;i<;myArray.length-1;i++){result tWithLoop.ush(array[i]*2)}result tWithLoop>;[2,2。

嘿,见鬼的是什么?为什么我的ResultWithLoop缺少一个项目?我从零开始索引,每次只递增一个,并且通过确保不包括索引myArray.length处的元素来确保不会出现越界错误。

哦,等等,我的for循环中的<;应该是<;=(或者我可以从myArray.length-1中删除-1)。是我的错。

For-循环太强大了,根本不关心您实际使用它的目的。也许你真的想排除最后一个元素,它怎么会知道呢?幸运的是,我们很早就发现了这一点,但无论你是错过了一个=,还是错过了一个手榴弹别针,有时当你意识到自己的错误时,已经太晚了。

.map在这里是合适的选择,因为它是一种抽象,隐藏了在列表中循环每个项的控制流,这意味着您不可能出错。使用.map时,可以保证结果中的元素与原始map一样多,并且输出数组中的每个元素都是输入数组中相应元素的函数**。

将上面的for-loop方法和.map方法进行比较,作为阅读器,哪种方法更容易解析?如果您只熟悉for-循环,那么您会选择它,但是考虑到.map在当今编程语言中无处不在,现在可能是学习它的时候了。对于熟悉这两种方法的人来说,.map方法要容易得多:

您不需要通读如何在for循环中操作i变量,因为这是抽象出来的。

您不必担心原始变量是否在每次迭代中发生变异。

甚至不需要查看传递给.map的回调函数,您就可以很清楚地知道应该从结果中得到什么。对于for-循环就不能这么说了。

同样,假设我有一系列水果,我想知道它是否包含苹果。下面是几种方法:

Const Fruits=[';橙子,';梨;,';Apple&39;,&39;Apple&39;,&39;Pach&39;]Const hasAppleViaFilter=Fruits(Fruits=';Apple&39;).Length>;0>;trueconst有AppleViaFind=Fruits(。Trueconst有AppleViaSome=水果。一些(水果=水果=#39;苹果)>;true。

每种方法都按功率递减排序。注意到了吗?有些是最好看的?一旦您看到,您就知道hasAppleViaSome将被赋予一个布尔值,这是基于回调FORUE=>;FUILE=&';Apple';。在筛选器方法中,您需要记住这样一个事实,即我们正在使用原始数组的结果子集创建一个数组,然后我们重新检查它的长度,并与零进行比较。在过滤器方法中,您需要记住这样一个事实:我们正在用原始数组的结果子集创建一个数组,然后我们重新检查它的长度,并将其与零进行比较。只有在解析了所有这些之后,您才会意识到实际的隐式意图,这恰好与.Some方法的显式意图相同。

这些只是一些小示例,但是当您有一个内部包含大量代码的毛茸茸的大型回调时,读者可以看到它仍然只是一个对.ome的调用,并且可以放心,回调所做的一切都是返回true或false。这校准了读者的预期,并使其更容易处理回调中发生的事情。

Const hasAppleViaContrivedSome=Fruits。Some(Fruits=>;{If(Typpeof水果!==';String';){return false}If(Fruits=';Par&39;){Return False}If(Fruits=';橙色&39;){Return False}If(Fruits=';禁果';){Return False}If(Fruits=';String';){Return False}If(水果.substring(1。}返回FALSE})。

另一方面,当有人看到你的代码,看到一个强大的函数用来执行像调用这样琐碎的事情时,他们会比他们在你通常放苍蝇拍的地方偶然发现手榴弹的时候更困惑。

像Haskell这样的核心函数式语言不允许在函数中出现副作用,这意味着在.map中处理项的顺序对输出没有影响,因此可以通过并行运行回调调用来提高效率。对于javascript则不是这样:在所有上述函数中,语言都允许副作用。

那么,在其中一个函数的回调中隐藏一些副作用有什么害处呢?假设您正在检查我们的水果列表中是否有苹果,但是您还想将遇到的任何橙子的索引附加到orangeIndexs数组中。您可能会忍不住这样做:

常量水果=[';橙子,';梨';,';苹果';,';苹果';,';桃子';]让orangeIndex=[]const有苹果=水果。一些((水果,索引)=>;{if(水果类型!==#39;字符串';){return false}if(水果='}){return false}if(水果=';){return false}if(水果='。橙色){orangeIndexes.ush(Index)//如果(水果=#39;禁果){返回假}if(水果=#39;苹果';){返回真}返回假},则某些突变肯定不会伤害返回假}如果(水果=#39;苹果';){返回真}返回假})如果(水果=#39;苹果';){返回真}返回假}如果(水果=#39;苹果';){返回真}返回假}[0]。

不是的。这真的很糟糕。通过改变调用中的变量,我们误导读者认为回调符合根据结果返回TRUE或FALSE的预期行为,而实际上它直接处理回调范围之外的变量。往好了说,这证明是暂时的混乱,往坏了说,它削弱了读者的信任,即他们遇到的下一个低功耗功能将名副其实。

在这种情况下,你有两个选择:1)切换到更强大的功能,停止对你的读者撒谎2)找到一种更干净的方式来实现你想要的行为

Const Fruits=[';橙子,';梨';,';苹果';,';苹果';,';桃';]让橙色指数=[]让苹果=假水果。for Each((水果,索引)=>;{if(水果类型!==';字符串';){return}if(水果=&){return}if(水果=&。){orangeIndexes.ush(Index)return}if(水果=#39;禁果';){return}if(水果=#39;苹果';){hasApple=true return}})是否有Apple>;true orangeIndex>;[0]。

常量水果=[';橙子,';梨;,';苹果;,#39;苹果,#39;苹果,桃子]常量橙色指数=水果。Reduce((acc,curr,index)=>;curr=&39;橙色&39;?Acc.contat(Index):ACC,[])const有Apple=Fruits。有些(Fruit=>;Fruit=>;Fruits=>;Apple&39;)有Apple>;true ororangeIndex>;[0]。

我并不是说选项2比选项1更好。有时,尽管它是正确的,但函数解决方案可能只是比可变替代方案更难阅读,特别是在非类型化语言中。

我要说的是,选项1和选项2都大大优于我们一开始的半功能半变异代码:这里的重要收获是,从.duce到.一些回调函数的副作用侵蚀了这些函数传达作者意图的表达能力,而且比那篇关于蚊子季节家用手榴弹爆炸的新闻文章更难阅读。

在做了懒人和懒人之后,我花了相当多的时间在围棋世界里,那里没有地图,没有过滤器,也没有减少。虽然我在这篇文章中对for循环进行了相当多的分析,但值得注意的是,强制使用for循环的语言有一些好处:

1)学习一个控制结构比学习本文中的所有不同函数更容易2)当速度很重要时,具有可变副作用的for循环可能比替代函数更快3)在类型化语言中,map/filter/duce需要泛型类型,这使得类型系统更复杂,这意味着编译时间更慢,学习曲线更陡。

话虽如此,哦,我的天哪,我迫不及待地想知道Go引入了泛型,我不再需要编写for循环来检查一组水果是否包含一个苹果。

如果您有幸使用一种支持过滤/映射/还原和好友的语言,那就使用它们吧!

如果您使用的是非类型化语言,有时需要判断调用来在公开底层类型的可变方法和更难解析的函数方法之间进行选择(例如.duce vs.forEach)。

但是,无论你是在写代码还是在捕杀蚊子,如果你能从这篇文章中学到什么,那就一定要问问自己:

前面我说过,我想关注单个阵列函数是如何低功耗的,即使它们的组合被证明是高功耗的。这就回避了一个问题:是否有一些时候,数组函数的组合被证明过于强大,阻碍而不是帮助读者理解?

这里有一个例子,大致模拟了我遇到的真实情况:假设我们有一个系统,它通过product Click模型跟踪某个网站上的产品点击情况,该模型引用该产品和点击该产品的客户。我们需要一个函数,该函数在给定一组ductClick ID时,返回一个包含产品和客户的对象数组(不需要包括单击对象本身)。我们还必须跳过任何无法找到客户/产品的点击。

FetchProductClick:(products ClickId:Number)=>;ProductClick|nullfetchProduct:(ductClick:ProductClick)=>;Product|nullfetchCustomer:(products Click:ProductClick)=>;Customer|null。

让我们也假设这些函数的运行成本比较高,但是添加批量获取数据的函数的问题已经积压得很严重,我们现在没有时间来解决这个问题。

Const fetchProductClickInfo=ductClickIds=>;{let Results=[]products tIds.forEach(ductClickId=>;{const products tClick=fetchProductClick(Products TClickId)if(products tClick=null){return}const product=fetchProduct(Products TClick)if(product==null){return}const Customer=fetchCustomer(Products TClickId)if(product==null){return}const Customer=fetchCustomer(DuctClickId)if(product==null){return}const Customer=fetchCustomer(DuctClickId)。

每当找不到对象时,我们都会利用提前返回,最后,如果我们有产品和客户,我们会将它们放入结果数组中。

您可能会说,我们可以使用.duce做同样的事情,而不会降低可读性,我同意这一点。但为什么要止步于Reduce呢?您还可以通过合成.map和.filter来实现相同的行为,如下所示:

Const fetchProductClickInfo=ProductClickIds=>;ProductClickIds.map(products ClickId=>;fetchProductClick(Products TClickId)).filter(products Click=>;products Click!==null).map(ductClick=>;({ProductClick,product:fetchProduct(ProductClick),}).filter(products ClickInfo。

好的,这不仅比使用.forEach循环的示例更难阅读,而且实际上引入了与需求无关的复杂性。对于我们获取的每个对象,如果找不到它,我们需要停止该进程,这意味着每个.map后面必须跟一个.filter,以检查空值。这意味着我们在数组中重新循环的次数远远超过了需要的次数。

我们不希望在同一个.map回调中同时获取产品和客户,以防产品为空,这会使获取客户(一个昂贵的操作)变得多余。

更重要的是,尽管我们不想将产品包括在最终结果中单击,但我们仍然需要将其贯穿第二个.map,以便我们可以使用它来获取第三个.map中的客户。真让人头疼!

这里要吸取的教训是,尽管一些单独的功能可能是低功耗的,但这并不意味着这些功能的组合本身就是低功耗的。如果所需的行为需要一些高性能的东西,那么您需要仔细考虑哪种方法引入的外部复杂性最小。仅仅因为您更熟悉.map和.filter而不是可怕的.duce,并不意味着您应该在.duce更合适的时候使用它们(甚至是for循环)。

根据我的个人经验,在比较这些方法时,性能从来不是什么大问题,正如我发现的那样:1)在前端应用程序中,很少处理大型数组;2)在像Reaction这样的框架中,速度慢通常来自于在每次渲染只需要执行一次操作时不必要地执行某些操作,这意味着useMemo挂钩比更改处理数据的方式更好的解决方案。

但是在javascript中,有很多时候性能确实很重要。因此,我使用Benchmark.js进行了一些基准测试,比较了不同方法在执行地图操作时的表现:

对于按升序排列的1000个数字的数组,在每次迭代中递增项目,结果如下:

MAP x 386,175次/秒±13.96%(91次采样)对于每个在位x 193,596次/秒±0.78%(92次采样)循环x 190,224次/秒±0.36%(96次采样)对于每个在位x 444,796次/秒±0.61%(93次采样)对于每个在位x 805,311次/秒±19.99%(。

如果不考虑就地突变,那么对于简单的转换(如递增数字),.map要远远优于替代方案。如果您允许原始数组的就地突变,则forEach将分得一杯羹,其速度大约是.map和就地for循环的两倍。

对于包含1000个对象的数组,在每次迭代中合并一个键/值对,我们得到:

MAP x 48,774次/秒±11.75%(86次采样)对于每个环路减少x 215次/秒±2.39%(83次采样)x 45,489次/秒±2.12%(92次采样)对于环路x 48,263次/秒±4.41%(93次采样)对于每个就地x 3,440次/秒±0.69%(90次采样)对于环路在位x 3,430次/秒±1.31%(91次运行。

因此,当转换需要创建新对象时(例如,将键/值对合并到对象中时),一切都会较慢,但for循环是最快的。就地.forEach和。

.