函数Java 8 中只提供了接受一个参数的 Function 和接受 2 个参数的 BiFunction。Vavr 提供了函数式接口 Function0、Function1 到 Function8,可以描述最多接受 8 个参数的函数。这些接口的方法 apply 不能抛出异常。如果需要抛出异常,可以使用对应的接口 CheckedFunction0、CheckedFunction1 到 CheckedFunction8。
Vavr 的函数支持一些常见特征。
组合函数的组合指的是用一个函数的执行结果作为参数,来调用另外一个函数所得到的新函数。比如 f 是从 x 到 y 的函数,g 是从 y 到 z 的函数,那么 g(f(x))是从 x 到 z 的函数。Vavr 的函数式接口提供了默认方法 andThen 把当前函数与另外一个 Function 表示的函数进行组合。Vavr 的 Function1 还提供了一个默认方法 compose 来在当前函数执行之前执行另外一个 Function 表示的函数。
在清单 2 中,第一个 function3 进行简单的数学计算,并使用 andThen 把 function3 的结果乘以 100。第二个 function1 从 String 的 toUpperCase 方法创建而来,并使用 compose 方法与 Object 的 toString 方法先进行组合。得到的方法对任何 Object 先调用 toString,再调用 toUpperCase。
清单 2. 函数的组合1
2
3
4
5
6
7
8
9
10
11
12
13
| Function3< Integer, Integer, Integer, Integer> function3 = (v1, v2, v3)
-> (v1 + v2) * v3;
Function3< Integer, Integer, Integer, Integer> composed =
function3.andThen(v -> v * 100);
int result = composed.apply(1, 2, 3);
System.out.println(result);
// 输出结果 900
Function1< String, String> function1 = String::toUpperCase;
Function1< Object, String> toUpperCase = function1.compose(Object::toString);
String str = toUpperCase.apply(List.of("a", "b"));
System.out.println(str);
// 输出结果[A, B]
|
部分应用在 Vavr 中,函数的 apply 方法可以应用不同数量的参数。如果提供的参数数量小于函数所声明的参数数量(通过 arity() 方法获取),那么所得到的结果是另外一个函数,其所需的参数数量是剩余未指定值的参数的数量。在清单 3 中,Function4 接受 4 个参数,在 apply 调用时只提供了 2 个参数,得到的结果是一个 Function2 对象。
清单 3. 函数的部分应用1
2
3
4
5
6
| Function4< Integer, Integer, Integer, Integer, Integer> function4 =
(v1, v2, v3, v4) -> (v1 + v2) * (v3 + v4);
Function2< Integer, Integer, Integer> function2 = function4.apply(1, 2);
int result = function2.apply(4, 5);
System.out.println(result);
// 输出 27
|
柯里化方法使用 curried 方法可以得到当前函数的柯里化版本。由于柯里化之后的函数只有一个参数,curried 的返回值都是 Function1 对象。在清单 4 中,对于 function3,在第一次的 curried 方法调用得到 Function1 之后,通过 apply 来为第一个参数应用值。以此类推,通过 3 次的 curried 和 apply 调用,把全部 3 个参数都应用值。
清单 4. 函数的柯里化1
2
3
4
5
| Function3<Integer, Integer, Integer, Integer> function3 = (v1, v2, v3)
-> (v1 + v2) * v3;
int result =
function3.curried().apply(1).curried().apply(2).curried().apply(3);
System.out.println(result);
|
记忆化方法使用记忆化的函数会根据参数值来缓存之前计算的结果。对于同样的参数值,再次的调用会返回缓存的值,而不需要再次计算。这是一种典型的以空间换时间的策略。可以使用记忆化的前提是函数有引用透明性。
在清单 5 中,原始的函数实现中使用 BigInteger 的 pow 方法来计算乘方。使用 memoized 方法可以得到该函数的记忆化版本。接着使用同样的参数调用两次并记录下时间。从结果可以看出来,第二次的函数调用的时间非常短,因为直接从缓存中获取结果。
清单 5. 函数的记忆化1
2
3
4
5
6
7
8
| Function2<BigInteger, Integer, BigInteger> pow = BigInteger::pow;
Function2<BigInteger, Integer, BigInteger> memoized = pow.memoized();
long start = System.currentTimeMillis();
memoized.apply(BigInteger.valueOf(1024), 1024);
long end1 = System.currentTimeMillis();
memoized.apply(BigInteger.valueOf(1024), 1024);
long end2 = System.currentTimeMillis();
System.out.printf("%d ms -> %d ms", end1 - start, end2 - end1);
|
注意,memoized 方法只是把原始的函数当成一个黑盒子,并不会修改函数的内部实现。因此,memoized 并不适用于直接封装本系列 文章中用递归方式计算斐波那契数列的函数。这是因为在函数的内部实现中,调用的仍然是没有记忆化的函数。 |