前言: 上篇文章介绍了Observable这个类的来历。但是操作符是RxJava又一大优势。这篇文章我会介绍一下操作符背后的相关概念。
(读完这篇文章可能会引起身体强烈不适,甚至出现你以前懂操作符,读了之后反而不懂的情况。甚至这篇文章对你开发Android App不会有很大帮助,所以这篇文章需要谨慎阅读)
我们在了解操作符之前,首先要了解几个概念: Monad 和 函数式编程。这里我会一一介绍他们,但是不会太详细,一篇文章肯定不能详细的介绍完这两个巨大的概念,甚至我自己都没有理解透彻这两个概念,但是这并不妨碍我们理解RxJava的操作符。
函数式编程
我们首先来说函数式编程,函数式编程的意义很简单。就是 用函数来编程。或者说,是用数学概念上的函数( mathematical functions )来编程。函数是两个集合之间的一种映射。
我们常常用 f:x -> y 这种形式来表示函数f是从X到Y的一种映射。
用我们熟悉的Kotlin语言来表示就是
fun f(x:X):Y
但一般这种函数需要满足一下几个条件,我们才说这个函数是一个 Pure Function 也就是纯函数。
- 对应一个相同的输入值 x, 一定会获得一个相同的输出值 y。
- 在执行 f 的时候不会产生任何副作用。
这里,我们又遇到了一个新名词,副作用。我们先来看维基百科对Side Effect的解释:
在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。
也就是说,任何会改变外部状态的操作,都会被考虑为副作用,包括但不仅限于
- 对I/O的操作。例如读取文件或者在控制台输出,打印log。
- 修改外部变量,或者修改函数本身的参数。
- 抛出异常。
等等。
Side Effect在函数式编程被认为是不好的东西。因为它太不可控了,比如常用的System.currentTimeMillis()
方法。
我们每次调用这个方法,都会返回一个不同的值,这便是所谓的不可控。再比如readLine()
函数,我们也无法知道他究竟会读取哪一行。
但是反过来,如果我们不是生活在“美好”的纯函数世界里。在我们的世界里,如果没有side effect,几乎做不了任何事。没有Side Effect我们甚至都不会接收到用户输入,因为用户的输入,比如屏幕点击都是一个Side Effect。为了解决这个问题,在Haskell
(一种纯函数式编程语言)中,引入了Monad,来控制Side Effect。
Monad
我们说Side Effect虽然是不好的,但是是有用的。我们不希望消除Side Effect,我们更希望的是Side Effect在我们掌握之中,是可控的。所以引入Monad,来控制Side Effect。
Monad 在函数式编程中,有太多的教程,文章来解释。但是看了之后都云里雾里,甚至有人说过:
The curse of the monad is that once you get the epiphany, once you understand - "oh that's what it is" - you lose the ability to explain it to anybody.
Monad的诅咒就是一旦你理解他了,你就失去了向别人解释他的能力。
我不敢说这个诅咒在我这篇文章中消除了,我只能尽我所能,用一个Android开发者读得懂的语言尽力解释这个概念,所以我也在前言中提到了,这篇文章读后可能会引起严重不适。
So,言归正传,什么是Monad。
我们回到刚才的纯函数, 一个纯函数比如
f : x -> y
我们如何给他加入一个可控的Side Effect?
有一种做法便是,把Side Effect统统装进一个盒子里,和y一起当做输出值输出。
比如
f : x -> S y
S 代表了在输出y之前一系列Side Effect相关的操作。 但是这样的问题就是,我们如果连续进行好几个Side Effect操作。我们都要带着这个S,比如我们有两个函数f,g:
f : x -> S y
g : y -> S z
那么我们连续调用f,g之后,那结果就变成了:
f (g(x)) : x -> S(Sz)
这里Monad就要显示他的作用了。 很明显,我们需要一种“组合”的能力,将两个S结合成一个,我们更希望多个S可以结合成一个,比如这样:
f(g(x)) : x -> S z
一个Monad 我们简单的定义为有包含如下两个操作的盒子S:
- 一个进入盒子的操作(Haskell中的return) return: x -> S x
在RxJava的世界中,更像是一系列产生Observable的操作符,比如create
,just
,fromXXX
等等。比如:
val x = 10
Observable.just(x)
// 这里我们进入了Monad的世界,而这个Monad是我们的Observable
- 一个"神秘"的运算bind(haskell中的==>)。 也就是我们结合的能力,他会接收一个函数 f: x -> M y 将两个带有Monad的函数连在一起。
Haskell的定义: (>>=) :: m x -> ( x -> m y) -> m y
我相信大家是看不懂的,我们用Java的语言来形容一下,我们知道Java中函数不是一等公民,不能直接当参数传给方法。我们只能用接口来模拟一个函数。
我们来定义我们的函数 function:
public interface Function{
R apply(T t)
}
T就是我们的输入,R就是我们的输出。(这个其实是Java 8 中的Function接口)。
而这个bind函数,就是接收一个函数f: x ->M y,然后自己生产出一个M y,我们暂时在Java世界中用Monad
来代表一个Monad。
public class Monad {
public Monad bind(Function> function)
}
也就是,我们刚才所说的,结合的能力。我们通过接收一个 x -> M y 将我们的Monad
转换成了 Monad
,而不是Monad
>这样的嵌套操作。
但其实本质上,我们得到的Monad
还是将我们本来的Monad
包裹在里面,只是形式上我们得到了Monad
。
这一部分用kotlin 可以更简洁的表达:
class Monad
fun Monad.bind(function:(X) -> Monad) :Monad
在上一篇文章中,我曾经说过
Collection可以通过高阶函数(High Oroder Function)进行组合,变换等等,所以作为集合之一的Observable也可以进行组合,变换。
但是其实这句话是错误的,因为在上一篇文章中,我们并没有Monad,函数式等等的知识,我们只能先这么理解。而给予Observable这个组合,变换能力的其实就是这个Monad。
结论1 :
Observable 是一个 monad
如果入门RxJava是从RxJava1 和 扔物线大佬的给 Android 开发者的 RxJava 详解这篇的话。 会知道RxJava 1中有一个
lift()
操作符。是几乎所有操作符的“父”操作符,其实这也就是Monad中的bind的一个具体实现。也有人将flatMap
理解为Monad中的bind,我个人认为是不对的。他们虽然签名是一致的,效果也是一样的。但是flatMap操作符在RxJava中的实现和其他操作符是非常不一样的。而lift()
在RxJava 1.x 中就担任了所有操作符的抽象的工作。也就是我们说的接收一个 x-> Observable y 这样一个函数,来将Observable x 转换为 Observable y这样一个过程。而在RxJava2 中,由于性能问题,lift()操作符实现改为了直接继承Observable,来将lift的操作写到subscribeActual()
来进行操作。这样虽然减少了性能损耗,但是正确的写一个操作符却变得更加困难一些。
当然,不是仅仅有return 和 bind 就可以是Monad,Monad 还需要满足如下三个规则:
这里我们用id(X) 来代表return
-
左单位元:
id(X).bind(f:X -> Monad
) = Monad 也就是bind 在左边加上id这个函数,他获得的还是 bind的结果Monad
本身。
用RxJava 来表示就是
Observable.just(1)
.flatMap(new Function>() {
@Override
public ObservableSource apply(Integer integer) throws Exception {
return Observable.just(integer.toString());
}
})
//这里在just之后flatMap的observable 和我们直接使用Observable.just("1")没有任何区别
-
右单位元:
Monad(X).bind(id) = Monad
也就是 如果Monad
和 id 这个函数来进行结合,我们得到的还是Monad
用RxJava 来表示就是
Observable observable = Observable.just(1)
.flatMap(new Function>() {
@Override
public ObservableSource apply(Integer integer) throws Exception {
return Observable.just(integer);
}
})
//这里进行过 flatMap 的 observable 和我们的Observable.just(1)没有任何区别
- 结合律:
Monad.bind(function :X -> Monad).bind(function:Y -> Monad)
= Monad.bind(function:x -> Monad.bind(function: Y -> Monad))
也就是,将后面两个Monad
用RxJava 来表示就是
Observable observable1 = Observable.just(2)
.flatMap(new Function>() {
@Override
public ObservableSource apply(Integer integer) throws Exception {
return Observable.just(integer.toString());
}
})
.flatMap(new Function>() {
@Override
public ObservableSource apply(String s) throws Exception {
return Observable.just(Double.valueOf(s));
}
});
Observable observable2 = Observable.just(2)
.flatMap(new Function>() {
@Override
public ObservableSource apply(Integer integer) throws Exception {
return Observable.just(integer.toString())
.flatMap(new Function>() {
@Override
public ObservableSource apply(String s) throws Exception {
return Observable.just(Double.valueOf(s));
}
});
}
});
//这里 observable1 和 observable2 等价
遵守以上三个规则,并且拥有return/id 和 bind的“盒子”,我们就称之为一个Monad。我们在理解Monad之后,会发现我们身边很多东西,甚至每天都在用的一些东西,他就是Monad。
比如C#中的LINQ是Monad,Java 8新引入的CompletableFuture
和Stream API是Monad, JavaScript中的Promise
是Monad,RxJava中的Observable是Monad。
这也就解释了很多人在理解RxJava源码的时候,不理解为什么 Observable 操作符要写成这种 Observable套着Observable。最终互相通知的形式。
如:(这里为了简化我们使用Kotlin来写)
Observable.just(1, 2, 3, 4)
.map{x -> x +1}
.filter { x -> x >3 }
.flatMap { x -> Observable.just(x,x+2) }
这其实生成的Observable是 ObservableFlatMap(ObservableFilter(ObservableMap(ObseravbleJust(1,2,3,4))))
这样一个一层层嵌套的Observable盒子。而赋予其嵌套能力,并将其省略为仅仅一个Observable
强大力量的便是Monad。
所以我们得出一个结论2
Observable的操作符 Monad中 bind 的一个具体实现形式。
而这个结论并不适合所有操作符,有一些特殊操作符会从Monad中跳出返回我们正常的Java/Kotlin世界。比如Subscribe
,blockingFirst()
,forEach()
等等。
这些是我们跳出Monad/Observable世界的出口。
总结:
这篇我主要介绍了函数是编程和Monad的概念,着重介绍了Monad和Observable紧密的关系。个人认为如果对函数式编程不感兴趣,对Monad的意义不必太过纠结,只需将其理解为一种对集合进行组装变换的一种解决方案即可。
参考文献(部分链接可能需要梯子)
- Pure Function
- functional programming
- 函数副作用
- Functor and monad examples in plain Java