这是Java函数式编程的自学笔记。
虽然昨晚使劲想明白了,不过思维还是没有完全转变过来,估计要自如运用,还得有段时间。
这篇笔记里充满着反复的思考,希望过段时间自己回来还能够看明白。
函数柯里化的一个关键的思想就是,如果是按顺序传递参数的话,每次返回的函数对象的类型应该是下一个参数的类型。这个到了后来才搞懂。如果是反向运用,一定要根据返回类型的指引才能做出来结果。
函数的概念
函数的定义域叫做domain,就是source set。结果集叫做 codomain,二者并不一定不相同。
深入理解函数的本质在于函数可以被结果直接替换,没有不可控的副作用。
但是所有的domain中的元素,必须在codomain中存在唯一对应的元素。也就是意味着:
- domain中的元素,在codomain中不能没有对应的元素
- domain中的一个元素,codomain中不能存在两个或更多对应的元素
- codomain中的元素不能在domain中没有对应的元素
- 可以有domain中的多个元素对应一个codomain中的元素
codomain中所有与domain中有对应关系的元素组成的集合叫做元素的image(像)。
看了英文版原书,发现其实domain和codomain是人为规定的定义域和结果集,比如如果规定f(x) = 2 * x
,规定x为整数集和codomain也是整数集,那么只有偶数是函数的像。
一个函数未必会有逆函数,即反向计算的关系不一定符合正向的结果。比如一个值可能反过来对应多个值。还是前边的f(x) = 2 * x
,如果domain和codomain都是整数集,则不存在逆函数。
复合函数为 f◦g,表示f(g(x))。
多参函数实际上不存在,需要将所有的参数看成是一个特殊的组合,比如元组。所以可以理解为,给定一个元组,返回一个特定的结果。
函数柯里化
这是理解函数式编程的一个关键。假如有一个函数f(x,y) = n
,如果可以将其改写成f(x)(y) = n
,后者就是前者的柯里化函数。
柯里化的关键是,f(x)返回一个函数,这个函数直接作用于第二个参数y,最终得到结果。
f(x)的codomain此时不是一个具体的结果,而是一个函数集,其中附带着已经确定的x。
Java中的函数
在Java中,没有独立存在的函数,只有方法。并不是所有的方法都是函数式的,满足纯函数的要求,可以说是函数式的:
- 不能修改函数之外的东西
- 不能修改参数
- 不能抛出异常和错误
- 必须返回一个值
- 调用参数相同,结果也必须相同
将实例方法改写成静态方法
有些对象的方法,在其中直接调用了实例变量,而没有通过参数传入,这种函数不是纯函数,因为实例变量可能发生变化。
如果把实例变量传入函数,由于依然与实例绑定,因此也不是纯函数,最好将其改造为传入的是一个对象,这样这个方法就会实例无关,可以改造成一个静态方法。
将所有的参数都显式写在参数上,对实例的引用改成一个对象变量,就可以彻底改造成静态方法。
简单的链式调用的改写,则需要实例方法返回一个带有当前计算过的所有参数的新对象即可。
Java 的函数式接口与匿名类
package fpinjava.chapter2;
public interface Function<T,R> {
R apply(T t);
static Function<Integer, Integer> compose(Function<Integer, Integer> f1, Function<Integer, Integer> f2) {
return new Function<Integer, Integer>() {
@Override
public Integer apply(Integer integer) {
return f2.apply(f1.apply(integer));
}
};
}
}
在使用的时候,可以编写这个Function类的多个实例:
public static void main(String[] args) {
Function<Integer,Integer> triple =new Function<Integer, Integer>() {
@Override
public Integer apply(Integer integer) {
return integer * 3;
}
};
Function<Integer,Integer> square = new Function<Integer, Integer>() {
@Override
public Integer apply(Integer integer) {
return integer * integer;
}
};
//静态方法Compose用于组合两个Integer泛型的函数
Function<Integer, Integer> tripleThenSquare = Function.compose(triple, square);
System.out.println(tripleThenSquare.apply(4));
Function<Integer, Integer> squareThenTriple = Function.compose(square, triple);
System.out.println(squareThenTriple.apply(4));
}
由于Java的函数一定要有个对象罩着,实际上将squareThenTriple.apply(4)理解成 squareThenTriple(4)更加易于学习函数式编程
同样,可以将triple.apply(4)理解为triple(4) square.apply(4)理解成square(4)
那么compose的结果就是 square(triple(4))或者 triple(square(4)) 取决于参数顺序和compose方法的具体编写
可以看到,就像是先把参数进行了第一个参数里的函数的运算,结果再交给第二个参数的函数运算一样
但是可以发现,自己编写新的函数对象的时候,实际上还是new 了一个对象出来,然后重写方法。
可以用lambda表达式,让Java程序看上去像是一个函数而不是一个对象+方法。
lambda表达式
在之前总以为lambda表达式主要是简化函数。其实最重要的是,lambda表达式为代码编译提供了类型推断。
看一下原来的代码:
Function<Integer,Integer> triple = new Function<Integer, Integer>() {
@Override
public Integer apply(Integer integer) {
return integer * 3;
}
};
triple变量的类型已经由前边的Function<Integer,Integer>
声明了,所以我们知道,后边去new一个新对象的时候,一定也是Function<Integer,Integer>
对象。
原来的接口只有一个方法,要让对象成为接口的实现类,一定会重写唯一的方法。这个方法的返回值和名称都写在了接口里。
也就是说,这段代码里标为红色的部分,即使不写出来,我们也能推断出来:
Function<Integer,Integer> triple = new Function<Integer, Integer>() {
@Override
public Integer apply(Integer integer) {
return integer * 3;
}
};
当然参数的类型也是可以知道的,因为被泛型确定了。但是由于函数体的计算过程需要形参的名称,所以不能省略。
既然如此,我们就传入一个lambda表达式来代替这个匿名的接口实现类对象,只保留需要的内容:
Function<Integer, Integer> triple = arg -> arg * 3;
后边就是Java 8 中的lambda表达式的标准形式,相比原来的匿名类,这个表达式看起来就像是一个函数对象而不是一个类对象,当然,实际上Java 8是没有函数对象的。
所以凡是匿名内部类的地方,都可以考虑换成lambda表达式,只要通过观察看是否可以推断出类型即可:
修改后的compose静态方法如下:
package fpinjava.chapter2;
public interface Function<T,R> {
R apply(T t);
static Function<Integer, Integer> compose(Function<Integer, Integer> f1, Function<Integer, Integer> f2) {
return arg -> f2.apply(f1.apply(arg));
}
}
如果思维绕不过来,就记住,lambda表达式是一个匿名对象,并不是一个函数,也不是函数的值,就可以了。
高阶函数特性-函数返回函数or函数的参数是函数
把多参函数转换为柯里化函数
前边已经说过,不存在多个参数的函数,形式上的多参函数,其参数其实是一个元组。但是,参数可以一个一个的应用。
理论上来说,一个函数的参数是元组,则也可以找到一个函数,对元组里的各个元素一个个的应用函数即可。
这就要求在过程中,每一次应用一个函数,会生成一个新的函数,直到最后一个函数来求值。
前边我们写了:
public interface Function<T,R> {
R apply(T t);
}
表示这个函数对象通过传入T返回R类型,现在新建一个声明:(注意,不要将其想成匿名对象,想成其中的方法就是对象,就是一个函数对象)
Function<Integer, Function<Integer,Integer>> myFunction
这个声明是什么意思呢,从泛型可以看出,这个函数接受一个Integer,返回一个Function<Integer,Integer>。即我们的函数对象接受一个整数,返回一个接受整数返回整数的函数。
也就是说,假如此时我们执行:
myFunction.apply(5)
返回的是一个Function<Integer,Integer>
对象。
刚才我们已经知道,对于Function<Integer, Integer>
可以写成lambda表达式,那么这个myFunction可以写成伪代码:
Function<Integer, Function<Integer,Integer>> myFunction = Integer - >(Function<Integer,Integer>);
而Function<Integer, Integer>
依然可以写成lambda表达式,最终就是:
Integer -> (Integer -> Integer)
更刺激的是还可以去掉括号,因为函数是从右向左逐层求值然后递归的,变成Integer -> Integer -> Integer
。
这里我们没有编写具体的计算结果的函数体,就以返回值作为伪代码来示例,但是这样也已经足够刺激了,至少我看到的时候内心“卧槽”了一声,Java代码还能这么写。
如果用这种方式编写两个数相加的函数,如下:
Function<Integer, Function<Integer, Integer>> add = x -> y -> x + y;
注意这里的x和y其实是两个不同的参数,分别为先接受的第一个参数,和接受了第一个参数之后返回的函数需要再接受的参数。由于我们的Function函数(实际上是类)每次只接受一个函数,因此这里编写的两个数相加的函数,其实就是add(x,y) = x + y
的柯里化函数:add(x)(y) = x + y
。
想一下,如果这个函数用没有lambda表达式的Java语言写出来会是什么样子:
Function<Integer, Function<Integer, Integer>> curry = new Function<Integer, Function<Integer, Integer>>() {
@Override
public Function<Integer, Integer> apply(Integer x) {
return new Function<Integer, Integer>() {
@Override
public Integer apply(Integer y) {
return x + y;
}
};
}
};
相比lambda表达式,看明白上边这串玩意要花点功夫,不要说更复杂的逻辑了。
有了这个函数之后,其实相当于我们有了一个新的接口,原来的接口标示了 f(T) -> R,现在可以创建一个新接口,用于标识 f(T)(T) -> R:
public interface BinaryOperator extends Function<Integer, Function<Integer, Integer>> {
}
这个接口继承了原来的泛型接口的一个具体类型的接口,其中的apply方法也继承了过来,返回一个Function<Integer, Integer>
对象。
我们的这个BinaryOperator
接口,实际上就是一个先后接收两个参数,返回一个结果的函数对象。
有了这个,定义两个参数的函数就方便多了:
BinaryOperator add = x -> y -> x + y;
BinaryOperator multi = x -> y -> x * y;
BinaryOperator pow = x -> y-> {
int temp = 1;
for (int i = 0; i < y; i++) {
temp *= x;
}
return temp;
};
最后一个是取x的y次幂,lambda表达式还可以具体使用参数来计算结果,可以写任意长度的逻辑,保证返回值与需要的一致即可。
这样就完成了函数的柯里化。遗憾的是,在应用具体操作的时候,只能写成:
System.out.println(add.apply(3).apply(4));
System.out.println(multi.apply(3).apply(4));
System.out.println(pow.apply(5).apply(5));
像python可以写成add(3)(4)
,但在Java里不能这么写。
高阶函数
之前的compose方法,接受两个函数对象,返回一个函数对象。但是apply方法中是直接调用了函数的结果。
现在我们要函数套函数,采用柯里化的方式来改造compose方法,也就是接受第一个函数参数,应用过之后,再应用到第二个函数参数上,最后还得到一个函数对象。
仔细想想原来的例子:
Function<Integer, Function<Integer, Integer>>
Function<Integer, Integer>
表示一个从Integer到Integer的关系,即函数,如果接收一个函数返回一个函数,则应该是Function<Function<Integer, Integer>, Function<Integer, Integer>>
那么第一个参数显然应该第二个函数对象的参数,也就是Function<Integer, Integer>
最终的函数应该是:Function<Function<Integer, Integer>, Function<Function<Integer, Integer>, Function<Integer,
Integer>>>
如果不理解的话,只要想想Function<Integer, Function<Integer, Integer>>
,把里边的Integer都换成Function<Integer, Integer>
即可。
始终要注意映射关系,接受一个函数,返回一个函数,然后柯里化,实际上就是Function<T, Function<T, T>>
,这里的T换成其他类型也一样。
最后写出来的东西是这样:
public interface Compose extends Function<Function<Integer, Integer>, Function<Function<Integer, Integer>, Function<Integer, Integer>>> {
}
传参数的话,记得其实是三个参数,最后一个参数是一个Integer,如果只传两个函数作为参数,得到的其实就是一个从参数类型获取返回类型的函数:
Compose compose = x -> y -> z -> x.apply(y.apply(z));
一次性看到这里不晕的话感觉很厉害了。其实就是从最简单的f(x,y) = n 转换到f(x)(y) = n的过程来推算。
f(x)的结果必然是一个g(y) = n的函数,也就是Function<Integer,Integer>
,那f(x)传入x返回g(y),必然就是Function<Integer,Function<Integer,Integer>
>了。一次柯里化暗含着2个参数。
compose是什么呢,接受两个函数作为参数,返回一个函数对象,也就是即f(x)(y)(z) = n,也就是f(x)(y) = g(z)。
注意观察,其实f(x)(y)和之前的一样,可以写成Function<T,Function<T,R>>,关键就是这个T和R到底是什么。
从之前的分析可以发现,x的类型一定是T,最后的返回类型是R,这里的x,y和返回值都是一个函数对象,因此把T和R都替换成Function<Integer, Integer>
,就得到了答案:
Function<Function<Integer, Integer>,Function<Function<Integer, Integer>,Function<Integer, Integer>>>
要调用这个方法,就要按照次序传入:
System.out.print(compose.apply(triple).apply(square).apply(4));
这下就很清楚了,传入两个函数得到一个函数,再给这个函数传个参数得到值。
写到这里,突然想写一个普通的三个值的柯里化函数,就是:
public interface TripleOperator extends Function<Integer,Function<Integer, Function<Integer, Integer>>> {
}
调用这个就是三个参数了:
TripleOperator threeCurry = x -> y -> z -> x + y + z;
System.out.println(threeCurry.apply(2).apply(3).apply(4));
结果是7,有了BinaryOperator之后,就可以把一个参数是一个元组的函数,变成柯里化函数了。
泛型高阶函数
如果把上边的改造过的compose继续改造成泛型。就得好好考虑一下类型了。
看这条:
假如我们有两个lambda了:
Function<Double, Integer> f = a -> (int) (a * 3);
Function<Long, Double> g = x -> x + 2.0;
很显然我们要组合成f.apply(g.apply(x))
那么很显然,在链式调用的时候,先传入一个f当参数,然后是g当参数,最后是x。
那么我们的泛型接口就要有三个具体类型,于是第一个肯定是最外层,也就是Function<Double,Integer>
,第二个是Function<Long,double>
。
关键是最后返回的Function对象是什么,由于对外来看,最后返回的就是整个函数。而整个函数实际完成的工作是Long->Integer,就是Function<Long,Integer>
了。
先把泛型定死,写下来看看:
interface ComposeR extends Function<Function<Double, Integer>, Function<Function<Long, Double>, Function<Long, Integer>>> {
}
然后再把泛型抽象出来:
interface ComposeR<T,U,V> extends Function<Function<U, V>, Function<Function<T, U>, Function<T, V>>> {
}
可见多次柯里化,不管传实际值还是传函数对象,泛型从最开始的类型到最终返回类型按顺序排好,而其中的函数则从最外层排到最内层,最后返回的对象是整个函数的映射,所以泛型是从最初类型到最终返回类型。这就是高阶函数泛型的关键。
书里67页的题目,通过higherCompose编写higherAndThen,也就是先应用g,再应用f。
要写这个题目,其实只要想到每一步返回的到底是什么类型的函数,先应用g,也就是第一个参数是Function<Long, Double>
,返回的函数肯定参数是Function<Double, Integer>
,关键还是最后一个函数,是什么呢,其实还是从最终参数返回最终结果,所以依然是Function<Long, Integer>
。
然后串起来,一层套一层:
Function<Function<Long, Double>, Function<Function<Double, Integer>, Function<Long, Integer>>>
再把泛型抽取出来:
interface ComposeThen<T,U,V> extends Function<Function<T, U>, Function<Function<U, V>, Function<T, V>>> {
}
这里如果往细了想,容易绕进去,不要太多关注泛型,就想三个参数是什么,参数类型只要匹配,就一层一层套壳柯里化即可。
方法引用
如果lambda的实现是一个单参数的方法调用,可以不用写表达式,直接以类名::方法名来写即可。
闭包
在方法内部使用匿名类或者lambda表达式的时候,方法的局部变量只要被引用,就自动会变成实际上的final,这就是Java的闭包。
这是因为在之前介绍纯函数的时候说过,函数的返回值如果直接引用了局部变量,就变成了隐式参数,如果变化,就会影响返回值。
如果之后再给被引用的变量赋值,就无法通过编译。
最好尽量少引用局部变量,将其作为一个显式的参数可能更加,虽然多参数可能需要进一步柯里化,但使用元组作为一个参数也是可以的,从元组中取出参数就行了。
柯里化的应用
如果一个函数多参(实际上参数是一个元组),所有的参数都要被计算出来才能够传入函数,但是将函数柯里化之后,可以发现可以按顺序传入参数。
比如在计算税的时候,如果一次性传入税率和税基,那么只能针对这一种组合计算一次。
但是一般税率比较固定,只想更改税基来看对应的结果,这个时候可以将函数柯里化,先传入一个税率生成一个函数对象。之后只要复用这个函数对象,作用于不同的税基就可以了。
柯里化如果反序,其实就是在类型上进行游戏,记住或者将这段代码放到自己的库里即可:
public static <T, U, V> Function<U, Function<T, V>> reverseArgs(Function<T, Function<U, V>> f) {
return u -> t -> f.apply(t).apply(u);
}
递归
匿名的递归无法编写,因为无法调用到自己。所以不能直接用lambda表达式。但是用this就可以调用当前对象,进而可以调用自己。
比较直观的是采用静态方法,加上类名来反复调用自己,比如阶乘方法:
static Function<Integer, Integer> factorial = n -> n <= 1 ? n : n * Function.factorial.apply(n - 1);
恒等函数
对于函数式操作来说,函数也被当成值一样传来传去,也会用于操作函数,所以需要一个中性元素或者恒等元素。
最后实际上是编写了基于Function<T,R>的一个完整的接口,这个接口如下:
public interface Function<T, U> {
//作为抽象函数接口的基础方法,接受T类型返回U类型
U apply(T arg);
default <V> Function<V, U> compose(Function<V, T> f) {
return x -> apply(f.apply(x));
}
default <V> Function<T, V> andThen(Function<U, V> f) {
return x -> f.apply(apply(x));
}
static <T> Function<T, T> identity() {
return t -> t;
}
static <T, U, V> Function<V, U> compose(Function<T, U> f, Function<V, T> g) {
return x -> f.apply(g.apply(x));
}
static <T, U, V> Function<T, V> andThen(Function<T, U> f, Function<U, V> g) {
return x -> g.apply(f.apply(x));
}
static <T, U, V> Function<Function<T, U>, Function<Function<U, V>, Function<T, V>>> compose() {
return x -> y -> y.compose(x);
}
static <T, U, V> Function<Function<T, U>, Function<Function<V, T>, Function<V, U>>> andThen() {
return x -> y -> y.andThen(x);
}
static <T, U, V> Function<Function<U, V>, Function<Function<T, U>, Function<T, V>>> higherCompose() {
return f -> g -> x -> f.apply(g.apply(x));
}
static <T, U, V> Function<Function<T, U>, Function<Function<U, V>, Function<T, V>>> higherAndThen() {
return f -> g -> z -> g.apply(f.apply(z));
}
}