一篇不太一样的RxJava介绍(二):关于操作符背后的故事

2,136 阅读10分钟

前言: 上篇文章介绍了Observable这个类的来历。但是操作符是RxJava又一大优势。这篇文章我会介绍一下操作符背后的相关概念。 (读完这篇文章可能会引起身体强烈不适,甚至出现你以前懂操作符,读了之后反而不懂的情况。甚至这篇文章对你开发Android App不会有很大帮助,所以这篇文章需要谨慎阅读)

我们在了解操作符之前,首先要了解几个概念: Monad函数式编程。这里我会一一介绍他们,但是不会太详细,一篇文章肯定不能详细的介绍完这两个巨大的概念,甚至我自己都没有理解透彻这两个概念,但是这并不妨碍我们理解RxJava的操作符。

函数式编程

我们首先来说函数式编程,函数式编程的意义很简单。就是 用函数来编程。或者说,是用数学概念上的函数( mathematical functions )来编程。函数是两个集合之间的一种映射。 我们常常用 f:x -> y 这种形式来表示函数f是从X到Y的一种映射。 用我们熟悉的Kotlin语言来表示就是

    fun f(x:X):Y

但一般这种函数需要满足一下几个条件,我们才说这个函数是一个 Pure Function 也就是纯函数。

  1. 对应一个相同的输入值 x, 一定会获得一个相同的输出值 y。
  2. 在执行 f 的时候不会产生任何副作用

这里,我们又遇到了一个新名词,副作用。我们先来看维基百科对Side Effect的解释:

在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。

也就是说,任何会改变外部状态的操作,都会被考虑为副作用,包括但不仅限于

  1. 对I/O的操作。例如读取文件或者在控制台输出,打印log。
  2. 修改外部变量,或者修改函数本身的参数。
  3. 抛出异常。 等等。

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:

  1. 一个进入盒子的操作(Haskell中的return) return: x -> S x 在RxJava的世界中,更像是一系列产生Observable的操作符,比如create,just,fromXXX等等。比如:
    val x = 10
    Observable.just(x)
    // 这里我们进入了Monad的世界,而这个Monad是我们的Observable
  1. 一个"神秘"的运算bind(haskell中的==>)。 也就是我们结合的能力,他会接收一个函数 f: x -> M y 将两个带有Monad的函数连在一起。

Haskell的定义: (>>=) :: m x -> ( x -> m y) -> m y

我相信大家是看不懂的,我们用Java的语言来形容一下,我们知道Java中函数不是一等公民,不能直接当参数传给方法。我们只能用接口来模拟一个函数。 我们来定义我们的函数 function:

public interface Function<T,R>{
    R apply(T t)
}

T就是我们的输入,R就是我们的输出。(这个其实是Java 8 中的Function接口)。

而这个bind函数,就是接收一个函数f: x ->M y,然后自己生产出一个M y,我们暂时在Java世界中用Monad<X>来代表一个Monad。

public class Monad<X> {
    public Monad<Y> bind(Function<X,Monad<Y>> function) 
}

也就是,我们刚才所说的,结合的能力。我们通过接收一个 x -> M y 将我们的Monad<X>转换成了 Monad<Y>,而不是Monad<Monad<Y>>这样的嵌套操作。 但其实本质上,我们得到的Monad<Y>还是将我们本来的Monad<X>包裹在里面,只是形式上我们得到了Monad<Y>。 这一部分用kotlin 可以更简洁的表达:


class Monad<X>

fun<X,Y> Monad<X>.bind(function:(X) -> Monad<Y>) :Monad<Y>

在上一篇文章中,我曾经说过

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

  1. 左单位元:

    id(X).bind(f:X -> Monad<Y>) = Monad<Y>

    也就是bind 在左边加上id这个函数,他获得的还是 bind的结果Monad本身。 用RxJava 来表示就是

        Observable.just(1)
                .flatMap(new Function<Integer, ObservableSource<String>>() {
                    @Override
                    public ObservableSource<String> apply(Integer integer) throws Exception {
                        return Observable.just(integer.toString());
                    }
                })

  //这里在just之后flatMap的observable 和我们直接使用Observable.just("1")没有任何区别

  1. 右单位元:

    Monad(X).bind(id) = Monad<X>

    也就是 如果Monad和 id 这个函数来进行结合,我们得到的还是Monad 用RxJava 来表示就是

        Observable observable = Observable.just(1)
                .flatMap(new Function<Integer, ObservableSource<Integer>>() {
                    @Override
                    public ObservableSource<Integer> apply(Integer integer) throws Exception {
                        return Observable.just(integer);
                    }
                })
    
    //这里进行过 flatMap 的 observable 和我们的Observable.just(1)没有任何区别
  1. 结合律:
Monad<X>.bind(function :X -> Monad<Y>).bind(function:Y -> Monad<Z>) 
    = Monad<X>.bind(function:x -> Monad<Y>.bind(function: Y -> Monad<Z>))

也就是,将后面两个Monad,Monad合并在一起,再和Monad合并。和先合并,Monad,Monad,在与Monad合并,效果是一样的。 用RxJava 来表示就是

        Observable observable1 = Observable.just(2)
                .flatMap(new Function<Integer, ObservableSource<String>>() {
                    @Override
                    public ObservableSource<String> apply(Integer integer) throws Exception {
                        return Observable.just(integer.toString());
                    }
                })
                .flatMap(new Function<String, ObservableSource<Double>>() {
                    @Override
                    public ObservableSource<Double> apply(String s) throws Exception {
                        return Observable.just(Double.valueOf(s));
                    }
                });

        Observable observable2 = Observable.just(2)
                .flatMap(new Function<Integer, ObservableSource<Double>>() {
                    @Override
                    public ObservableSource<Double> apply(Integer integer) throws Exception {
                        return Observable.just(integer.toString())
                                .flatMap(new Function<String, ObservableSource<Double>>() {
                                    @Override
                                    public ObservableSource<Double> 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的意义不必太过纠结,只需将其理解为一种对集合进行组装变换的一种解决方案即可。

参考文献(部分链接可能需要梯子)

  1. Pure Function
  2. functional programming
  3. 函数副作用
  4. Functor and monad examples in plain Java