Java的函数式编程(二)

4 阅读5分钟

compose和andThen

在Java中,Function有这么两个操作compose和andThen。它们用来实现函数式编程中一个非常强大的功能“组合”。这个功能被大部分人忽略,然而它的存在让开闭原则在FP编程中有着很好的体现。刚开始接触这两个方法的时候,我有着很深的困惑。

Function<Integer,Integer> a = (x)->x+1;
Function<Integer,Integer> b = (x)->x*2;
Function<Integer,Integer> c = a.compose(b)

如果按照阅读顺序,那么这个代码实际上应该这么运行

(x+1)*2

但实际上它是

x*2+1

而andThen

Function<Integer,Integer> a = (x)->x+1;
Function<Integer,Integer> b = (x)->x*2;
Function<Integer,Integer> c = a.andThen(b)

似乎才应该是一个默认的使用方式。奇怪的是我看过的关于FP编程中的组合,似乎都是把compose当成默认实现,而把andThen当成是补充。这就很奇怪。

f(f(x))

要理解为什么如此,我们要回到数学的函数上。上一期我们已经做过相同的定义。那么我们继续:

f(x) = x+1
f(x) = x*2

为了能更好的观察,我们回到初中的函数

y1 = x+1
y2 = x*2

当我们要实现x*2+1的时候,我们需要y2替换掉y1中的x,于是我们得到如下:

y1 = y2+1

也就是

y1 = x*2+1

所以,当我们把y1组合y2的时候。我们优先运行的是y2。如果我们把y2组合y1

y2 = y1+1

那么实际运行的是

y2 = (x+1)*2

实际上当我们把两个不同函数组合的时候,我们得到的是新的一个函数

y3 = (x+1)*2

回头看Function<Integer,Integer> c,这就是为什么a组合b,优先运行的却是b的原因。

组合为什么强大

在面向对象编程中,继承是个很重要的特性。 大部分面向对象的教程都会有这么一个例子来说明继承带来的优势:首先有一个animal类,它有一些动物共有的能力(异养)。我们使用birds类继承animal,通过添加fly来添加飞行能力。使用fish类继承animal,通过添加swim来添加游泳能力。 组合也可以,比如,你可以这么理解

// animal(异养)
Function<Integer,Integer> animal = (x)->x+1;
// fly
Function<Integer,Integer> fly = (x)->x*2;
// swim
Function<Integer,Integer> swim = (x)->x/2;

Function<Integer,Integer> birds = fly.andThen(animal);
Function<Integer,Integer> fish = swim.andThen(animal);

当然,必须强调,这是为了让你从面向对象的角度来帮助你去理解组合。 实际上两者有极大的不同。 面向对象要求你从顶层向下思考,你需要先考虑到最顶层的animal,然后去考虑一个duck应该有fly。 FP则要求你把能力优先考虑,有fly的能力,然后你发现了animal。当fly和animal组合后,你得到了birds。这就很有意思了,如果你的fly,结合的是machine,你得到的是airplane。它要求你自下向上的去考虑一个问题。 意识到这个点非常关键,组合会赋予你更强大的灵活性,但对你的抽象思维提出了更高的要求。大多数时候,你可以意识到我要处理birds和fish的时候,它们会有一些共同的顶端特征。但是你很难意识到birds之所以是birds,是因为它是animal和fly的组合。而animal可以和swim组合,fly也可以和machine组合。那么我们能否在一起开始就识别出fly可以抽象出来,就非常考验你对全局的把控了。

类型

需要注意的是,组合必须要把控好类型的适配。上一个函数的出参,类型必须匹配上下一个函数的入参。这很好理解。不过这里也隐藏着“命题即类型(Propositions as Types)”。只要我们的函数证明无误,那么两个函数组合后(类型无误),证明也就是无误的。比如:

Function<Integer,Integer> a = (x)->x+1;
Function<Integer,String> b = (x)->"x+1的运算结果是"+String.value(x);
Function<Integer,String> c = b.compose(a); //正确 命题 一个数字可以转换一个数字,然后可以被转换为一个字符
Function<Integer,String> c = a.compose(b); //错误,类型错误,一个数字可以转换为一个字符,但是无法再转换为一个数字,因为第二个命题是,数字转数字

到这里,我们也可以发现一个问题了,非Function类的函数式接口,很难被组合。大家可以试试。 当然BiFunction可以科里化,这取决于你需不需在这个层级上做组合。Supplier和Consumer要么是起点要么是终点。而Function,嗯一个入参,一个出参,这味道才对嘛。(Supplier和Consumer还有可以深究的点,这么我们以后有机会聊)

总结

本篇主要聊了下我对函数是编程中组合的理解,顺便引导出了关于类型的一些思考。同时也部分呼应了上篇说的两个点:

第一:类型可以不用定义,因为它可以通过上下文推断;

第二:定理名(函数名)可以不用定义,因为它不通用;

本篇也拓展了一个比较有意思的点,比如fly可以和animal组合,也可以和machine组合。 如果说上篇点了个题“何为相等”,这就引出了我接下来想点个题“何为计算”。 待续。。。