本系列旨在将您的观点重新定位为实用的心态,帮助您以新的方式看待常见问题并找到改善日常编码的方式。 它探讨了函数式编程的概念,允许在Java语言中进行函数式编程的框架,在JVM上运行的函数式编程语言以及语言设计的一些未来方向。 该系列面向那些了解Java及其抽象如何工作,但很少或没有使用功能语言经验的开发人员。
到目前为止,在本系列的每一期中,我已经说明了理解函数式编程很重要的原因。 但是,有些原因跨越了各个阶段,并且只有在更大范围的组合思想中才变得完全清楚。 在本期中,我将结合前期的各个课程,探讨功能性编程之所以兴起的所有原因。
在短短的计算机科学历史上,技术的主流有时会产生分支,无论是实践分支还是学术分支。 1990年代的4GL(第四代语言)是实际的分支,而功能编程是学术界的一个例子。 偶尔会有一个分支加入主流,这就是现在函数式编程正在发生的事情。 功能语言不仅在JVM(Scala和Clojure两种最有趣的新语言)上崭露头角,还在.NET平台上发芽,F#是一流的公民。 为什么所有平台都支持函数式编程? 答案是随着时间的流逝,随着运行时变得能够处理更多的繁忙工作,开发人员已经能够将更多的日常任务控制权交给他们。
在1980年代初期,当我上大学时,我们使用了称为Pecan Pascal的开发环境。 它的独特功能是相同的Pascal代码可以在Apple II或IBM PC上运行。 美国山核桃工程师通过使用一种神秘的“字节码”实现了这一壮举。 开发人员将其Pascal代码编译为此“字节代码”,该代码在为每个平台本地编写的“虚拟机”上运行。 这是一次可怕的经历! 即使对于简单的类分配,生成的代码仍然非常缓慢。 当时的硬件还无法应对挑战。
在Pecan Pascal发表十年之后,Sun使用相同的体系结构发布了Java,尽管如此,但在1990年代中期的硬件环境中却颇为艰难。 它还添加了其他对开发人员友好的功能,例如自动垃圾收集。 在使用C ++等语言工作之后,我再也不想使用非垃圾收集语言进行编码了。 我宁愿花时间在更高的抽象级别上思考解决复杂业务问题的方法,而不是诸如内存管理之类的复杂管道问题。
Java简化了我们与内存管理的交互; 函数式编程语言使我们能够用更高阶的抽象替换其他核心构建块,并更加注重结果而不是步骤。
函数式编程的标志之一是强大的抽象的存在,这些抽象隐藏了平凡的操作(例如迭代)的许多细节。 我在本系列中使用的一个示例是数字分类-发现数字是完美的 , 丰富的还是不足的 (有关完整定义,请参阅第一部分 )。 清单1中显示了解决此问题的Java实现:
清单1中的代码是使用迭代来确定和求和因子的典型Java代码。 使用函数式编程语言,开发人员sumFactors()在乎诸如迭代(由calculateFactors() )和诸如对列表求和(由sumFactors() )之类的转换的细节,而是更喜欢将这些细节让与高阶函数和粗粒度抽象。
处理诸如迭代之类的任务的抽象的存在减少了要维护的代码,因此减少了可能发生错误的地方。 清单2显示了数字分类器的简要版本,使用其功能样式化方法用Groovy编写:
中的代码清单2确实所有清单1确实(减去缓存总和,这将在下面的示例重新出现)与显着更少的代码。 例如,通过使用findAll()方法来确定factorsOf()因子的迭代消失了,该方法接受具有我的过滤条件的代码块(一个高阶函数)。 Groovy通过让单参数块将it用作隐式参数名称,甚至允许使用更简短的代码块。 类似地, sumOfFactors()方法使用inject() ,它以0作为种子值,将代码块应用于每个元素,从而将每对减少为一个值。 {i, j -> i + j}代码块返回两个参数的和。 在我一次“折叠”列表对时应用此块即可得出总和。
Java开发人员习惯于在框架级别重用。 在面向对象的语言中进行重用的必要技巧需要付出很大的努力,以至于它们通常只用于较大的问题。 通过支持通过高阶函数进行的自定义,功能语言在基本数据结构(如列表和地图)之上提供了更细粒度的重用。
在面向对象的命令式编程语言中,重用的单位是类,它们与之通信的消息被捕获在类图中。 该领域的开创性工作,“ 设计模式:可重用的面向对象软件的元素” (请参阅参考资料 ),每个模式至少包含一个类图。 在OOP世界中,鼓励开发人员创建独特的数据结构,并以方法的形式附加特定的操作。 函数式编程语言不会尝试以相同的方式实现重用。 他们更喜欢一些关键数据结构(例如列表,集合和映射),并对这些数据结构进行高度优化的操作。 您传递数据结构和高阶函数以“插入”此设备,以针对特定用途对其进行自定义。 例如,在清单2中 , findAll()方法接受一个代码块作为确定过滤条件的“插入式”高阶函数,并且该机制以一种有效的方式应用过滤条件,返回过滤后的列表。
与构建自定义类结构相比,功能级别的封装可以在更细致的基础级别上重用。 这种方法的一个优势已经出现在Clojure中。 库中最新的巧妙创新已将map功能重写为可自动并行化,这意味着所有地图操作均会受益于性能提升,而无需开发人员干预。
例如,考虑解析XML的情况。 Java中存在许多用于此任务的框架,每个框架都有自定义数据结构和方法语义(例如,SAX与DOM)。 Clojure将XML解析为标准的Map结构,而不是强迫您使用自定义数据结构。 因为Clojure包含许多用于处理地图的工具,所以使用内置的list-comprehension函数for来执行XPath样式的查询非常简单,如清单3所示:
在清单3中 ,我访问Yahoo的气象服务以获取给定城市的天气预报。 由于Clojure是Lisp的变体,因此从内到外阅读最容易。 对服务端点的实际调用发生在(parse (format WEATHER-URI city-code)) ,它使用String的format()函数将city-code嵌入到字符串中。 列表理解函数for将使用xml-seq转换的xml-seq放置到名为x的可查询映射中。 :when谓词确定匹配条件; 在这种情况下,我正在搜索一个标签(转换为Clojure关键字) :yweather:condition 。
要了解用于从数据结构中提取值的语法,查看其中的内容很有用。 解析后,对气象服务的相关调用将返回此摘录中显示的数据结构:
({:tag :yweather:condition, :attrs {:text Fair, :code 34, :temp 62, :date Tue, 04 Dec 2012 9:51 am EST}, :content nil})由于Clojure已针对与地图一起使用进行了优化,因此关键字成为包含它们的地图上的函数。 清单3中对(:tag x)的调用是“从存储在x的映射中检索与:tag键对应的值”的简写。 因此, :yweather:condition产生与该键关联的映射值,其中包括我通过使用相同语法来获取:temp的attrs映射。
Clojure最初令人生畏的细节之一是与地图和其他核心数据结构进行交互的看似无止境的方式。 但是,这反映出Clojure中的大多数事情都试图解决这些核心的,优化的数据结构这一事实。 它不是将解析的XML捕获在唯一的框架中,而是尝试将其转换为工具已经存在的现有结构。
依赖基本数据结构的一个优势出现在Clojure的XML库中。 为了遍历树形结构(例如XML文档),在1997年创建了一个有用的数据结构,称为zipper (请参阅参考资料 )。 拉链允许您通过提供坐标方向在结构上导航树。 例如,从树的根开始,您可以发出诸如(-> z/down z/down z/left)来导航到第二级左元素。 Clojure中已经存在用于将解析的XML转换为拉链的功能,从而可以在所有树形结构中进行一致的导航。
函数式编程提供了新型的工具,可以用优雅的方式解决棘手的问题。 例如,Java开发人员不习惯于惰性数据结构,因为它们会尽可能长时间地延迟其值的生成。 未来派功能性语言提供了对此类高级功能的支持,但某些框架将此功能改型为Java。 例如,清单4中出现的数字分类器版本使用Totally Lazy框架(请参阅参考资料 ):
完全懒惰添加了懒惰集合和流利接口方法,大量使用了静态导入来使代码可读。 如果您羡慕下一代语言的某些功能,那么一些研究可能会得出解决特定问题的特定扩展。
大多数开发人员在错误的观念下工作,他们的工作是解决一个复杂的业务问题并将其翻译成Java之类的语言。 他们之所以这样做,是因为Java在语言方面并不是特别灵活,因此迫使您将您的想法塑造成已经存在的刚性结构。 但是,随着开发人员使用可扩展语言,他们看到了将语言更多地针对自己的问题而不是针对他们的语言的机会。 像Ruby之类的语言(对领域特定语言(DSL)的支持要优于主流语言)证明了这种潜力。 现代函数式语言走得更远。 Scala旨在容纳主机内部DSL,并且所有Lisps(包括Clojure)在使开发人员针对问题建模时都具有无与伦比的灵活性。 例如,清单5使用Scala中的XML原语来实现清单3的天气示例:
Scala是为可延展性而设计的,允许进行扩展,例如运算符重载和隐式类型。 在清单5中 ,Scala进行了扩展,以允许使用\\运算符进行类似XPath的查询。
函数式编程的目标之一是最小化可变状态。 在清单1中 ,两种类型的共享状态清单。 _factors和_number存在使代码易于测试(编写此代码的原始版本是为了说明最大的可测试性),并且可以折叠成更大的函数来消除它们。 但是, _sum存在的原因有所不同。 我预计此代码的用户可能需要检查多个分类。 (例如,如果对完整性的检查失败,那么我可能会在下一次检查丰度。)求和这些因子的操作可能很昂贵,因此我为此创建了一个延迟初始化的访问器。 首次调用时,它将计算总和并将其存储在_sum成员变量中以优化将来的调用。
像垃圾收集一样,缓存现在可以降级为该语言了。 清单2中的Groovy数字分类器省略了清单1中出现的sum的惰性初始化。 如果要实现相同的功能,则可以更改分类器,如清单6所示:
在最新版本的Groovy中,不再需要清单6中的代码。 考虑清单7中分类器的改进版本:
可以记住任何纯函数(一个没有副作用的函数),如清单7中的sumOfFactors()方法所示 。 记忆该功能允许运行时缓存重复值,从而无需手写缓存。 实际上,请注意进行实际工作的getFactors()与factors()方法之间的关系,该方法是getFactors()的备注版本。 Totally Lazy还向Java添加了备忘录,这是另一种高级功能,可以反馈到主流中。
随着运行时获得更多功能和额外开销,开发人员可以将忙碌的工作让给该语言,使我们有更多的时间去思考更重要的问题。 Groovy中的记忆化是许多示例之一。 所有现代语言都在底层运行时允许的范围内添加了功能构造,包括诸如Totally Lazy之类的框架。
随着运行时功能的增强和语言功能的增强,开发世界变得越来越功能化,从而使开发人员可以花更多的时间思考结果的含义,而不是如何生成结果。 随着诸如高阶函数之类的抽象语言的出现,它们成为高度优化操作的定制机制。 您可以将其转换为已经可以使用的工具的数据结构,而不是创建处理XML之类的框架。
随着第20期的发布, 功能性思维将不再起作用,而我将继续探索探索三种下一代JVM语言的新系列。 Java.next可以让您瞥见您的不久的将来-并帮助您对必须投入新语言学习的时间做出明智的选择。
翻译自: https://www.ibm.com/developerworks/java/library/j-ft20/index.html