Bruce Eckel - 详解函数式编程(卷三)
Bruce Eckel
读完需要
2分钟速读仅需 1 分钟
布鲁斯 • 埃克尔(Bruce Eckel),C++ 标准委员会的创始成员之一,知名技术顾问,专注于编程语言和软件系统设计方面的研究,常活跃于世界各大顶级技术研讨会。
他自 1986 年以来,累计出版 Thinking in C++、Thinking in Java、On Java 等十余部经典计算机著作,曾多次荣获 Jolt 最佳图书奖(被誉为“软件业界的奥斯卡”),其代表作 Thinking in Java 被译为中文、日文、俄文、意大利文、波兰文、韩文等十几种语言,在世界范围内产生了广泛影响。
7
函数组合
函数组合基本上就是说,将多个函数结合使用,以创建新的函数,这通常被认为是函数式编程的一部分。在 TransformFunction.java 中我们已经见过一个使用 andThen()的函数组合的示例。java.util.function 中的一些接口也包含了支持函数组合的方法(见表 13-2)。
以下示例使用了来自 Function 的 compose()和 andThen():
// functional/FunctionComposition.java
import java.util.function.*;
public class FunctionComposition {
static Function<String, String>
f1 = s -> {
System.out.println(s);
return s.replace('A', '_');
},
f2 = s -> s.substring(3),
f3 = s -> s.toLowerCase(),
f4 = f1.compose(f2).andThen(f3);
public static void main(String[] args) {
System.out.println(
f4.apply("GO AFTER ALL AMBULANCES"));
}
}
/* 输出:
AFTER ALL AMBULANCES
_fter _ll _mbul_nces
*/
这里的重点是,我们创建了一个新函数f4,它几乎可以像任何其他函数一样使用apply()来调用。[1]
当f1得到String时,字符串的前3个字符已经被f2剥离了。这是因为对compose(f2)的调用意味着f2会在f1之前被调用。
以下示例演示了Predicate(谓词)的逻辑运算:
[1] 有些语言支持组合而成的函数完全像其他函数一样调用,例如Python。但这是Java,我们就有什么用什么吧。
// functional/PredicateComposition.java
import java.util.function.*;
import java.util.stream.*;
public class PredicateComposition {
static Predicate<String>
p1 = s -> s.contains("bar"),
p2 = s -> s.length() < 5,
p3 = s -> s.contains("foo"),
p4 = p1.negate().and(p2).or(p3);
public static void main(String[] args) {
Stream.of("bar", "foobar", "foobaz", "fongopuckey")
.filter(p4)
.forEach(System.out::println);
}
}
/* 输出:
foobar
foobaz
*/
p4接受所有的谓词,并将其组合成一个更复杂的谓词。我们可以这样理解:“如果这个String不包含‘bar’并且长度小于5,或者其中包含‘foo’,则结果为true。”
在main()中我使了一点小手段,借用了下一章的内容,语法才会这样清晰。首先我创建了一个String对象的“流”(一个序列),然后将其中的每一个对象交给filter()操作。filter()使用我们的p4谓词来决定将流中的每一个对象保留还是放弃。最后,forEach()会在每个保留下来的对象上应用println方法引用。
可以从输出中看到p4是怎么工作的:任何带有“foo”的字符串都得以保留,即使其长度大于5。“fongopuckey”确实不包含“bar”,但是它太长了,所以不能保留下来。
8
柯里化和部分求值
柯里化(Currying)是以其发明者之一的 Haskell Curry 的姓氏命名的。而 Haskell Curry 也可能是唯一一位姓氏和名字都被用来命名重要事物的计算机科学家,Haskell 编程语言就是以他的名字命名的。柯里化的意思是,将一个接受多个参数的函数转变为一系列只接受一个参数的函数。
// functional/CurryingAndPartials.java
import java.util.function.*;
public class CurryingAndPartials {
// 未柯里化:
static String uncurried(String a, String b) {
return a + b;
}
public static void main(String[] args) {
// 柯里化函数:
Function<String, Function<String, String>> sum =
a -> b -> a + b; // [1]
System.out.println(uncurried("Hi ", "Ho"));
Function<String, String>
hi = sum.apply("Hi "); // [2]
System.out.println(hi.apply("Ho"));
// 部分应用:
Function<String, String> sumHi =
sum.apply("Hup ");
System.out.println(sumHi.apply("Ho"));
System.out.println(sumHi.apply("Hey"));
}
}
/* 输出:
Hi Ho
Hi Ho
Hup Ho
Hup Hey
*/
[1] 这一行不太好懂,有一连串箭头。而且请注意,在函数接口声明中,Function中有另一个Function作为其第二个参数。
[2] 柯里化的目的是能够通过提供一个参数来创建一个新函数,所以我们现在有了一个“参数化函数”和剩下的“自由参数”。实际上,我们从一个接受两个参数的函数开始,最后变成了一个单参数的函数。
还可以再添加一层,对接受三个参数的函数进行柯里化:
// functional/Curry3Args.java
import java.util.function.*;
public class Curry3Args {
public static void main(String[] args) {
Function<String,
Function<String,
Function<String, String>>> sum =
a -> b -> c -> a + b + c;
Function<String,
Function<String, String>> hi =
sum.apply("Hi ");
Function<String, String> ho =
hi.apply("Ho ");
System.out.println(ho.apply("Hup"));
}
}
/* 输出:
Hi Ho Hup
*/
对于每一层级联箭头,我们都要在其类型声明外再包裹另一个Function。
当处理基本类型和装箱时,可以使用适当的函数式接口:
// functional/CurriedIntAdd.java
import java.util.function.*;
public class CurriedIntAdd {
public static void main(String[] args) {
IntFunction<IntUnaryOperator>
curriedIntAdd = a -> b -> a + b;
IntUnaryOperator add4 = curriedIntAdd.apply(4);
System.out.println(add4.applyAsInt(5));
}
}
/* 输出:
9
*/
我们可以在网上找到更多的柯里化示例。它们通常是用其他语言编写的,而不是Java,但是如果你理解了基本概念,用Java改写应该也非常容易。
9
纯函数式编程
我们通过一些训练,在没有函数式支持的语言中(哪怕是像C这样原始的语言),也可以编写出纯函数式的程序。相较而言,Java要更容易些,但是我们必须仔细设计,让所有的内容都是final的,并确保所有的方法和函数都没有副作用。因为Java本质上并不是不可变语言,所以如果我们犯了错误,编译器是帮不上忙的。
有一些第三方工具可以帮助我们[1],但是使用像Scala或Kotlin这样的语言也许更容易,因为它们天生就是为支持函数式编程而设计的。借助这些语言,我们可以将项目的一部分用Java编写,而如果必须以纯函数式风格编写的话,其他部分则可以用Scala或Kotlin。尽管在进阶卷第5章中你会看到Java确实支持并行,但是如果并行是项目的核心部分,可以考虑至少在项目的一部分中使用Scala或Kotlin这样的语言。
[1] 例如Immutables和Mutability Detector。
10
总结
lambda表达式和方法引用不是将Java变成函数式语言,而是提供了对函数式编程风格的更多支持。这是对Java的巨大改进,因为这样可以支持编写更简洁、更干净、更易懂的代码。下一章将介绍它们是如何支持流的。如果你像我一样,你会喜欢流的。
大部分Java程序员对像Kotlin和Scala等新的、更为函数式的语言感到焦虑,同时对它们的特性又有点羡慕。lambda表达式和方法引用这些特性可能会满足他们的需求,同时也可能会阻止Java程序员转而使用那些语言(或者至少在他们仍然决定迁移时,让他们能有更好的准备)。
然而,lambda表达式和方法引用远非完美,我们要永远承受Java设计者在语言诞生初期的草率决定所导致的代价。最后,lambda在Java中并非一等公民。这并不意味着Java 8没有大的改进,但它确实意味着,像许多Java特性一样,最终会有一个让你感到不爽的临界点。
随着学习曲线的上升,记住:你可以从像NetBeans、IntelliJ IDEA和Eclipse这样的IDE中得到帮助。针对何时可以使用lambda表达式或方法引用,它们能提供建议(而且往往会为你重写代码)。(全文完)
本书特色
l 查漏宝典:涵盖Java关键特性的设计原理和应用方法
l 避坑指南:以产业实践的得失为鉴,指明Java开发者不可不知的设计陷阱
l 经典普适:值得不同层次的Java开发者反复研读
l 专家领读:4位一线业务专家、知名作译者帮你拆解书中难点,总结Java开发精要
值得一提的是,为了帮助新手加深理解,出版方邀请了4位从业10年以上知名作译者(DDD 专家张逸、服务端专家梁桂钊、软件系统架构专家王前明、译者陈德伟)为本书录制【精讲视频】和【导读指南】,该视频已在B站和图灵社区发布,感兴趣的朋友可以去看看。
读者福利 :
618特别活动,基础卷限时5折,800多页软精装到手价64.9!!
618限时5折专享
想购买全套装书的读者,1400多页软精装到手价160
限量套装,售完为止
往期推荐
Bruce Eckel - 详解函数式编程(卷二)
聊聊技术人员如何做好团队管理
Google工程师是怎么写设计文档的?
如葑:阿里云原生网关Envoy Gateway实践
如何用研发效能搞垮一个团队
他教全世界程序员怎么写好代码,答案写在这里!
研发效能提升的实践框架、模式与反模式
聊聊大中型公司都热衷于造轮子的故事
被滥用的“架构师”!