前言:高阶函数是函数式编程中较为初级的一项技术。笔者首先用高阶函数实现了对象浅遍历,然后在其基础上轻松实现了对象的深度遍历。然后将代码稍加改造,实现了带路线追踪的深度遍历和部分遍历。最后,笔者通过之前的例子对函数式编程进行进一步讨论。笔者希望通过本文向大家展示函数式编程相对于传统的编程模式在思路上的不同。
高阶函数是函数式编程中较为初级的一项技术。笔者首先用高阶函数实现了对象浅遍历,然后在其基础上轻松实现了对象的深度遍历。然后将代码稍加改造,实现了带路线追踪的深度遍历和部分遍历。最后,笔者通过之前的例子对函数式编程进行进一步讨论。笔者希望通过本文向大家展示函数式编程相对于传统的编程模式在思路上的不同。
本小节主要面对不太了解函数式编程的读者,如果你也是Functional Lover,可以跳过这一节。
幺元的概念非常简单。不严谨地说,幺元就是某个类型的基本单元。在数学上,对于类型A的某个元素e
和任意元素a
,在运算符⊕
上,如果满足公式e ⊕ a = a ⊕ e = a
,那么e
就是类型A的幺元(双边)。
例如,相对于运算符+
而言,Integer
的幺元就是0
,String
的幺元就是""
;对于*
而言,Integer
的幺元就是1
;对于或
运算而言,Boolean
的幺元就是false
;其他的,列表的幺元就是空列表,对象的幺元就是空对象,每次当我们new
一个对象的时候,实际上就是创建了一个幺元。请注意,null
不是对象的幺元。
Java 8添加了如下函数类型:一元函数Function
、一元消费器Consumer
、一元谓词Predicate
、二元函数BiFunction
、二元消费器BiConsumer
、二元谓词BiPredicate
等等。事实上,这些类型都只是接口,没有具体的对象实现,她们代表的是不同入参和回参的方法。本质上,方法就是函数。例如:
// 方法写法
// 接受一个整型入参,返回值类型为整型
Integer increase (Integer x) { return x = 1; }
increase(1); // 2
// 函数写法
// 一元函数:一个参数,一个返回值,类似于 y = f(x)
Function<Integer, Integer> increase = x -> x + 1;
increase.apply(1); // 2
// 其他函数
// 一元消费器:只有入参,没有返回值
Consumer printOne = System.out::println; // 等价于 print = o -> System.out.println(o);
printOne.accept(1);
// 一元谓词:返回布尔型的函数
Predicate<Integer> equalOne = x -> x == 1;
equalOne.test(1);
// 二元函数:两个参数,一个返回值,类似于 y = f(x1, x2)
BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
// 等等......
简单的说,我们只是将方法声明成了变量。从此,方法也成为Java名词王国中的一等公民。
既然函数变成了变量,那么变量总是有类型的,在函数变量名的左边就是函数类型。其中<>
外的是自身的类型,<>
内最后一个是返回值类型。类型签名是一个函数的灵魂。我们以上面的add
为例,她的类型为BiFunction
。其中,最外面的BiFunction
表示这是一个二元函数,前两个Integer
表示两个参数必须是整型,最后一个Integer
表示返回值是整型。
既然函数有了类型,那就必然有幺元,那么函数的幺元是什么呢?其实很简单,函数的幺元就是:什么都不做。
Function identity = o -> o; // 一元函数幺元
Consumer pass = o -> {}; // 一元消费器幺元
函数有了类型,又可以接收参数,那么函数是否可以做为参数传入和返回呢?答案是肯定的。如果一个函数的返回类型还是函数,那么这个函数就是一个高阶函数。
我们写一个稍微复杂点的函数的类型签名,这个一元函数接收一个一元函数做为参数,并返回一个一元函数:
Function<Function<Integer, Integer>, Function<Integer,Integer>>
increaseBeforeApplyF = f -> x -> f.apply(x + 1);
// 调用方式
Integer y = increaseBeforeApplyF.apply(someFunction).apply(x);
// 部分调用
Function<Integer,Integer> somePartialFunction = increaseBeforeApplyF.apply(someFunction);
首先,你可能会狐疑为什么这么多的.apply
、.accept
、.test
,她们只是表示要调用一下。我们并不能像方法一样去调用函数,因为最初的Java认为什么都可以是对象,唯独方法不是对象。
另外,你可能想问,当发生部分调用的时候系统做了什么。事实上系统什么也没做,因为啥也做不了。只有完全调用后,系统才会开始计算。这种特性称为惰性求值。
高阶函数并不罕见,Java 8中的流式计算都是高阶函数,主要用来处理集合类型,深受广大程序员的喜爱。在各种编程语言中几乎都支持以下高阶函数:
map
映射函数:接收一个一元函数,将集合中所有元素都映射到另一个集合上reduce
归纳函数:接收一个幺元,和一个二元函数,将集合中所有元素归纳成一个元素filter
过滤函数:接收一个一元谓词,返回集合中谓词断言为真的元素集合注: 本文为了区分高阶函数和低阶函数,所有的高阶函数都使用
F
做为结尾。并且尽量不使用方法的写法,目的为了将函数的类型签名集中在一处,方便阅读。
我们不用思考究竟要举什么样的例子来说明我们实现了对象的遍历,这种传统的思路是非常有害的。我们只需要知道将来有个很普通的函数f
,她需要遍历处理对象中的所有元素。因此,我们写的遍历函数应当是一个修饰、或者是一个增强,她可以将一个普通的函数f
升级成为一个可以遍历对象元素的函数。
我们所要遍历的目标,可能是集合或者是一个非集合对象,他们可能有若干个元素或属性,每个元素或属性又有可能是一个集合或者是一个非集合对象。直接思考如何深度遍历是见很困难的事情,我们先思考如何实现单层的浅遍历。
我们可以从最简单的List列表写起:
traversalListF
- 接收一个普通的函数f
做为参数,返回一个可以消费List的消费器,最终会遍历List的索引,将每个元素都用f
处理一下
/** 列表单层遍历修饰符 */
private static final Function<Function, Consumer<List>>
traversalListF = f -> l -> IntStream.range(0, l.size()).forEach(i -> l.set(i, f.apply(l.get(i))));
我们可以用x -> x + 1
去测试一下List
,然后将list
打印出来,看看我们写的遍历器有没有将元素各个加1。事实上,根本没有这个必要,也非常不“函数范儿”。事实上,测试的时候,我们只需要知道我们的函数确实遍历到了每个元素就可以了,具体遍历做什么并不重要。
我们写一个辅助函数beforeF
:这个函数依次接收一个函数f
和一个消费器c
做为参数,并在f
处理入参之前,先用c
消费一下入参。
/** 前置调用修饰符 */
public static final Function<Function, Function<Consumer, Function>>
beforeF = f -> c -> o -> { c.accept(o); return f.apply(o); };
那么我们可以这样测试:遍历的对象原封不动,但是在此之前将他们打印出来。
/** 函数幺元 */
private static final Function identity = o -> o;
//测试
List<Integer> list = Arrays.asList(1, 2, 3);
traversalListF.apply(beforeF.apply(identity).apply(System.out::println)).accept(list);
// 输出
1
2
3
同样,我们可以写一个afterF
/** 后置调用修饰符 */
public static final Function<Function, Function<Consumer, Function>>
afterF = f -> c -> o -> { Object oo = f.apply(o); c.accept(oo); return oo; };
// 测试
traversalListF.apply(afterF.apply(beforeF.apply(x -> x + 1).apply(System.out::println)).apply(System.out::println)).accept(list);
// 输出
1
2
2
3
3
4
有些人可能会笑了:这样岂不是可以一行写完所有代码?其实beforeF
、afterF
表达的是两个函数之间的顺序关系。更深一步来讲,不仅顺序结构,选择结构、循环结构都只是一种关系或者亦可称为一种数据结构,本质上没有区别。在数学家眼里:这个世界没有什么东西是对象,有的只是关系。
对Map的遍历也很简单:
traversalMapF
- 接收一个函数f
,将Map里所有的值都处理一下
/** 映射单层遍历修饰符 */
private static final Function<Function, Consumer<Map>>
traversalMapF = f -> m -> m.forEach((k,v) -> m.put(k, f.apply(v)));
// 测试
Map<String, Integer> map = new HashMap();
map.put("first", 1);
map.put("second", 2);
map.put("third", 3);
traversalMapF.apply(beforeF.apply(identity).apply(System.out::println)).accept(map);
// 输出
1
2
3
对Set的遍历要特殊一些,因为处理完之后,元素的数量并不一定和原来相等。
traversalSetF
- 使用traversalListF
帮助Set遍历对象,将生成的元素重新加入到Set中
/** 集合单层遍历修饰符 */
private static final Function<Function, Consumer<Set>>
traversalSetF = f -> s -> {
List l = new ArrayList(s);
traversalListF.apply(f).accept(l);
s.clear();
s.addAll(l);
};
//测试
Set<Integer> set = new HashSet();
set.add(1);
set.add(2);
set.add(3);
traversalSetF.apply(x -> 1).accept(set);
set.forEach(System.out::println);
//输出
1
要想遍历一个未知对象的所有属性,只能使用反射。我们先写一个高阶函数modifyF
。
modifyF
- 她依次接收处理的函数f
、处理的对象o
和和她的属性名field
,并用f
去处理o
的field
/** 反射修改对象属性 */
private static final Function<Function, Function<Object, Consumer<Field>>>
modifyF = f -> o -> field -> {
try {
field.setAccessible(true);
field.set(o, f.apply(field.get(o)));
} catch (IllegalArgumentException | IllegalAccessException e){ /* pass.accept(o) */ }
};
然后我们去写遍历对象属性的函数traversalObjectF
,要注意对象的静态属性其实是全局变量,不能当作对象的成员去看待。
traversalObjectF
- 遍历对象o
的所有属性,如果是静态属性,则跳过;如果不是,就用f
去处理o
的属性
/** 谓词: 属性在对象中是否static修饰 */
private static final Predicate<Field> isStaticField = field -> Modifier.isStatic(field.getModifiers());
/** 对象单层遍历器 */
private static final Function<Function, Consumer>
traversalObjectF = f -> o -> Stream.of(o.getClass().getDeclaredFields()).forEach(
field -> {
if (isStaticField.test(field)) ; /* pass.accept(field) */
else modifyF.apply(f).apply(o).accept(field);
});
我们测试一下:
class Person {
private String name;
private String englishName;
private String age;
private String sex;
// ... oh, my god! a lot of get/set
}
//测试
Person myDaughter = new Person();
myDaughter.setName("Xinyu");
myDaughter.setEnglishName("Alice");
myDaughter.setAge("1");
myDaughter.setSex("Female");
traversalObjectF.apply(beforeF.apply(identity).apply(System.out::println)).accept(myDaughter);
//输出
Xinyu
Alice
1
Female
有人会较真,age
怎么可能是String
类型,分明应该是Integer
,sex
也有可能声明为Boolean
。对于任意一个对象,不可能所有的属性类型都是相同的,我们的函数应该只处理她能处理的类型,不要越俎代庖。
我们再写一个高阶matchingF
和typeF
来解决这个问题。
matchingF
- 依次接收谓词p
、函数f
、对象o
,如果p
断言为真,那么就使用f
去处理对象,否则什么都不做
typeF
- 接收一个类型,使用matchingF
去约束函数的作用范围(仅当对象类型符合预期)
/** 谓词匹配修饰符 */
public static final Function<Predicate, Function<Function, Function>>
matchingF = p -> f -> o -> p.test(o) ? f.apply(o) : o; /* identity.apply(o) */
/** 类型匹配修饰符 */
public static final Function<Class, Function<Function, Function>>
typeF = clazz -> matchingF.apply(o -> o.getClass().equals(clazz));
我们再测试一下:
class Person {
private String name;
private String englishName;
private Long age; // Hope to live long
private Boolean sex;
// ... oh, my god! a lot of get/set
}
//测试
Person myDaughter = new Person();
myDaughter.setName("Xinyu");
myDaughter.setEnglishName("Alice");
myDaughter.setAge(1L);
myDaughter.setSex(true);
traversalObjectF.apply(afterF.apply(typeF.apply(String.class).apply(x -> "Hello, " + x)).apply(System.out::println)).accept(myDaughter);
//输出
Hello, Xinyu
Hello, Alice
1
true
以后需要遍历对象时,只需要typeF
去约束一下函数就可以了,用matchingF
则可以实现更灵活的约束。下文我们不会再使用typeF
、matchingF
,因为我们只是想实现遍历,具体做什么是没有意义的。
到目前为止,我们都没有考虑对象为null
的情况。因为我们不会一开始就使用上述遍历器去遍历对象。其实不止是null
,如果不小心直接传入了一个Integer
或者String
,我们也会傻眼的。
我们首先写一个traversalF
,让普通的函数f
具有识别上述情况的能力。
traversalF
- 依次接收f
、o
:
1. 如果o
是null
,那什么也不做,也就是跳过;
2. 如果o
的类型(注意不是o
本身)是final
修饰的,那么我们认为已经遇到了f
可以处理的对象;
3. 如果都不是,那么同样需要跳过。
/** 谓词: 对象是否为null */
private static final Predicate isNull = Objects::isNull; /* o -> o == null */
/** 谓词: 反射判断对象的类型是否final修饰 */
private static final Predicate isFinalClass = o -> Modifier.isFinal(o.getClass().getModifiers());
/** 通用对象的遍历修饰符 */
private static final Function<Function, Function>
traversalF = f -> o ->
isNull.test(o) ? o : /* identity.apply(o) */
isFinalClass.test(o) ? f.apply(o) :
o; /* identity.apply(o) */
//注意:不可以写成 traversalF = f -> o -> isFinalClass.test(o) ? f.apply(o) : o;
这里使用了三元运算符的? :
的级联,可以清晰的表示向下依次进行匹配返回。因为在函数式编程中,只有表达式的概念,没有逻辑结构的概念。如果不是Java个别写法不支持,我们将干掉所有的if
、else
、break
、continue
、for
、while
、return
。一切,为了流畅地阅读代码。
然后,我们写一个函数getTraversalF
,让普通的函数f
可以自动选择适合的高阶遍历函数。
getTraversalF
- 她类似简单工厂模式,通过判断对象是否实现了集合的接口,来获取指定的遍历器。如果以后还有需要特殊处理的类型,那只要加入到遍历器字典就可以了。
/** 单层遍历器字典 */
private static final Map<Class, Function> traversalFMap = new HashMap<>();
static {
traversalFMap.put(List.class, traversalListF);
traversalFMap.put(Map.class, traversalMapF);
traversalFMap.put(Set.class, traversalSetF);
}
private static final Function defaultTraversalF = traversalObjectF;
/** 获取实现已知接口的遍历修饰符 */
private static final Function<Object, Function<Function, Consumer>>
getTraversalF = o -> {
Optional<Class> op = traversalFMap.keySet().stream().filter(clazz -> clazz.isAssignableFrom(o.getClass())).findAny();
return op.isPresent() ? traversalFMap.get(op.get()) : defaultTraversalF;
};
最后,我们写一个静态方法simpleTraversal
,包装最后的通用对象浅遍历方法。并且我们传入一个String
类型的对象,来看看simple
是否识别。
/** 浅遍历 */
public static final Consumer simpleTraversal (Function f) {
return o -> getTraversalF.apply(o).apply(traversalF.apply(f)).accept(o);
}
//测试
simpleTraversal(beforeF.apply(identity).apply(System.out::println)).accept("Hello, world!");
//输出
Hello, world!
终于开始深度遍历了。但是在深度遍历之前,我们需要先思考一下“深度遍历”究竟是什么。如果你的思维已经转变过来,那么你就会很自然地想到:既然各种类型的浅遍历是对函数的一种修饰和增强,那么深度遍历同样也是。
显然我们最终需要实现深度遍历fullTraversal
方法应该具有如下的特点:
fullTraversal
- 首先将f
增强为深度遍历的函数,然后用getTraversalF
增强获取指定遍历器,就可以深度遍历对象了
// depthTraversalF = ?
/** 全遍历 */
public static final Consumer fullTraversal (Function f) {
return o -> getTraversalF.apply(o).apply(depthTraversalF.apply(f)).accept(o);
}
怎么写这个depthTraversalF
呢?我们先简单点,使用写一个nextTraversal
让f
能够进行进行下一层遍历:
depthTraversalF
- 类似traversalF
,依次断言isNull
、isFinalClass
,如果都不是,则说明这个对象不能用f
处理,需要进行跳过,但是在跳过对象之前,需要对对象进行遍历
// nextTraversal = ?
private static final Function<Function, Function>
depthTraversalF = f -> o ->
isNull.test(o) ? o : /* identity.apply(o) */
isFinalClass.test(o) ? f.apply(o) :
beforeF.apply(identity).apply(nextTraversal.apply(f)).apply(o);
即使你忘记做“跳过”动作(beforeF.apply(identity)
)也没有关系,编译器会提醒你的。
事实上,nextTraversal
做的事情和fullTraversal
一样,就像接力似的,继续将深度遍历的能力增强给full
。最终的形式如下
/** 深度遍历修饰(双递归) */
private static final Function<Function, Function>
depthTraversalF = f -> o ->
isNull.test(o) ? o : /* identity.apply(o) */
isFinalClass.test(o) ? f.apply(o) :
beforeF.apply(identity).apply(fullTraversal(f)).apply(o);
/** 全遍历 */
public static final Consumer fullTraversal (Function f) {
return o -> getTraversalF.apply(o).apply(depthTraversalF.apply(f)).accept(o);
}
我们写个稍复杂的例子测试一下:
//测试:我的女儿家人有爸爸妈妈,和两个小狗朋友,爸爸的家人有爷爷和奶奶
class Person {
private String name;
private Long age; // I hope we all live long
private Map<String, Person> family;
private List<Person> friends;
// ... oh, my god! a lot of get/set
}
Person grandfather = new Person();
grandfather.setName("Grandpa");
Person grandmother = new Person();
grandmother.setName("Grandma");
Map<String, Person> myParents = new HashMap<>();
myParents.put("grandfather", grandfather); myParents.put("grandmother", grandmother);
Person me = new Person();
me.setName("Larry"); me.setAge(30L);
me.setFamily(myParents);
Person lover = new Person();
lover.setName("Lotus"); lover.setAge(30L);
Map<String, Person> family = new HashMap<>();
family.put("father", me); family.put("mother", lover);
Person doggy = new Person();
doggy.setName("Doggy"); doggy.setAge(2L);
Person puppy = new Person();
puppy.setName("Puppy"); puppy.setAge(3L);
Person myDaughter = new Person();
myDaughter.setName("Alice"); myDaughter.setAge(1L);
myDaughter.setFamily(family);
List<Person> friends = new ArrayList<>();
friends.add(doggy); friends.add(puppy);
myDaughter.setFriends(friends);
fullTraversal(beforeF.apply(identity).apply(System.out::println)).accept(myDaughter);
//输出
Alice
1
Lotus
30
Larry
30
Grandma
Grandpa
Doggy
2
Puppy
3
我们数一数,一共设置了12次Integer
和String
的属性,也一共输出了12个值。深对遍历对象的功能就这么轻松实现了!
到这里先总结一下,我们究竟做了什么。我们并没有具体实现深度遍历时要对对象做些什么,我们仅仅是描述了一种遍历的结构,这种结构描述了一个普通函数f
到一个普通对象o
的一种关系。
我们一共写了以下和深度遍历相关的函数identity
、beforeF
、isNull
、isFinalClass
、isStaticField
、traversalListF
、traversalMapF
、traversalSetF
、modifyF
、traversalObjectF
、getTraversalF
、depthTraversalF
、fullTraversal
共13个函数表达式,每个函数都非常的简洁明了,就像平时说话一样。同时,我们并没有约束遍历时具体做什么,任何一个普通的函数都可以升级成深度遍历的函数。或者换句话说:我们通过“说出”这13句话,就让计算机明白了什么是深度遍历。
常规的编程模式是通过向计算机下达指令,step by step,一步一步地完成某个功能。这种编程模式叫做命令式编程。但是事实上,“一步一步下达指令”是计算机擅长的事情,并不是人类擅长的。而我们今天采用的方式,是通过向计算机描述,来让计算机理解某项功能。这种编程模式叫做声明式编程。其实声明式编程也不是很罕见的东西,常用的SQL
、XML
/HTML
/CSS
都是声明式的(想想我们平时写的SQL
语句是不是也只有一行)。
函数式编程就是声明式编程的一种。简洁、不易出错、更加接近自然语言、高度抽象等等,这些都是函数式编程时的优势。在Java的泛型约束下,笔者书写深度遍历的代码时,几乎是“一遍灵”的,节约了大量的测试和debug时间。但同样值得注意的是,函数式编程提笔尤重,更多的时间是花费在思考和类型设计上。有人曾经开玩笑:命令式程序员把代码写完了,发现函数式程序员还在纸上写写画画;函数式程序员代码写完测试一下OK,发现命令式程序员还在debug。
上文的例子将深度遍历做成了黑盒,我们不能直观的看到函数f
的遍历过程。下面我们将之前的代码,改写成一个带有路线追踪能力的深度遍历。可以假设,我们将提供一个函数trace
专门负责追踪遍历的路线,而f
的执行则可能需要参考trace
追踪的结果。
也就说我们的路线追踪函数trace
应该是一个二元函数,将对象的属性不断地放入到上次追踪的队列中,返回最新的队列;同时普通的函数f
也应该是一个二元函数,她依靠已有的队列来决定如何处理传入的对象。同样的,f
和trace
究竟是什么并不重要。我们只需要知道f
要处理对象,trace
要处理队列就可以了。
//猜想
BiFunction trace = (property, lastQueue) -> ??; // push property into queue, and return queue
BiFunction f = (o, latestQueue) -> ??; // handle object according to queue, and return object
同时,我们还要明确什么样的属性才适合放入队列中,显然对象的成员名和Map的键适合做为属性,List和Set的索引则意义不大。
因此,对于List和Set,带追踪器的遍历函数只要改变入参数目和函数类型签名就可以了,队列不要变化;而Map和Object,我们则需要将键和属性放入到trace
中,获取最新的队列,然后传给f
。
/** 列表单层遍历器(带追踪器) */
private static final BiFunction<BiFunction, BiFunction, BiConsumer<List, Object>>
traversalListBF = (f, trace) -> (l, queue) -> IntStream.range(0, l.size()).forEach(i -> l.set(i, f.apply(l.get(i), queue)));
/** 集合单层遍历器(带追踪器) */
private static final BiFunction<BiFunction, BiFunction, BiConsumer<Set, Object>>
traversalSetBF = (f, trace) -> (s, queue) -> {
List l = new ArrayList(s);
traversalListBF.apply(f, trace).accept(l, queue);
s.clear();
s.addAll(l);
};
/** 映射单层遍历器(带追踪器) */
private static final BiFunction<BiFunction, BiFunction, BiConsumer<Map, Object>>
traversalMapBF = (f, trace) -> (m, queue) -> m.forEach((k,v) -> m.put(k, f.apply(v, trace.apply(k, queue))));
/** 反射修改对象属性(追踪属性) */
private static final BiFunction<BiFunction, BiFunction, BiFunction<Object, Object, Consumer<Field>>>
modifyBF = (f, trace) -> (o, queue) -> field -> {
try {
field.setAccessible(true);
field.set(o, f.apply(field.get(o), trace.apply(field, queue)));
} catch (IllegalArgumentException | IllegalAccessException e){ /* biPass.accept(o, queue) */ }
};
/** 对象单层遍历器(带追踪器) */
public static final BiFunction<BiFunction, BiFunction, BiConsumer>
traversalObjectBF = (f, trace) -> (o, queue) -> Stream.of(o.getClass().getDeclaredFields()).forEach(
field -> {
if (isStaticField.test(field)) ; /* pass.accept(field) */
else modifyBF.apply(f, trace).apply(o, queue).accept(field);
});
/** 单层带追踪器遍历器字典 */
private static final Map<Class, BiFunction> traversalBFMap = new HashMap<>();
private static final BiFunction defaultTraversalBF = traversalObjectBF;
static {
traversalBFMap.put(List.class, traversalListBF);
traversalBFMap.put(Map.class, traversalMapBF);
traversalBFMap.put(Set.class, traversalSetBF);
//traversalBFMap.put(Object.class, traversalObjectBF);
}
private static final Function<Object, BiFunction<BiFunction, BiFunction, BiConsumer>>
getTraversalBF = o -> {
Optional<Class> op = traversalBFMap.keySet().stream().filter(clazz -> clazz.isAssignableFrom(o.getClass())).findAny();
return op.isPresent() ? traversalBFMap.get(op.get()) : defaultTraversalBF;
};
private static final BiFunction biIdentity = (o1, o2) -> o1;
public static final Function<BiFunction, Function<BiConsumer, BiFunction>>
beforeBF = f -> c -> (o, queue) -> { c.accept(o, queue); return f.apply(o, queue); };
private static final BiFunction<BiFunction, BiFunction, BiFunction>
depthTraversalBF = (f, trace) -> (o, queue) ->
isNull.test(o) ? o : /* biIdentity.apply(o, queue) */
isFinalClass.test(o) ? f.apply(o, queue) :
beforeBF.apply(biIdentity).apply(traceTraversal(f, trace)).apply(o, queue);
/** 带追踪器的遍历 */
public static final BiConsumer traceTraversal (BiFunction f, BiFunction trace) {
return (o, queue) -> getTraversalBF.apply(o).apply(depthTraversalBF.apply(f, trace), trace).accept(o, queue);
}
现在可以测试了。事实上,在测试的时候,我们没有必要真的使用队列去完成路线追踪,只要具有和队列类似的追加功能就可以了。我们先简单点,就将对象的各个层级属性,设计成类似文件系统的/x/y/z
的形式。因此我们可以这样测试:
//测试
BiFunction pathTrace = (unknown, lastPath) -> unknown.getClass().equals(Field.class) ? lastPath + "/" + ((Field)unknown).getName() : lastPath + "/" + unknown.toString();
BiConsumer printPath = (o, path) -> System.out.println(path + " = " + o);
traceTraversal(beforeBF.apply(biIdentity).apply(printPath), pathTrace).accept(myDaughter, ""); // 传入String的幺元做为初始路径
//输出
/name = Alice
/age = 1
/family/mother/name = Lotus
/family/mother/age = 30
/family/father/name = Larry
/family/father/age = 30
/family/father/family/grandmother/name = Grandma
/family/father/family/grandfather/name = Grandpa
/friends/name = Doggy
/friends/age = 2
/friends/name = Puppy
/friends/age = 3
假设现在我们要写一个部分遍历的方法partial
。直接在f
里判断当前遍历的对象是否需要处理是不能实现部分遍历的,因为f
被增强后一定是个全遍历函数。即我们需要一个二元谓词p
,当p
去断言当前的o
和queue
为假时,就不再遍历。改动非常小,只要在深度遍历增强函数中添加一行! p.test(o, queue) ? o :
就可以了。但是现在的首要问题是,我们的函数接收参数的形式变成了(f, p, trace) -> (o, queue) -> ...
,这是三元高阶函数,但是Java并没有TriFunction
这样的类型。这个没关系,我们写个接口告诉Java就行了。
@FunctionalInterface
public interface TriFunction<U, T, S, R> {
R apply(U u, T t, S s);
}
@FunctionalInterface
会告诉Java,这是一个函数接口,请向方法那样去调用她。在编辑器提示下,改造起来是非常快的,这里不再详细解释,有兴趣的朋友可以测试一下。
private static final TriFunction<BiFunction, BiPredicate, BiFunction, BiFunction>
depthTraversalTF = (f, p, trace) -> (o, queue) ->
isNull.test(o) ? o : /* biIdentity.apply(o, queue) */
! p.test(o, queue) ? o : /* biIdentity.apply(o, queue) */
isFinalClass.test(o) ? f.apply(o, queue) :
beforeBF.apply(biIdentity).apply(partial(f, p, trace)).apply(o, queue);
public static final BiConsumer partial (BiFunction f, BiPredicate p, BiFunction trace) {
return (o, queue) -> getTraversalBF.apply(o).apply(depthTraversalTF.apply(f, p, trace), trace).accept(o, queue);
}
有些朋友乍一看会有很大的疑惑:为什么浅层遍历没有改造,只传入了f, trace
,没有p
,那么继续遍历时p
的断言还会有效吗?我们可以看一下partial
:我们可以看到,浅层遍历的高阶函数传入的f
其实是depthTraversalTF.apply(f, p, trace)
,里面已经包含了p
。表面上看是我们将函数做为参数传递给了高阶函数,但事实上,对计算机而言,这是做不到的事情,实际情况是高阶函数将最后接收到的对象(o, queue)
回传给了传入的(f, p, trace)
。也就是说,高阶函数仅仅是组织了参数之间的关系,她没有办法单独起作用。就像一开始说的那样,如果高阶函数没有接收到完整的参数,那么计算机什么也不会做,因为什么也做不了。
再写部分遍历的时候,笔者刻意地将trace
放在三个入参的最后,因为trace
并不是个普通的函数。
那么我们现在讨论一个遗留的问题:我们写的trace
究竟是什么?她该怎样去写?假设我们使用下面的代码去完成上述功能,能得到正确的结果吗?
//测试
BiFunction<Object, List<String>, List<String>> queueTrace = (unknown, lastQueue) -> {
if(unknown.getClass().equals(Field.class)) lastQueue.add(((Field)unknown).getName());
else lastQueue.add(unknown.toString());
return lastQueue;
};
BiConsumer<Object, List<String>> printQueue = (o, queue) -> System.out.println(queue.stream().reduce("", (x1, x2) -> x1 + "/" + x2) + " = " + o);
traceTraversal(beforeBF.apply(biIdentity).apply(printQueue), queueTrace).accept(myDaughter, new ArrayList<String>()); // 传入List的幺元做为初始队列
trace
怎么写执行上面的代码时,会发现我们打印的队列会越来越长,每次遍历都会将追踪到的路径加入到同一个队列中。这并不算太糟,至少我们的运行结果是确定的。但是如果traceTraversal
内部是多线程执行的,那么f
接收到的queue
将不会再有意义。
之所以会产生这样的问题,是因为传入的初始幺元new ArrayList
相对于traceTraversal
是一个全局对象,而在命令式编程语言中确实存在全局变量被污染的情况。因此,为了保证高并发,我们总是要将锁控制加入到我们的代码中。
事实上,在函数式编程中,并不需要这么做。我们之前描述过:”我们的路线追踪函数trace
应该是一个二元函数,将对象的属性不断地放入到上次追踪的队列中,返回最新的队列“。那我们每次都返回一个最新的队列就可以了。
// 正确的写法
BiFunction<Object, List<String>, List<String>> queueTrace = (unknown, lastQueue) -> {
List<String> lastQueue_ = new ArrayList<>();
lastQueue_.addAll(lastQueue);
if(unknown.getClass().equals(Field.class)) lastQueue_.add(((Field)unknown).getName());
else lastQueue_.add(unknown.toString());
return lastQueue_;
};
在函数式编程语言(例如Lisp、Haskell、Clojure、Scala等等)中,并不存在全局变量的概念,只有局部变量(自由变量和约束变量),并且一个函数总是会返回一个新的对象。因此,在传统编程语言去书写函数式代码时,我们需要额外注意这一点。
有人会质疑创建新对象带来的性能损耗。事实上,这种损耗是值得的(并且Java new的效率也是非常高的),因为这种方式消灭了依赖状态而运行的代码,让我们真正实现了无锁高并发/并行。加入锁控制只会让我们的代码更加复杂、更加不可控。
没错,函数式编程语言不仅没有那么多的关键字(if
、else
、for
…),没有全局变量,甚至连锁都可以没有。虽然这种说法过于绝对,但在函数式编程中确实可以让你体验”自由“的感觉。
trace
是什么如果抽象来看,trace
实际上是traceTraversal
运行时的状态迁移函数。trace
总是接收当前的状态和之前的状态,返回下一个新的状态。
事实上,我们所有代码都可以抽象成如下的形式:
M a c h i n e ( ∑ i = 1 n ( f i ) , s t a t e ( c s , l s ) ) . l o a d ( d a t a , i s ) , ( i s = 初 始 状 态 , c s = 当 前 状 态 , l s = 上 一 个 状 态 ) Machine(\sum_{i=1}^{n}(f_i), state(cs, ls)).load(data, is), (is = 初始状态, cs = 当前状态, ls = 上一个状态) Machine(i=1∑n(fi),state(cs,ls)).load(data,is),(is=初始状态,cs=当前状态,ls=上一个状态)
正是这个状态函数state
是一个无状态的函数,她只依赖传入参数,使得我们的程序是无状态的。
我们这里总结一下函数式编程的优势:
骚年,扔掉GoF,拿起眼前这把剑吧,让我们为了自由去战斗!
(什么什么?你问我什么是单子?好吧,单子不过是自函子范畴上的一个幺半群罢了…T_T)