学会像一个函数式程序员那样思考

原创
开发 开发工具
函数式编程已经开始流行起来了,它宣称具有更少的缺陷和更高的生产力。尽管很多开发者曾经尝试过函数式编程,但是依然无法理解是什么让函数式语言如此引人注目。通常学习一种新语言的语法是相对容易的,但是学会以不同的方式去思考却很难。Neal Ford在他的函数式思考专栏里面介绍了一些函数式编程的概念,并讨论如何在Java和Groovy中去运用这些概念。

【51CTO独家特稿】在开始进入正题之前,我们先来做一个比喻。假设你是一个伐木工人,你拥有一把这个森林里最好的斧子,而它使也你成为了当地最有生产力的伐木工人。 某一天,有人向你展示并称赞了一个新的伐木工具--电锯。由于销售人员是一个非常能推销的人,所以你买了一把电锯回来,尽管你并不知道如何去用。于是你尝试像以前砍树那样的来回摆动去锯树。并且你很快得出了一个结论这个新式的电锯毫无用处,于是你又重新拿起斧子去伐木。一直到有人过来并给你演示了如何去运转电锯,你才明白这里的不同。

你可能联想到了用函数式编程来代替故事中的电锯。但是问题在于函数式编程是一种全新的编程模式,而不是一门新的语言,语法只是一个细节问题。而最不同的地方是要如何以不同的方式去思考。而我作为一名“电锯演示者”和一个函数式程序员来到了这里。

欢迎来到函数式思维专栏。这个系列将探索函数式编程的话题,但是并不仅仅局限在函数式编程语言有关的内容上。正如我描绘的那样,以函数式的方法来写代码涉及到了设计,权衡,代码重用和其他一系列的观点。我会尝试着以Java(或是类Java语言)的方式尽可能多的展示函数式编程的概念, 进而演示一些其他语言的能力-那些Java不具有的能力。当然我不会直接切入的非常深,然后讨论一些时髦的事物。取而代之的是,我会逐渐演示一种新的思考问题的方式(或许你已经在某些地方用了,但还没有意识到)。

在接下来的两部分里,你可以把它当作是有关于函数式编程话题的一个旅行。其中的某些概念将会有大量的细节,在这个系列中我会用更多的情景和细节去描述。在旅程开始前,我将带你看一下一个相同问题的两个不同实现,一个用传统的方式来写,另一个使用更多的函数式方式。

数字归类

谈论两种不同的编程模式,你必须用代码来做比较。第一个例子是我另一本书《The Productive Programmer》和《测试驱动设计1,2》两篇文章中的一个变体。我选取了少量的代码,因为在这两篇文章里已经深入的分析了这段代码。这些文章对这个设计所做的称赞并没有错,但我想在这里进一步提供一个不同的设计意图。

问题的需求是这样的:假设给定任意一个正整数都大于1,你必须按照完美的,过剩的和不足的进行归类。一个完美数正好是它所有整除因子的总和。同样地,一个过剩数的所有整除因子总和大于该数,而一个不足数的所有整除因子总和小于该数。

快速数字归类器

列表1中的类(NumberClassifier)满足所有这些需求:

  1. public class Classifier6 { 
  2.     private Set<Integer> _factors; 
  3.     private int _number; 
  4.   
  5.     public Classifier6(int number) { 
  6.         if (number < 1) 
  7.             throw new InvalidNumberException("Can't classify negative numbers"); 
  8.         _number = number; 
  9.         _factors = new HashSet<Integer>>(); 
  10.         _factors.add(1); 
  11.         _factors.add(_number); 
  12.     } 
  13.   
  14.     private boolean isFactor(int factor) { 
  15.         return _number % factor == 0; 
  16.     } 
  17.   
  18.     public Set<Integer> getFactors() { 
  19.         return _factors; 
  20.     } 
  21.   
  22.     private void calculateFactors() { 
  23.         for (int i = 1; i <= sqrt(_number) + 1; i++) 
  24.             if (isFactor(i)) 
  25.   
  26.                 addFactor(i); 
  27.     } 
  28.   
  29.     private void addFactor(int factor) { 
  30.         _factors.add(factor); 
  31.         _factors.add(_number / factor); 
  32.     } 
  33.   
  34.     private int sumOfFactors() { 
  35.         calculateFactors(); 
  36.         int sum = 0; 
  37.         for (int i : _factors) 
  38.             sum += i; 
  39.         return sum; 
  40.     } 
  41.   
  42.     public boolean isPerfect() { 
  43.         return sumOfFactors() - _number == _number; 
  44.     } 
  45.   
  46.     public boolean isAbundant() { 
  47.         return sumOfFactors() - _number > _number; 
  48.     } 
  49.   
  50.     public boolean isDeficient() { 
  51.         return sumOfFactors() - _number < _number; 
  52.     } 
  53.   
  54.     public static boolean isPerfect(int number) { 
  55.         return new Classifier6(number).isPerfect(); 
  56.     } 

这段代码有几处地方需要关注一下:

它拥有大范围的测试(有一部分我是为了讨论测试驱动开发而写的)注:这条所说的测试位于作者另一篇文章中。

这个类由大量的紧耦合方法组成,在它的构造函数中拥有测试驱动开发的边际效应。

在calculateFactors()方法里内嵌了性能优化算法。这个类的主体是由采集因子组成,因此我可以在之后对它们进行求和并进行最终的归类。整除因子总是以成对的形式被获取。例如,如果这个数是16,当我采集的因子为2时,我就能得到另一个因子为8,因为8x2=16。如果我获得的因子是成对的,那么我只需要去检查那些有平方根的数,这就是calculateFactors()方法所做的事情。

更多的功能归类

使用相同的测试开发技术,我创建了一个修改后的版本。列表2,更丰富的功能数字归类器

  1. public class NumberClassifier { 
  2.   
  3.     static public boolean isFactor(int number, int potential_factor) { 
  4.         return number % potential_factor == 0; 
  5.     } 
  6.   
  7.     static public Set<Integer> factors(int number) { 
  8.         HashSet<Integer> factors = new HashSet<Integer>(); 
  9.         for (int i = 1; i <= sqrt(number); i++) 
  10.             if (isFactor(number, i)) { 
  11.                 factors.add(i); 
  12.                 factors.add(number / i); 
  13.             } 
  14.         return factors; 
  15.     } 
  16.   
  17.     static public int sum(Set<Integer> factors) { 
  18.         Iterator it = factors.iterator(); 
  19.         int sum = 0; 
  20.         while (it.hasNext()) 
  21.             sum += (Integer) it.next(); 
  22.         return sum; 
  23.     } 
  24.   
  25.     static public boolean isPerfect(int number) { 
  26.         return sum(factors(number)) - number == number; 
  27.     } 
  28.   
  29.     static public boolean isAbundant(int number) { 
  30.         return sum(factors(number)) - number > number; 
  31.     } 
  32.   
  33.     static public boolean isDeficient(int number) { 
  34.         return sum(factors(number)) - number < number; 
  35.     } 

这两个版本的类尽管差别细微但是很重要。最主要的区别是例2的版本缺少了状态共享。消除状态共享在函数式编程中是比较受欢迎的一种抽象手法。作为跨方法共享状态的替代方案,我采用直接调用的方式来消除状态共享。从设计的角度来说,它让factors()方法变的更长,但是它也防止了factors字段暴漏到方法之外。注意,例2是完全由静态方法组成的。在方法间不存在知识共享的问题,因此我可以在更少函数范围上做封装。一旦你给它们输入参数和期待值,这些方法都会工作的很好(这个是一个纯函数例子,这个概念我在将来会进一步探索它)。

函数

函数式编程属于一个宽泛的计算机科学范畴,它已经受到了极大的关注。有新的基于JVM上开发的函数式语言(如scala和clojure)和框架(如Functional Java和Akka),它们都声称能够带来更少的缺陷,更高的生产力,更易读,更赚钱等等。相比驻足门外的去解决函数式编程这一大话题,我更愿意将注意力放在一些概念以及这些概念衍生出来的话题上。

函数式编程的核心就是函数, 正如在面向对象语言里面类是主要的抽象那样。函数形成了处理过程的基础,同时它具有其他传统语言没有的一系列特性。

高阶函数

高阶函数可以将其他函数作为参数或者作为返回结果。这在Java语言中是无法想像的。最接近的方案是你使用一个类(通常是匿名类)作为执行方法的“持有者”。Java没有独立的函数(或方法),因此它们不能作为返回值或参数出现。

这个能力对函数式语言来说很重要的原因有两点:第一,拥有高阶函数就意为着你可以在如何连结语言元素上作出一个假设。例如,你可以构建一个机制来消除一个类继承体系上的一大堆方法,通过遍历列表并对每一个元素应用一个(或多个)高阶函数来实现。(我将展示一个简短的例子给你)第二,通过将函数作为返回值,你有机会去创建一个高动态,适应性的系统。

通过使用高阶函数我们就可以使问题服从于方案,但高阶函数对函数式语言来说并不是唯一的。因此,当你在使用函数式思维的时候, 你解决问题思路就会不一样。考虑一下列表3中的例子,一个保护数据访问的方法:

  1. public void addOrderFrom(ShoppingCart cart, String userName, 
  2.                      Order order) throws Exception { 
  3.     setupDataInfrastructure(); 
  4.     try { 
  5.         add(order, userKeyBasedOn(userName)); 
  6.         addLineItemsFrom(cart, order.getOrderKey()); 
  7.         completeTransaction(); 
  8.     } catch (Exception condition) { 
  9.         rollbackTransaction(); 
  10.         throw condition; 
  11.     } finally { 
  12.         cleanUp(); 
  13.     } 
  14. }   

列表3中的代码执行初始化等具体任务,如果所有操作都成功就完成事务,反之回滚,并在最后清理掉资源。很明显,代码有一部分可以被重用,并且我们在面向对象语言中也常常创建这样的结构。在这个例子中,我组合使用了两个“四人团”的设计模式:模版方法和命令模式。模版方法建议我应该移动一些通用的模版代码到继承体系中,并推迟算法细节到子类。命令行模式提供了一个方法以众所周知的执行语义来封装行为到类,列表4就是列表3代码应用这两个模式之后的样子:

列表4 重构顺序后的代码

  1. public void wrapInTransaction(Command c) throws Exception { 
  2.     setupDataInfrastructure(); 
  3.     try { 
  4.         c.execute(); 
  5.         completeTransaction(); 
  6.     } catch (Exception condition) { 
  7.         rollbackTransaction(); 
  8.         throw condition; 
  9.     } finally { 
  10.         cleanUp(); 
  11.     } 
  12.   
  13. public void addOrderFrom(final ShoppingCart cart, final String userName, 
  14.                          final Order order) throws Exception { 
  15.     wrapInTransaction(new Command() { 
  16.         public void execute() { 
  17.             add(order, userKeyBasedOn(userName)); 
  18.             addLineItemsFrom(cart, order.getOrderKey()); 
  19.         } 
  20.     });                 
  21. }   

在列表4中,我提取了一部分通用的代码到wrapInTransaction()(这个样式你可能认识-这是最简单的Spring事务模版的版本)方法中,传递一个命令对象作为工作单元。addOrderFrom()方法包含了一个匿名内部类的创建,这个类以命令模式封装了两个工作单元。

封装行为纯粹是Java的设计产物,我需要用到一个不包含任何形式的,独立行为的命令类。Java中所有的行为都必须驻留在一个类中。甚至语言的设计者很早的就看到了这个不足,但是显然在发布后再去考虑不将行为联接到类上就有些晚了。因此在JDK1.1中纠正了这个缺陷,通过添加匿名内部类的方式来实现。这只是以一种语法糖的方式来为少量的方法创建一大堆小类,这样做仅仅是从纯功能角度出发,而非从结构上。如果想看有关Java这方面有趣的文章,请看Steve Yegge’s的《Execution in the Kingdom of Nouns》。

尽管我非常想要类里面的这个方法,但Java还是强制我去创建一个命令类的实例。这个类本身没有任何用处:它没有字段,没有构造器(这个由java自动生成),并且也没有状态。它纯粹的目的就是为了在方法里包装行为。在函数式语言里,我们通过高阶函数来取代这个模式。

如果我不准备用Java的类,那么我可能采用最接近的语义是函数式编程里面的闭包。列表5显示了重构后的例子,但是使用Groovy代替了Java。

列表5, 使用Groovy的闭包代替命令类

  1. def wrapInTransaction(command) { 
  2.   setupDataInfrastructure() 
  3.   try { 
  4.     command() 
  5.     completeTransaction() 
  6.   } catch (Exception ex) { 
  7.     rollbackTransaction() 
  8.     throw ex 
  9.   } finally { 
  10.     cleanUp() 
  11.   } 
  12.   
  13. def addOrderFrom(cart, userName, order) { 
  14.   wrapInTransaction { 
  15.     add order, userKeyBasedOn(userName) 
  16.     addLineItemsFrom cart, order.getOrderKey() 
  17.   } 
  18. }   

在Groovy里面,任何位于大括号{}之间的东西都是一个代码块,并且代码块可以被当作参数来模仿一个高阶函数。在这种情景下,Groovy为你实现了命令模式。Groovy中的每一个闭包块就是一个Groovy的闭包类型,它包含一个call()方法。当你把一对空括号放到变量后面用于保存闭包实例时,该方法会被自动调用。Groovy启用了一些类函数式编程的行为,通过在语言本身使用相应的语法糖来构建适当的数据结构。正如我将会逐步展示的那样,Groovy也包含其他函数式语言的能力。我将在下面的部分继续对闭包和高阶函数做一些有意思的比较。

第一级函数

函数被认为是函数式语言里面的一等公民,这就意味着函数可以出现在任何地方,正如其他语言的构造体(如变量)那样。在思考不同解决方案的时候,第一级函数的存在允许函数以一种特别的方式来使用,如应用同样的比较操作到相同的数据结构上。这就体现了函数式语言的一个基本思考原则:关注结果,而不是过程。

在命令式的编程语言里,我必须考虑算法的每一个原子操作。如列表1的代码显示的那样。为了实现数字归类器,我不得不精确的识别如何去采集整除因子,这就意为着为了确定一个因子,我不得不写代码去遍历所有数字。但是像遍历列表,然后对每一个元素实施操作,这听起来像是很通用的东西。考虑使用Functional Java框架来重新实现数字归类器的代码,代码如列表6所示:

列表6. 函数式的数字归类器

  1. public class FNumberClassifier { 
  2.   
  3.     public boolean isFactor(int number, int potential_factor) { 
  4.         return number % potential_factor == 0; 
  5.     } 
  6.   
  7.     public List<Integer> factors(final int number) { 
  8.         return range(1, number+1).filter(new F<Integer, Boolean>() { 
  9.             public Boolean f(final Integer i) { 
  10.                 return number % i == 0; 
  11.             } 
  12.         }); 
  13.     } 
  14.   
  15.     public int sum(List<Integer> factors) { 
  16.         return factors.foldLeft(fj.function.Integers.add, 0); 
  17.     } 
  18.   
  19.     public boolean isPerfect(int number) { 
  20.         return sum(factors(number)) - number == number; 
  21.     } 
  22.   
  23.     public boolean isAbundant(int number) { 
  24.         return sum(factors(number)) - number > number; 
  25.     } 
  26.   
  27.     public boolean isDeficiend(int number) { 
  28.         return sum(factors(number)) - number < number; 
  29.     } 
  30. }   

列表6和列表2的不同在于两个方法:sum()和factors()。在Functional Java里, Sum()方法具有List类的foldLeft()方法优势。列表操作概念上的一个具体变化就是被称之为catamorphism,它是列表折叠上的一般化。在这里“向左折叠”的意思是:

1. 携带一个初始值并组合它到列表的第一个元素上

2. 携带结果并应用相同的操作到下一个元素上

3. 一直操作直到列表结束

注意当你对一堆数求和的时候,所做的事情是非常明显的:从零开始,加上第一关元素,携带结果去加第二个,重复这个过程直到所有列表的元素都被处理。Functional Java提供高阶函数(在这个例子里就是Intergers.add枚举器)并小心翼翼的为你的代码启用它。(当然Java真的没有高阶函数,但是你可以通过限制具体的数据结构和类型来写一个较类似的东西)。

在列表6里面另一奇妙的方法是factors(),它充分说明了我关于“关注结果,而不是过程”的建议。发现一个数的整除因子这个问题的本质是什么?换个方式来说,给出一个到目标数的所有可能数的列表。那么我该如何确定哪个数是这个数的整除因子?这里的建议是进行一次过滤操作 – 我能过滤整个列表,消除那些不符合我标准的数。这个方法基本上就如同以下描述:取得1到这个数的范围;用f()方法来过滤这个列表,Functional Java的方式将允许你创建一个具有特殊数据类型的类,并返回结果。

这段代码同时也描绘了一个更大的概念,一个编程语言的趋势。回到过去,开发人员不得不处理一大堆烦人的东西,如内存分配,垃圾回收和指针。随着时间的推移,语言本身背负起了更多这方面的职责。就像计算机越来越强大一样,我们把越来越多的现实任务丢给了语言和运行时。作为一名Java开发者,我比较倾向于把所有的内存问题都交给语言处理。函数式编程扩大了这个需求,并包含了更多的细节。随着时间的推移,我们将花费更少的时间去关心每一个步要解决的问题和思考的过程。随着本系列的进展,我将展示更多相关的例子。

结论

函数式编程更多的是一种观念而不是一个工具或者一门语言。在开始的部分,我曽提到过一些函数式编程的议题,范围从简单设计的讨论到某些宏观问题的重新思考。我写了一个简单的Java类,并让它更符合函数式编程理念,然后开始用传统的命令式语言来构建部分函数式语言的方式去深入探讨了这些议题。

另外引申出两个重点:首先是关注结果而非过程。函数式编程尝试着使用不同的方式去表达问题,因为你已经构建不同的代码块来帮助项目成长。第二点是我一直在这个系列中展示的那样,将那些单调的细节交给编程语言和运行时去处理。这样将有助于我们将注意力集中我们的编程问题上。在下一部分,我将继续着眼与函数式编程语言的常规方面,并介绍如何将它应用于现时的软件开发当中。

 

 

责任编辑:彭凡 来源: 51CTO
相关推荐

2020-07-10 09:55:15

程序员技能开发者

2014-01-08 09:26:05

程序员招聘

2013-06-05 10:49:57

招聘招聘程序员

2012-11-12 10:46:37

2020-02-22 21:51:43

程序员Microsoft SServerSQL

2012-12-17 10:50:27

程序员

2020-10-05 21:13:37

程序员技能开发者

2014-01-06 09:33:32

程序员管理

2021-07-01 07:43:41

项目程序员代码

2014-05-12 10:12:09

程序员

2012-11-28 13:25:27

程序员

2023-12-26 18:47:32

2015-06-16 10:31:36

程序员

2015-06-08 10:48:39

程序员程序员自白

2011-02-14 13:05:17

PythonWeb

2019-11-07 15:30:00

EmacsIDE

2019-04-22 10:25:52

程序员技术职场

2012-04-12 14:49:31

程序员

2009-02-12 15:07:57

程序员创业经验

2016-12-02 19:14:16

数据科学大数据
点赞
收藏

51CTO技术栈公众号