Java 下一代: 函数式编码风格——Groovy、Scala 和 Clojure 共享的函数结构及其优势
本文内容
- 命令式处理
- 函数式处理
- 函数式编程的优势
所有 Java 下一代语言都包括函数式编程结构,让您可以从一个更高的抽象层面来思考问题。然而,语言间术语的不同使得难以看到类似的结构。本期文章将展示常见的函数式编程结构在 Java 下一代语言中的表示方式,指出那些功能在实现细节上的一些细微差别。
当垃圾回收成为主流时,它消除了所有类别的难以调试的问题,使运行时能够为开发人员管理复杂的、容易出错的进程。函数式编程旨在为您编写的算法实现同样的优化,这样您就可以从一个更高的抽象层面开展工作,同时运行时执行复杂的优化。
Java 下一代语言并不都占用从命令式到函数式的语言频谱的同一位置,但都展现出函数功能和习语。函数式编程技术有明确定义,但语言有时为相同的函数式概念使用不同的术语,使得我们很难看到相似之处。在本期文章中,我比较了 Scala、Groovy 和 Clojure 的函数式编码风格并讨论了它们的优势。
命令式处理
首先,探讨一个常见问题及其命令式解决方案。
假如给定一个名称列表,有些只有一个字符。要求用逗号做分割符返回列表中的名称,字符串中不包含单字母名称,每个名称的首字母都大写。实现该算法的 Java 代码如清单 1 所示。
清单 1. 命令式处理
public class TheCompanyProcess {
public String cleanNames(List<String> listOfNames) {
StringBuilder result = new StringBuilder();
for(int i = 0; i < listOfNames.size(); i++) {
if (listOfNames.get(i).length() > 1) {
result.append(capitalizeString(listOfNames.get(i))).append(",");
}
}
return result.substring(0, result.length() - 1).toString();
}
public String capitalizeString(String s) {
return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
}
}
由于必须处理整个列表,解决清单 1 中问题最简单的方式是使用一个命令式循环。对于每个名称,都要进行检查,确认其长度是否大于 1,如果长度大于 1 将首字母大写的名称附加到 result
字符串,并在后面加逗号。最终字符串中的最后一个名称不应包含逗号,所以我将它从最后返回值中移走。
在命令式编程中,建议您在较低级上别执行操作。在 清单 1 中的 cleanNames()
方法中,执行了三个任务:筛选 列表以消除单字符,将列表中每个名称的首字母变换 为大写,然后将列表转化 为一个字符串。在命令式语言中,我不得不为三个任务都使用同一低级机制(对列表进行迭代)。函数式语言将筛选、变换和转化视为常见操作,因此它们提供给您从不同视角解决问题的方式。
函数式处理
函数编程语言与命令式语言的问题分类方式不同。筛选、变换和转化逻辑类别表现为函数。那些函数实现低级变换并依赖于开发人员来编写作为参数传递的函数,进而定制函数的行为。我可以用伪代码将 清单 1 中的问题概念化为:
listOfEmps -> filter(x.length > 1) -> transform(x.capitalize) ->
convert(x, y -> x + "," + y)
利用函数式语言,您可以建模这一概念性解决方案,无需担心实现细节。
这在 .NET 中很常见——链式语法,并且,在 .NET 中,有很多函数委托,比如 Action、Func 等,函数委托可以使用 Lambda 表达式。
Scala 实现
清单 2 使用 Scala 实现清单 1 中的处理示例。它看起来就像是前面的伪代码,包含必要的实现细节。
清单 2. Scala 处理
val employees = List("neal", "s", "stu", "j", "rich", "bob")
val result = employees
.filter(_.length() > 1)
.map(_.capitalize)
.reduce(_ + "," + _)
对于给定的名称列表,首先筛选,剔除长度不大于 1 的所有名称。然后将该操作的输出提供给 map()
函数,该函数对集合的每个元素执行所提供的代码块,返回变换后的集合。最后,来自 map()
的输出集合流向 reduce()
函数,该函数基于代码块中提供的规则将每个元素结合起来。在本例中,我将每对元素结合起来,用插入的逗号连接它们。
我不必考虑三个函数调用中参数的名称是什么,所以我可以使用方便的 Scala 快捷方式,也就是说,使用 _
跳过名称。reduce()
函数从前两个元素入手,将它们结合成一个元素,成为下一个串接中的第一个元素。在 “浏览” 列表的同时,reduce()
构建了所需的逗号分隔的字符串。
我首先展示 Scala 实现是因为我对它的语法比较熟悉,而且 Scala 分别为筛选、变换和转化概念使用了行业通用的名称,即 filter、map 和 reduce。
Groovy 实现
Groovy 拥有相同的功能,但对它们进行命名的方式与脚本语言(比如 Ruby)更加一致。清单 1 中处理示例的 Groovy 版本如清单 3 所示。
清单 3. Groovy 处理
class TheCompanyProcess {
public static String cleanUpNames(List listOfNames) {
listOfNames
.findAll {it.length() > 1}
.collect {it.capitalize()}
.join(',')
}
}
尽管清单 3 在结构上类似于清单 2 中的 Scala 示例,但方法名称不同。Groovy 的 findAll
集合方法应用所提供的代码块,保留代码块为 true
的元素。如同 Scala,Groovy 包含一个隐式参数机制,为单参数代码块使用预定义的 it
隐式参数。collect
方法(Groovy 的 map
版本)对集合的每个元素执行所提供的代码块。Groovy 提供一个函数 (join()
),使用所提供的分隔符将字符串集合串联为单一字符串,这正是本示例中所需要的。
Clojure 实现
Clojure 是一个使用 reduce
、map
和 filter
函数名的函数式语言,如清单 4 所示。
清单 4. Clojure 处理示例
(defn process [list-of-emps]
(reduce str (interpose ","
(map clojure.string/capitalize
(filter #(< 1 (count %)) list-of-emps)))))
如果您不习惯查看 Clojure,可以使用清单 4 中的代码,其结构可能不够清晰。Clojure 这样的 Lisp 是 “由内而外” 进行工作的,所以必须从最后的参数值 list-of-emps
着手。Clojure 的 (filter )
函数接受两个参数:用于进行筛选的函数(本例中为匿名函数)和要筛选的集合。您可以为第一个参数编写一个正式函数定义,比如 (fn [x] (< 1 (count x)))
,但使用 Clojure 可以更简洁地编写匿名函数。与前面的示例一样,筛选操作的结果是一个较少的集合。(map )
函数将变换函数接受为第一个参数,将集合(本例中是 (filter )
操作的返回值)作为第二个参数。Clojure 的 (map )
函数的第一个参数通常是开发人员提供的函数,但接受单一参数的任何函数都有效;内置 capitalize
函数也符合要求。最后,(map )
操作的结果成为了 (reduce )
的集合参数。(reduce )
的第一个参数是组合函数(应用于 (interpose )
的返回的 (str )
)。(interpose )
在集合的每个元素之间(除了最后一个)插入其第一个参数。
当函数嵌套过多时,即使最有经验的开发人员也会倍感头疼,如 清单 4 中的 (process )
函数所示。所幸的是,Clojure 包含的宏支持您将结构 “调整” 为更可读的顺序。清单 5 中的功能与 清单 4 中的功能一样。
清单 5. 使用 Clojure 的 thread-last 宏
(defn process2 [list-of-emps]
(->> list-of-emps
(filter #(< 1 (count %)))
(map clojure.string/capitalize)
(interpose ",")
(reduce str)))
函数式编程的优势
在一篇标题为 “Beating the Averages” 的著名文章中,Paul Graham 定义了 Blub Paradox:他 “编造” 了一种名为 Blub 的虚假语言,并且考虑在其他语言与 Blub 之间进行功能比较:
只要我们假想的 Blub 程序员往下看一连串功能,他就知道自己是在往下看。不如 Blub 功能强大的语言显然不怎么强大,因为它们缺少程序员习惯使用的一些功能。但当我们假想的 Blub 程序员从另一个方向,也就是说,往上看一连串功能时,他并没有意识到自己在往上看。他看到的只不过是怪异的语言。他可能认为它们在功能上与 Blub 几近相同,只是多了其他难以理解的东西。Blub 对他而言已经足够好,因为他是在 Blub 环境中可以思考问题。
对于很多 Java 开发人员而言,清单 2 中的代码看起来陌生而又奇怪,因此难以将它看作是有优势的代码。但当您停止过于细化任务执行细节时,就释放了越来越智能的语言和运行时的潜能,从而做出了强大的改进。例如,JVM 的到来(解除了开发人员的内存管理困扰)为先进垃圾回收的创建开辟了全新的研发领域。使用命令式编码时,您深陷于迭代循环的细节,难以进行并行性等优化。从更高的层面思考操作(比如 filter、map 和 reduce)可将概念与实现分离开来,将并行性等修改从一项复杂、详细的任务转变为一个简单的 API 更改。
想一想如何将清单 1 中的代码变为多线程代码。由于您密切参与了 for
循环期间发生的细节,所以您还必须处理烦人的并发代码。然后思考一下清单 6 所示的 Scala 并行版本。
清单 6. 实现进程并行性
val parallelResult = employees
.par
.filter(f => f.length() > 1)
.map(f => f.capitalize)
.reduce(_ + "," + _)
清单 2 与 清单 6 之间惟一的差别在于,将 .par
方法添加到了命令流中。.par
方法返回后续操作依据的集合的并行版本。由于我将对集合的操作指定为高阶概念,所以底层运行时可以自由地完成更多的工作。
面向命令式对象的开发人员往往会考虑使用重用类,因为他们的语言鼓励将类作为构建块。函数编程语言倾向于重用函数。函数式语言构建复杂的通用功能(比如 filter()
、map()
和 reduce()
)并通过作为参数提供的函数来实现定制。在函数式语言中,将数据结构转换为列表和映射等标准集合是很寻常的事,因为它们接着就可以被强大的内置函数所操控。例如,在 Java 环境中存在许多 XML 处理框架,每个框架都封装自己的私有版本的 XML 结构,并通过自己的方法交付它。在 Clojure 这样的语言中,XML 被转换为基于映射的标准数据结构,该结构对已经存在于语言中的强大的变换、约简和筛选操作开放。
结束语
所有现代语言都包含或添加了函数式编程结构,使函数式编程成为未来开发中不可或缺的一部分。Java 下一代语言都实现了强大的函数式功能,有时使用不同的名称和行为。本文介绍了 Scala、Groovy 和 Clojure 中的一种新编码风格并展示了一些优势。
Java 下一代: 函数式编码风格——Groovy、Scala 和 Clojure 共享的函数结构及其优势的更多相关文章
- [中英对照]Linux kernel coding style | Linux内核编码风格
Linux kernel coding style | Linux内核编码风格 This is a short document describing the preferred coding sty ...
- paip.提升性能----java 无锁结构(CAS, Atomic, Threadlocal, volatile, 函数式编码, 不变对象)
paip.提升性能----java 无锁结构(CAS, Atomic, Threadlocal, volatile, 函数式编码, 不变对象) 1 锁的缺点 2 CAS(Compare ...
- [转载] 详述三种现代JVM语言--Groovy,Scala和Clojure
转载自http://www.tuicool.com/articles/jYzuAv和http://www.importnew.com/1537.html 在我与Martin Fowler曾经合作呈现的 ...
- Python 编码风格指南
原文:http://python.jobbole.com/84618/ 本文超出 PEP8 的范畴以涵盖我认为优秀的 Python 风格.本文虽然坚持己见,却不偏执.不仅仅涉及语法.模块布局等问题,同 ...
- JavaScript编码风格指南(中文版)
前言: 程序语言的编码风格对于一个长期维护的软件非常重要,特别是在团队协作中.如果一个团队使用统一规范的编码分风格,可以提高团队的协作水平和工作效率.编程风格指南的核心是基本的格式化规则,这些规则决定 ...
- paip. java的 函数式编程 大法
paip. java的 函数式编程 大法 Java 语言中常被忽视的一个方面是它被归类为一种命令式(imperative)编程语言.命令式编程虽然由于与 Java 语言的关联而相当普及,但是并不是惟一 ...
- Android编码风格
整理一下51CTO学院中张凌华老师讲的编码风格课程 一. 项目开发目录命名: Requirement - 需求相关文档 Design - 设计 Planning&Log - 计划,日志,会议 ...
- 辛星浅谈PHP的混乱的编码风格
我们都知道.各种编程语言都有自己的风格,即使是像C和C++那样一脉相承的语言(C++本意全然兼容C的语法).编程风格上还是有些区别.比方非常典型的就是C++风格的单行凝视和C风格的多行凝视. 而尽管J ...
- Objective-C 编码风格指南
本文转自:[Objective-C 编码风格指南 | www.samirchen.com][2] ## 背景 保证自己的代码遵循团队统一的编码规范是一个码农的基本节操,能够进入一个有统一编码规范的团队 ...
随机推荐
- 使用caffe模型测试图片(python接口)
1.加载相关模块 1.1 加载numpy import numpy as np 1.2 加载caffe 有两种方法. 方法一(静态导入): 找到当前环境使用的python的site-packages目 ...
- 2018-2019 2 20165203 《网络对抗技术》 Exp4 恶意代码分析
2018-2019 2 20165203 <网络对抗技术> Exp4 恶意代码分析 实验要求 监控你自己系统的运行状态,看有没有可疑的程序在运行. 分析一个恶意软件,就分析Exp2或Exp ...
- Visual Studio快速调出异常设置
使用快捷键:
- 线程池 多线程运行结束后 如何关闭? ExecutorService的正确关闭方法
前言 最近在使用ExecutorService的时候,对于与ExecutorService相关的概念有些迷糊, 加上本身ExecutorService内部的有些方法名在取名上也容易让使用者误解,导致 ...
- Redis数据结构之字符串
学习阶段分成两个部分,一个是redis客户端,一个是java客户端操作 一:在redis客户端操作 1.先删除里面的几个key 2.set与get与getset 3.数值的增减 值递增1,或者减一 如 ...
- Scrapy项目结构分析和工作流程
新建的空Scrapy项目: spiders目录: 负责存放继承自scrapy的爬虫类.里面主要是用于分析response并提取返回的item或者是下一个URL信息,每个Spider负责处理特定的网站或 ...
- 深入理解类成员函数的调用规则(理解成员函数的内存为什么不会反映在sizeof运算符上、类的静态绑定与动态绑定、虚函数表)
本文转载自:http://blog.51cto.com/9291927/2148695 总结: 一.成员函数的内存为什么不会反映在sizeof运算符上? 成员函数可以被看作是类 ...
- 条件随机场之CRF++源码详解-特征
我在学习条件随机场的时候经常有这样的疑问,crf预测当前节点label如何利用其他节点的信息.crf的训练样本与其他的分类器有什么不同.crf的公式中特征函数是什么以及这些特征函数是如何表示的.在这一 ...
- 如何开发一个npm包并发布
一.安装nodejs 不多说了,网上教程多得是 二.创建自己的npm包 目录结构 npm-test a.js b.js package.json 开发 为了简单便于理解,就开发一个简单地hello程序 ...
- SolidWorks知识积累系列-01
Solidworks学习 1. 基本知识点总结 基准视图 主视图:从前往后看,前视基准 俯视图:从上往下看,上视基准 侧视图:从右向左看,右视基准 草图要求 单封闭性,草图要依附于某个位置 绘制大概形 ...