到目前为止,在本系列的每期文章中,我都说明了为什么理解函数式编程非常重要。但是,有些原因是在多期文章中进行说明的,只有在综合思路的更大背景中,才可以完全了解这些原因。在本期文章中,我会探讨函数式编程方兴未艾的所有原因,并综合前几期文章中的一些个人经验教训。

在计算机科学短短的发展历史中,技术的主流有时会产生分支,包括实用分支和学术分支。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.sqrtclass 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.Sourceval 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 sumCacheClassifierCachedSum() {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 下一代 会让您对不久的将来有一个大致了解,并帮助您对必须投入新语言学习的时间作出明智选择。

转载自:http://www.ibm.com/developerworks/cn/java/j-ft20/

函数式思维: 为什么函数式编程越来越受关注相关推荐

  1. 函数式思维: 运用函数式思维,第2 部分

    关于本系列 本系列文章旨在将您的思维方式向函数式思维方式调整,使您以全新的角度来思考常见问题,并提高您的日常编码工作.本系列介绍了函数式编程概念,函数式编程在Java 语言中运行的框架.在JVM 上运 ...

  2. 函数式思维: 利用 Either 和 Option 进行函数式错误处理 类型安全的函数式异常...

    当您研究函数式编程等深奥学科时,令人着迷的分支偶尔会出现.在 函数式思维:函数设计模式,第 3 部分 中,我在迷你系列中继续以函数的方式重新思考传统的 Gang of Four 设计模式.在下一期文章 ...

  3. 你是如何离编程越来越远的?

    点击上方"CSDN",选择"置顶公众号" 关键时刻,第一时间送达! 第一节编程课你按照老师所说的,一行行打印出"Hello World"时, ...

  4. 学编程可以培养编程思维,而编程思维是什么?解读IT入门知识

    大家经常听说"学编程可以培养编程思维",而编程思维是什么?学编程和培养编程思维有什么联系你知道吗? 一.什么是编程? 在了解编程思维之前,首先,要和大家说一说什么是编程. 编程就是 ...

  5. 函数式编程语言python-Python函数式编程

    在 Python 中使用函数式编程的最佳实践! 简 介 Python 是一种功能丰富的高级编程语言.它有通用的标准库,支持多种编程语言范式,还有许多内部的透明度.如果你愿意,还可以查看 Python ...

  6. 纯函数式编程语言_函数式编程正在接管具有纯视图的UI。

    纯函数式编程语言 by Bobby Schultz 由Bobby Schultz 函数式编程正在接管具有纯视图的 UI . (Functional Programming is taking over ...

  7. java里函数式表达式_Java8函数式编程 (一) 数据流和lambda表达式

    JDK 1.8中引入了函数式编程(functional programming,FP),如果您已习惯OOP,一定会感到困惑:什么是函数式编程?这样的编程模式有什么好处? 本文将通过简单的实例令读者对函 ...

  8. 编程小TIPS:使用函数式风格Either来编程

    最近在一些国外的技术博客中见到一个以前自己没太见过的编程风格,那就是Either,觉得非常有意思,稍微了解了下.分享给大家. 同时,我会基于最流行的后端语言Java来简单的演示下如何使用Either. ...

  9. 5. 设计模式之对象思维:面向对象编程有哪些优势?

    一.编程语言 VS 编程范式 现在我们一说到"面向对象编程"似乎感觉就是编程的全部,实际上它是 20 世纪 60 年代就已经出现的一门"古老"技术,在 2000 ...

最新文章

  1. 计算机林中鸟歌曲,励志歌曲曲-林中鸟
  2. 配置腾讯云服务器-2021-3-27
  3. python判断字符是否为数字或字母
  4. Asigra无代理备份:“云”数据保护的先行者
  5. php双写绕过,高并发下缓存与数据库双写不一致解决方案
  6. 今晚直播:非自回归神经机器翻译 | PhD Talk #24
  7. 用toad实现oracle数据迁移,Oracle 使用TOAD实现导入导出Excel数据
  8. C语言 *p++/*(p)++/*(p++)/*p++ - C语言零基础入门教程
  9. 中国移动公布2019年智能家庭网关集采结果:华为、中兴中标
  10. 基于Centos搭建Maven 安装与使用
  11. sas 服务器版安装文件,SAS软件各个版本,包括服务器版本的切磋了解
  12. Centos7配置软RAID+LVM
  13. 15个值得推荐的个人提升方法
  14. matlab二阶阻尼震荡衰减,二阶欠阻尼电路的零响应输入-MATLAB课程设计.doc
  15. SLAM_旋转运动学_两个坐标系速度v和加速度a的关系
  16. speedoffice表格中如何插入子表
  17. 时间序列:时间序列模型---随机游走过程(The Random Walk Process)
  18. 京东数据化运营(五)— 行业分析篇
  19. Python 必备面试基础知识-3
  20. 《Deep Learning (Ian Goodfellow)》机器学习基础

热门文章

  1. html中td自动换行问题
  2. 用计算机语言说一局情话,情话大全:经典爱情语言、爱情句子、名句大全(15)...
  3. 还不会用Flutter?仿网易云音乐项目(已开源)
  4. 服装店如何利用好积分?
  5. 惠州免费旅游景点 惠阳 大亚湾篇(主页更多区域整合)
  6. Nokia MeeGo的故事
  7. ip地址分类和网段详解
  8. C# GridView的分页使用 附代码
  9. 利用R通过顺企网根据公司名称爬取企业地址
  10. python抖音视频上传_使用 python 发布超清抖音视频