原文:http://www.ibm.com/developerworks/cn/java/j-ft20/index.html
Java™ 即使不打算立刻改用函数式语言(比如 Scala 或 Clojure),Java 开发人员也应该了解函数式范式。随着时间的推移,所有主流语言都将变得更加函数化;Neal Ford 将在本期文章中探讨其中的原因。
到目前为止,在本系列的每期文章中,我都说明了为什么理解函数式编程非常重要。但是,有些原因是在多期文章中进行说明的,只有在综合思路的更大背景中,才可以完全了解这些原因。在本期文章中,我会探讨函数式编程方兴未艾的所有原因,并综合前几期文章中的一些个人经验教训。
在计算机科学短短的发展历史中,技术的主流有时会产生分支,包括实用分支和学术分支。20 世纪 90 年代的 4GL(第四代语言)是一个实用分支,而函数式编程是来自学术界的一个示例。每隔一段时间,都会有一些分支加入主流,函数式编程目前也是这种情况。函数式 语言不仅在 JVM 上刚刚崭露头脚(其中两个最有趣的新语言是 Scala 和 Clojure),在 .NET 平台上也是才开始得到应用,在 .NET 平台上,F# 是头等公民。为什么所有平台都如此欢迎函数式编程?答案是,随着时间的推移,随着运行时都要能够处理更多的繁忙工作,开发人员已经能够将日常任务的更多控 制权割让给它们。
割让控制权
在 20 世纪 80 年代初,在我上大学的时候,我们使用一个被称为 Pecan Pascal 的开发环境。其独特的特性是,相同的 Pascal 代码可以在 Apple II 或 IBM PC 上运行。Pecan 工程师使用某个称为 “字节码” 的神秘东西实现了这一壮举。开发人员将 Pascal 代码编译为 “字节码”,它可以在每个平台本地编写的 “虚拟机” 上运行。这是一个可怕的体验!所生成的代码慢得让人痛苦,甚至简单的类赋值也非常缓慢。当时的硬件还没有准备好迎接这个挑战。
在发布 Pecan Pascal 之后的十年,Sun 发布了 Java,Java 使用了相同的架构,对于 20 世纪 90 年代中期的硬件环境,运行该代码显得有些紧张,但最终取得了成功。Java 还增加了其他开发人员友好的特性,如自动垃圾收集。使用过像 C++ 这样的语言之后,我再也不想在没有垃圾收集的语言中编写代码。我宁愿花将时间花在更高层次上的抽象上,思考解决复杂业务问题的方法,也不愿意在内存管理等 复杂的管道问题上浪费时间。
Java 缓解了我们与内存管理的交互;函数式编程语言使我们能够用高层次的抽象取代其他核心构建块,并更注重结果而不是步骤。
结果比步骤更重要
函数式编程的特点之一是存在强大的抽象,它隐藏了许多日常操作的细节(比如迭代)。我在本系列文章中一直使用的一个示例是数字分类:确定某个数字是 perfect、abundant 还是 deficient(完整的定义参见 第一期文章)。清单 1 中显示的 Java 实现可以解决这个问题:
清单 1. 自带缓存总数的 Java 数字分类器
import static java.lang.Math.sqrt; public class ImpNumberClassifier { private Set<Integer> _factors; private int _number; private int _sum; public ImpNumberClassifier(int number) { _number = number; _factors = new HashSet<Integer>(); _factors.add(1); _factors.add(_number); _sum = 0; } private boolean isFactor(int factor) { return _number % factor == 0; } private void calculateFactors() { for (int i = 1; i <= sqrt(_number) + 1; i++) if (isFactor(i)) addFactor(i); } private void addFactor(int factor) { _factors.add(factor); _factors.add(_number / factor); } private void sumFactors() { calculateFactors(); for (int i : _factors) _sum += i; } private int getSum() { if (_sum == 0) sumFactors(); return _sum; } public boolean isPerfect() { return getSum() - _number == _number; } public boolean isAbundant() { return getSum() - _number > _number; } public boolean isDeficient() { return getSum() - _number < _number; } }
清单 1 中的代码是典型的 Java 代码,它使用迭代来确定和汇总系数。在使用函数式编程语言时,开发人员很少关心细节(比如迭代,由 calculateFactors()
使用)和转换(比如汇总一个列表,该列表由 sumFactors()
使用),宁愿将这些细节留给高阶函数和粗粒度抽象。
粗粒度的抽象
用抽象来处理迭代等任务,使得需要维护的代码变得更少,因此可能出现错误的地方也就更少。清单 2 显示了一个更简洁的数字分类器,用 Groovy 编写,借用了 Groovy 的函数风格方法:
清单 2. Groovy 数字分类器
import static java.lang.Math.sqrt class Classifier { def static isFactor(number, potential) { number % potential == 0; } def static factorsOf(number) { (1..number).findAll { isFactor(number, it) } } def static sumOfFactors(number) { factorsOf(number).inject(0, {i, j -> i + j}) } def static isPerfect(number) { sumOfFactors(number) == 2 * number } def static isAbundant(number) { sumOfFactors(number) > 2 * number } def static isDeficient(number) { sumOfFactors(number) < 2 * number } }
清单 2 中的代码使用很少的代码完成 清单 1 的所有工作(减去缓存总数,这会重新出现在下面的示例中)。例如,用于确定 factorsOf()
中的系数的迭代消失了,替换为使用 findAll()
方法,它接受一个具有我的筛选器条件的代码块(一个高阶函数)。Groovy 甚至允许使用更简洁的代码块,它允许单参数块使用 it
作为隐含参数名称。同样,sumOfFactors()
方法使用了 inject()
,它(使用 0 作为种子值)将代码块应用于每个元素,将每个对减少为单一的值。{i, j -> i + j}
代码块返回两个参数的总和;每次将列表 “折叠” 成一个对时,都会应用此块,产生总和。
Java 开发人员习惯于框架 级别的重用;在面向对象的语言中进行重用所需的必要构件需要非常大的工作量,他们通常会将精力留给更大的问题。函数式语言在更细化的级别提供重用,在列表和映射等基本数据结构之上通过高阶函数提供定制,从而实现重用。
少量数据结构,大量操作
在面向对象的命令式编程语言中,重用的单元是类以及与这些类进行通信的消息,这些信息是在类图中捕获的。该领域的开创性著作是 Design Patterns: Elements of Reusable Object-Oriented Software(参阅 参考资料), 至少为每个模式提供一个类图。在 OOP 的世界中,鼓励开发人员创建独特的数据结构,以方法的形式附加特定的操作。函数式编程语言尝试采用不同的方式来实现重用。它们更喜欢一些关键的数据结构 (如列表、集和映射),并且在这些数据结构上采用高度优化的操作。传递数据结构和高阶函数,以便 “插入” 这种机制,针对某一特定用途对其进行定制。例如,在 清单 2 中,findAll()
方法接受使用一个代码块作为 “插件” 高阶函数(该函数确定了筛选条件),而该机制以有效方式应用了筛选条件,并返回经过筛选的列表。
函数级的封装支持在比构建自定义类结构更细的基础级别上进行重用。此方法的优势之一已经体现在 Clojure 中。最近,库中的一些巧妙创新重写了 map
函数,使它可以自动并行化,这意味着所有映射操作都可以受益于没有开发人员干预的性能提升。
例如,考虑一下解析 XML 的情况。大量的框架可用于在 Java 中完成这个任务,每个框架都有自定义的数据结构和方法语义(例如,SAX 与 DOM)。Clojure 将 XML 解析为一个标准的 Map
结构,而不是强迫您使用自定义的数据结构。因为 Clojure 中包含大量与映射配合使用的工具,如果使用内置的列表理解函数 for
,那么执行 XPath 样式的查询就会很简单,如清单 3 所示:
清单 3. 将 XML 解释为 Clojure
(use 'clojure.xml) (def WEATHER-URI "http://weather.yahooapis.com/forecastrss?w=%d&u=f") (defn get-location [city-code] (for [x (xml-seq (parse (format WEATHER-URI city-code))) :when (= :yweather:location (:tag x))] (str (:city (:attrs x)) "," (:region (:attrs x))))) (defn get-temp [city-code] (for [x (xml-seq (parse (format WEATHER-URI city-code))) :when (= :yweather:condition (:tag x))] (:temp (:attrs x)))) (println "weather for " (get-location 12770744) "is " (get-temp 12770744))
在 清单 3 中,我访问雅虎的气象服务来获取某个给定城市的气象预报。因为 Clojure 是 Lisp 的一个变体,所有从内部读取是最简单的。对服务端点的实际调用发生在 (parse (format WEATHER-URI city-code))
上,它使用了 String
的 format()
函数将 city-code
嵌入字符串。列表理解函数 for
放置了解析后的 XML,使用 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(参阅 参考资料)。zipper 通过提供坐标系方向,让您可以结构性地导航树。例如,可以从树的根开始,发出 (-> z/down z/down z/left)
等命令,导航到第二级的左侧元素。Clojure 中已经有现成的函数可将解析的 XML 转换为 zipper,在整个树形结构中实现一致的导航。
新的、不同的工具
函数式编程提供了新的工具类型,以优雅的方式解决棘手的问题。例如,Java 开发人员不习惯尽能延迟生成其值的惰性 数据结构。而未来的函数式语言将对这种高级特性提供支持,一些框架将此功能加装到 Java 中。例如,清单 4 所示的数字分类器版本使用了 Totally Lazy 框架(参阅 参考资料):
清单 4. Java 数字分类器通过 Totally Lazy 使用惰性和函数式数据结构
import com.googlecode.totallylazy.Predicate; import com.googlecode.totallylazy.Sequence; import static com.googlecode.totallylazy.Predicates.is; import static com.googlecode.totallylazy.numbers.Numbers.*; import static com.googlecode.totallylazy.predicates.WherePredicate.where; public class Classifier { public static Predicate<Number> isFactor(Number n) { return where(remainder(n), is(zero)); } public static Sequence<Number> getFactors(final Number n){ return range(1, n).filter(isFactor(n)); } public static Sequence<Number> factors(final Number n) { return getFactors(n).memorise(); } public static Number sumFactors(Number n){ return factors(n).reduce(sum); } public static boolean isPerfect(Number n){ return equalTo(n, subtract(sumFactors(n), n)); } public static boolean isAbundant(Number n) { return greaterThan(subtract(sumFactors(n), n), n); } public static boolean isDeficient(Number n) { return lessThan(subtract(sumFactors(n), n), n); } }
Totally Lazy 增加了惰性集合和流畅接口方法,大量使用静态导入,使代码具有可读性。如果您羡慕下一代语言中的某些特性,那么一些研究可能会提供可以解决某个特定问题的特定扩展。
让语言迁就问题
大多数开发人员都将他们的工作误解为接受一个复杂的业务问题,将它转换成 Java 等语言。他们的这种误解是因为 Java 并不是一种特别灵活的语言,它迫使您让自己的想法适应于已经存在的刚性结构。但是,当开发人员使用可塑语言时,他们看到了让语言迁就问题,而不是让问题迁 就语言的机会。像 Ruby(它为领域特定语言 (DSL) 提供了比主流更友好的支持)等语言证明了这种潜在可能。现代函数式语言甚至走得更远。Scala 旨在协调内部 DSL 的托管,并且所有 Lisp(包括 Clojure)都可以提供无与伦比的灵活性,使开发人员能够让语言适应问题。例如,清单 5 使用了 Scala 中的 XML 基元来实现 清单 3 的天气示例:
清单 5. Scala 的 XML 语法修饰
import scala.xml._ import java.net._ import scala.io.Source val theUrl = "http://weather.yahooapis.com/forecastrss?w=12770744&u=f" val xmlString = Source.fromURL(new URL(theUrl)).mkString val xml = XML.loadString(xmlString) val city = xml \\ "location" \\ "@city" val state = xml \\ "location" \\ "@region" val temperature = xml \\ "condition" \\ "@temp" println(city + ", " + state + " " + temperature)
Scala 是为获得可塑性而设计的,它支持操作符重载和隐式类型等扩展。在 清单 5 中,Scala 被扩展为可以使用 \\
操作符支持类似 XPath 的查询。
与语言的趋势相一致
函数式编程的目标之一是最大程度地减少可变状态。在 清单 1 中,有两种类型的共享状态清单。_factors
和 _number
都存在,它们使代码测试变得更容易(编写原代码版本是为了说明最大可测试性),并可以折叠成更大的函数,从而消除它们。但是,_sum
是因为各种原因而存在。我预计,这段代码的用户可能需要检查多个分类。(例如,如果一个完美的检查失败,那么下一次我可能会检查百分比。)合计系数总数的操作可能很昂贵,所以我为它创建了一个经过惰性初始化的访问器。在第一次调用时,它会计算总和,并将它存储在 _sum
成员变量中,以便优化未来的调用。
像垃圾收集一样,现在缓存也可以降级用于语言。清单 2 中的 Groovy 数字分类器忽略了 清单 1 中总数的惰性初始化。如果想要实现同样的功能,可以修改分类器,如清单 6 所示:
清单 6. 手动添加一个缓存
class ClassifierCachedSum { private sumCache ClassifierCachedSum() { sumCache = [:] } def sumOfFactors(number) { if (sumCache.containsKey(number)) return sumCache[number] else { def sum = factorsOf(number).inject(0, {i, j -> i + j}) sumCache.putAt(number, sum) return sum } } // ... other code omitted
在最新版的 Groovy 中,清单 6 中的代码不再是必要的。考虑使用清单 7 中的改进版的分类器:
清单 7. 备忘数字分类器
class ClassifierMemoized { def static dividesBy = { number, potential -> number % potential == 0 } def static isFactor = dividesBy.memoize() def static factorsOf(number) { (1..number).findAll { i -> isFactor.call(number, i) } } def static sumFactors = { number -> factorsOf(number).inject(0, {i, j -> i + j}) } def static sumOfFactors = sumFactors.memoize() def static isPerfect(number) { sumOfFactors(number) == 2 * number } def static isAbundant(number) { sumOfFactors(number) > 2 * number } def static isDeficient(number) { sumOfFactors(number) < 2 * number } }
任何纯函数(没有副作用的函数)都可以备忘,比如 清单 7 中的 sumOfFactors()
方法。备忘函数允许运行时缓存重复出现的值,从而消除手工编写缓存的需要。事实上,请注意执行实际工作的 getFactors()
和 factors()
方法之间的关系,该方法是备忘版本的 getFactors()
。Totally Lazy 还为 Java 增加了备忘功能,这是反馈到主流中的另一个高级函数特性。
由于运行时获得了更多的能力并且有多余的开销,开发人员可以将繁忙的工作割让给语言,将我们解放出来,去思考更重要的问题。Groovy 中的备忘功能就是众多示例中的一个;因为基础运行时允许这样做,所有现代语言都添加了函数式构造,包括 Totally Lazy 等框架。
结束语
因为运行时的能力变得更强,并且语言获得了更强大的抽象,所以开发世界变得更加函数化,这使开发人员可以花费更多的时间来思考结果的影响,而不是思考如何 生成结果。由于高阶函数等抽象出现在语言中,它们将成为高度优化的操作的自定义机制。您不需要创建框架来处理问题(如 XML),您可以将其转换成您已经可以使用工具来处理的数据结构。
随着第 20 期文章的发布,函数式思维 将告一段落,我将准备开始一个新的系列,探索下一代的 JVM 语言。Java 下一代 会让您对不久的将来有一个大致了解,并帮助您对必须投入新语言学习的时间作出明智选择。
相关推荐
标题中的“函数式思维为什么函数式编程越来越受关注”揭示了我们讨论的核心——函数式编程在当前IT领域的兴起。函数式编程是一种编程范式,它强调将计算视为数学函数的评估,而不是指令的序列。这种思维方式有助于...
近年来,函数式编程在IT行业中受到了越来越多的关注,这主要得益于它的一些独特优势和现代计算环境的需求。 首先,函数式编程的核心概念是纯函数。一个纯函数在其执行过程中不会对程序的外部状态产生影响,也不会...
尽管函数式编程在近十多年用得越来越多,但市面上介绍其高阶特性的书却并不多。这本书在这方面是个重要的补充,它不仅仅面向 Scala 程序员,同样面向用任何编程语言开发的程序员,只要你充满好奇心。 ——挖财网...
与传统的过程性编程强调的是如何执行一系列操作不同,函数式编程关注的是数据流及如何对数据进行转换。这种编程方式的核心在于使用纯函数,即函数的结果仅依赖于其输入参数,且没有副作用。这种方式有助于减少代码中...
#### 二、为什么要使用函数式编程? 在传统的命令式编程中,程序员通常关注于如何逐步改变状态来达到目标结果。而函数式编程提倡的是声明式的编程风格,即告诉计算机“做什么”,而不是“怎么做”。这种方式可以...
- 函数式编程思维方式与传统的命令式编程有所不同,需要开发者有一定的学习曲线。 - 企业可能需要投入时间和资源来培训现有的开发团队,或招聘具备相应技能的新员工。 **现有系统集成**: - 零售企业往往已经拥有...
函数式编程:“JS 函数式... 函数式编程并不新鲜,但如今变得越来越流行,因为它是两个主要框架的核心:框架和库根据最新的谷歌趋势,ReactJS 和 Angular(这里不包括 AngularJS 和 VueJS)是最受欢迎的库和框架:Rea
在编程领域,函数式编程(Functional Programming, FP)作为一种编程范式,越来越受到开发者的青睐。本教程"learning-fp"专注于使用Scala语言来深入理解并实践函数式编程。Scala是一种多范式语言,它结合了面向对象...
函数式编程,作为一种优雅而强大的编程范式,近年来在IT领域越来越受到重视。它强调使用不可变数据、纯函数和高阶函数,以及通过组合来构造程序。然而,教授这种抽象思维方式并非易事。"Teaching-fp"项目集合了各种...
近年来,函数式编程在JavaScript社区中越来越受到重视。本文将详细探讨如何用函数式编程的思想对JavaScript进行断舍离,即如何去除那些陈旧、复杂、冗余的编程实践,而转向更为简洁、高效和可维护的编程方式。 首先...
类型系统的严格性可以提高代码的稳定性和可靠性,而函数式编程的思维方式则有助于写出更简洁、更易于维护的代码。 总之,Bisquit是一个用于学习和实践类型推断和函数式编程的工具,它的设计灵感来源于Scala。通过...
在IT领域,特别是软件开发行业中,函数式编程已经成为一种重要的编程范式,它以其独特的思维方式和强大的抽象能力受到越来越多开发者的青睐。本项目名为“Learning-Tracker”,旨在记录一位开发者致力于成为专注于...
首先,我们需要明白什么是函数式编程以及它在JavaScript编程语言中的应用。 函数式编程是一种编程范式,它将计算视作数学函数的评估,并避免改变状态和可变数据。它强调使用纯函数来构建软件,这些纯函数可以定义为...
C语言是一种过程式编程语言,强调函数和数据结构,而Java则是一种面向对象的语言,强调类和对象。在C语言中,我们可能更倾向于将所有逻辑集中在主函数中,而在Java中,我们需要将功能分散到不同的类中,每个类代表一...
1. **函数式编程**:这种编程范型强调计算过程视为函数的组合,避免可变状态和副作用。例如,Haskell 和 Lisp 是典型的函数式语言。通过学习函数式编程,我们可以掌握如何利用高阶函数、纯函数和递归等概念来解决...
对于函数式编程,手册可能涉及lambda表达式、高阶函数、闭包等概念,这些都是现代编程语言中越来越重要的特性。通过这部分的学习,读者可以了解到函数式编程的思维方式,提升代码的简洁性和可维护性。 面向对象编程...
8. **编程思想**:书中还可能探讨一些抽象思维、问题解决策略和创新思考的方式,比如迭代思维、函数式编程、模块化设计等。 9. **最新编程资源**:附带的“拼吾爱 - 最新编程资源的分享下载站.url”可能是一个链接...
这种思维方式在现代企业中越来越受到重视,因为数据已经成为了业务洞察和战略规划的关键来源。面向数据的思维模式包括以下几个关键步骤: 1. 数据获取:了解数据的来源,包括数据库、API、日志文件等,以及如何有效...
尤其是近年来,随着Node.js的崛起和前端开发的复杂化,函数式编程思想在JavaScript中的应用越来越广泛。"Applied Functional JavaScript"不仅是一个主题,更是一种编程哲学的体现,它旨在通过函数式的思维方式来解决...