这是Java函数式编程的自学笔记。

虽然昨晚使劲想明白了,不过思维还是没有完全转变过来,估计要自如运用,还得有段时间。
这篇笔记里充满着反复的思考,希望过段时间自己回来还能够看明白。

函数柯里化的一个关键的思想就是,如果是按顺序传递参数的话,每次返回的函数对象的类型应该是下一个参数的类型。这个到了后来才搞懂。如果是反向运用,一定要根据返回类型的指引才能做出来结果。

函数的概念

函数的定义域叫做domain,就是source set。结果集叫做 codomain,二者并不一定不相同。

深入理解函数的本质在于函数可以被结果直接替换,没有不可控的副作用。

但是所有的domain中的元素,必须在codomain中存在唯一对应的元素。也就是意味着:

  1. domain中的元素,在codomain中不能没有对应的元素
  2. domain中的一个元素,codomain中不能存在两个或更多对应的元素
  3. codomain中的元素不能在domain中没有对应的元素
  4. 可以有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中,没有独立存在的函数,只有方法。并不是所有的方法都是函数式的,满足纯函数的要求,可以说是函数式的:

  1. 不能修改函数之外的东西
  2. 不能修改参数
  3. 不能抛出异常和错误
  4. 必须返回一个值
  5. 调用参数相同,结果也必须相同

将实例方法改写成静态方法

有些对象的方法,在其中直接调用了实例变量,而没有通过参数传入,这种函数不是纯函数,因为实例变量可能发生变化。

如果把实例变量传入函数,由于依然与实例绑定,因此也不是纯函数,最好将其改造为传入的是一个对象,这样这个方法就会实例无关,可以改造成一个静态方法。

将所有的参数都显式写在参数上,对实例的引用改成一个对象变量,就可以彻底改造成静态方法。

简单的链式调用的改写,则需要实例方法返回一个带有当前计算过的所有参数的新对象即可。

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));
    }
}