当前位置: 首页 > 科技观察

让程序员变得更好:函数式思维和函数式编程

时间:2023-03-18 22:04:19 科技观察

作为Haskell语言[1]的完全新手,当我第一次看到用这种语言编写的快速排序算法的优雅示例时,我立即对这种语言产生了浓厚的兴趣。这是一个例子:quicksort::Orda=>[a]->[a]quicksort[]=[]quicksort(p:xs)=(quicksortlesser)++[p]++(quicksortgreater)wherelesser=filter(=p)xs我很困惑。这么简单漂亮,能行吗?确实,这种写法并不是最佳快速排序的“完全正确”的实现。但是,我不想在这里深入探讨性能问题[2]。我想强调的是,纯函数式编程是一种思维的转变,是一种完全不同的编程思维方式和方法,相当于你要重新开始学习另一种编程方法。首先,让我定义一个问题并以函数式的方式解决它。基本上我们要做的就是按升序对数组进行排序。为了完成这个任务,我使用了曾经改变我们世界的快速排序算法[3]。下面是它的一些基本排序规则:如果数组只有一个元素,当返回多于一个元素的数组时,随机选择一个基点元素P,将数组分成两组。使第一组中的所有元素p。然后在这两组数据上递归地应用这个算法。那么,如何以函数式的方式思考,以函数式的方式编程呢?在这里,我将模拟同一个程序员的两次内心对话。这两种内心的想法是截然不同的。一个是使用命令式编程的思维模式,这是这个程序员从开始学代码就形成的思维模式。.而第二颗心做了一些思想上的改造,洗掉了之前形成的所有偏见:用功能性的方式思考。事实上,程序员就是我,我正在写这篇文章。你会看到两个完全不同的我。没有谎言。让我们在这个简单的例子上与Java进行比较:publicclassQuicksort{privateint[]numbers;私人整数;publicvoidsort(int[]values){if(values==null||values.length==0){返回;}这。数字=值;数字=值.长度;快速排序(0,数字-1);}privatevoidquicksort(intlow,inthigh){inti=low,j=high;intpivot=数字[低+(高-低)/2];while(i<=j){while(numbers[i]pivot){j--;}如果(i<=j){交换(i,j);我++;j--;}}如果(低[a]->[a]JAVA:我将从简单的开始,如果是空数组,如果数组是空,我应该返回这个数组。但是......该死的,当这个数组为空时,程序崩溃了。让我通过在排序方法的开头添加一个if语句来防止这种情况。如果(values.length==0||values==null){返回;}HASKELL:首先很简单,一个空列表。对于这种情况,需要使用模式匹配。我看看怎么用,ok,太好了!quicksort[]=[]JAVA:好的,现在让我对正常情况使用递归。一般情况下,需要记录sort方法参数的状态。需要它的长度,所以我还需要向Quicksort类添加一个新属性。publicvoidsort(int[]values){if(values.length==0||values==null){返回;}this.numbers=values;this.length=values.length;快速排序(0,长度-1);}哈斯克尔:这已经是递归的了。无需再做任何事情。Nocode.Nothing.Nada.That'sgood.JAVA:现在,我需要根据上面解释的规则来实现快速排序的过程。我选择第一个元素作为基点元素,这个不需要使用其他的单数方法。比较,递归。每次比较同时从两端遍历,一个从头到尾(i,生成一个p的列表)。每次在i方向遍历中,发现j方向遍历的当前值大于j方向遍历的当前值,则交换它们的位置。当i的位置超过j时,停止比较,在新形成的两个队列上继续递归调用。privatevoidquicksort(intlow,inthigh){inti=low,j=high;intpivot=数字[低];while(i<=j){while(numbers[i]pivot){j--;}如果(i<=j){交换(i,j);我++;j--;}}如果(低=p)xs我试过了使用最详细的语言讲解在Java中使用迭代+递归实现快速排序。但是,如果我们在java代码中少了一个i++,在while循环条件中出错,会发生什么?嗯,这是一个相对简单的算法。但是我们可以想象,如果我们整天写这样的代码,整天面对这样的程序,或者如果这个排序只是一个非常复杂的算法的第一步,会发生什么。当然可以用,但难免会产生潜在的、内部的bug。现在让我们看看这些关于状态的陈述。如果由于某种原因数组为空并变为null,那么当我们调用Java版本的快速排序方法时会发生什么?性能上还有同步执行的问题。如果16个线程要同时访问Quicksort方法怎么办?我们需要监控它们,或者每个线程都有一个实例。越来越乱了最终它归结为编译器问题。编译器应该足够聪明,能够“猜出”要做什么以及如何优化[5]。程序员不要去想怎么索引,怎么处理数组。程序员应该思考数据本身,如何根据需要对数据进行改造。您可能认为函数式编程增加了算法思考和数据操作的复杂性,但事实并非如此。编程世界中普遍存在的命令式编程思维方式阻碍了我们的发展。事实上,您根本不必放弃您最喜欢的命令式编程语言来使用Haskell进行编程。Haskell语言有其自身的缺点[6]。只要能接受函数式编程思想,就能写出更好的Java代码。通过学习函数式编程,您可以成为更好的程序员。看到下面的Java代码了吗?publicListsort(Listelements){if(elements.size()==0)returnelements;Streamlesser=elements.stream().filter(x->x.compareTo(pivot)<0).collect(Collectors.toList());Streamgreater=elements.stream().filter(x->x.compareTo(pivot)>=0).collect(Collectors.toList());列表sorted=newArrayList();sorted.addAll(快速排序(较小));sorted.add(枢轴);sorted.addAll(快速排序(更大));返回排序;它类似于Haskell代码吗?没错,可能你现在使用的Java版本无法正确运行,这里使用了lambda函数,这是Java8[7]中引入的一种非常酷的语法。看,函数式语法不仅让程序员变得更好,而且让编程语言变得更好。函数式编程是编程语言向更高抽象层次的自然演变。正如我们认为用C开发Web应用程序效率低下一样,多年来我们对命令式编程语言也有同样的看法。使用这些语言是程序员在开发时间上的一种权衡。为什么许多初创公司选择Ruby而不是使用C++来开发他们的应用程序?因为它们可以缩短开发周期。不要误解。我们可以将程序员比作云计算单元。一个程序员一个小时的时间比高性能AWS集群服务器一个小时的时间要贵得多。通过让犯错变得更难、出现错误的可能性降低以及使用更高的抽象进行设计,我们可以让程序员更有效率、更有创造力、更有价值。标记:[1]Haskell从零开始,来自“学习Haskell大有裨益!”[2]我在这里展示的Haskell快速排序不是就地快速排序,因此它失去了它的一个属性,即内存效率.Haskell中的就地版本更像是:importqualifiedData.Vector.GenericasVimportqualifiedData.Vector.Generic.MutableasMqsort::(V.Vectorva,Orda)=>va->vaqsort=V.modifygowheregoxs|M.lengthxs<2=return()|otherwise=dop<-M.readxs(M.lengthxs`div`2)j<-M.unstablePartition(