RxCPP(一)编程模型入门
编程模型将涉及到以下几块内容:
- 数据流计算范例
- rxcpp库的介绍
- Rx操作符
- 调度
- flat/ concatmap的区别
- 更多重要的操作符
数据流计算简介
在函数响应式编程(FRP)中,所有这些主题都以系统的方式结合在一起。
简单地说,响应式编程就是使用异步数据流进行编程。通过对流应用各种操作,我们可以实现不同的计算目标。响应式程序中的主要任务是将数据转换为流,而不管数据的来源是什么。事件流通常称为可观察对象,事件流订阅者称为观察者。在可观察对象和观察者之间,存在流操作符(过滤器/转换)。
由于隐式假设在数据通过操作符传递时数据源不会发生变化,所以在可观察对象和观察者之间可以有多个操作符路径。不变性为无序执行提供了选项,并且可以将调度委托给称为调度程序的特殊软件。因此,可观测对象、观察者、流操作符和调度程序构成了FRP模型的主干。
数据流计算范例
传统上,程序员根据控制流来编码他们的程序。这意味着我们将程序编码为一系列小语句(序列、分支、迭代)或函数(包括递归),以及它们的关联状态。我们使用选择(if/else)、迭代(while/for)和递归函数等构造对计算进行编码。为这些类型的程序处理并发性和状态管理确实存在问题,并且会导致一些细微的bug。我们需要围绕共享可变状态放置锁和其他同步原语。在编译器级别,语言编译器将解析源代码以创建抽象语法树(AST),执行类型分析和代码生成。事实上,AST是一个信息流图,您可以在其中执行数据流分析(用于数据/寄存器级优化)和控制流分析,以利用处理器级的代码管道优化。即使程序员根据控制流来编码程序,编译器(至少部分)也会根据数据流来查看程序。这里的底线是,在每个程序中都有一个隐式数据流图处于休眠状态。
数据流计算将计算组织为显式图,其中节点是计算,边是数据在节点之间流动的路径。如果我们对节点上的计算施加某些限制(例如通过处理输入数据的副本来保存数据状态,避免使用就地算法),我们就可以利用并行性的机会。调度程序将通过对图数据结构进行拓扑排序来寻找并行的机会。我们将使用流(路径)和流(节点)上的操作构造图。这可以通过声明的方式实现,因为操作符可以被编码为lambdas,它可以执行一些本地计算。函数式编程社区标识了一组基本的标准(函数/流)操作符,如map、reduce、filter和take。在数据流计算框架中有一个条款是将数据转换为流。用于机器学习的tensorflow库就是使用这种范例的一个库。即使图创建并不像在tensorflow中那样完全显式的,但rxcpp库也可以看作是一个数据流计算库。由于函数式编程构造支持延迟计算,所以在构造具有异步数据流和操作的流管道时,我们正在创建一个计算流图。
rxcpp库的介绍
rxcpp库是一个只读的c++库,可以从github下载。RxCpp依赖于现代c++结构,如语言级并发性、lambda函数/表达式、函数组合/转换和操作符重载,来实现反应性编程结构。rxcpp库的结构类似于rx.net和rxjava等库。
与任何其他反应性编程框架一样,在编写第一行代码之前,每个人都应该理解一些关键构造。它们是:
- 可观察对象(可观察到的流)
- 观察者(订阅观察对象)
- 操作符(过滤、转换和归约)
- 调度器
rxcpp大部分计算都是基于可观测的概念。该库提供了大量原语来创建来自各种数据源的可观察流。数据源可以是范围、stl容器等等。我们可以在可观察对象和它们的消费者之间放置操作符(称为观察者)。由于函数编程构造支持函数的组合,所以我们可以将操作符链作为单个实体放在可观察对象和订阅流的观察者之间。与库关联的调度程序将确保当数据以可观察流的形式可用时,它将通过操作符传递,并且在经过一系列筛选和转换之后,如果有数据存在,将向订阅者发出通知。当调用订阅者中的一个lambda方法时,观察者需要考虑一些事情。观察员可以把注意力集中在他们主要负责的任务上。
Rx操作符
一个简单的可观察/观察者交互
让我们编写一个简单的程序来帮助我们理解rxcpp库的编程模型。在这个特殊的程序中,我们将有一个可观察的流和一个订阅流的观察者。我们将使用range对象生成一系列从1到12的数字。在创建值的范围及其上的可观察值之后,我们将为可观察值附加一个订阅者。当我们执行程序时,它会打印一系列的数字到控制台,并进行额外的测试:First.cpp
////////// //first.cpp // g++ -I<PathToRxcpplibfoldersrc> First.cpp // #include "rxcpp/rx.hpp" #include <iostream> int main() { //------------- Create an Observable.. a stream of numbers auto observable = rxcpp::observable<>::range(1, 12); //------------ Subscribe (only OnNext and OnCompleted Lambda given observable. subscribe( [](int v){printf("OnNext: %d\n", v);}, [](){printf("OnCompleted\n");}); }
可观察对象的过滤和转换
下面的程序将帮助我们理解过滤(filter)和映射(map)操作符的工作原理,以及使用订阅方法将观察者连接到可观察流的常用机制。filter方法对流的每个项计算谓词,如果计算碰巧产生一个正断言,则该项将出现在输出流中。map操作符对输入流的每个元素应用一个表达式,并帮助生成一个输出队列:
//------------------ Second.cpp #include "rxcpp/rx.hpp" #include <iostream> #include <array> int main() { auto values = rxcpp::observable<>::range(1, 12). filter([](int v) { return v % 2 == 0; }).map([](int x) {return x * x; }); values. subscribe( [](int v) {printf("OnNext: %d\n", v); }, []() {printf("OnCompleted\n"); }); }
从c++容器中流化值
即使rx用于处理随时间变化的数据,我们也可以将stl容器转换为响应流。我们需要使用iterate操作符来进行转换。这有时很方便,并有助于从使用stl的代码库集成代码:
//------------------ STLContainerStream.cpp #include "rxcpp/rx.hpp" #include <iostream> #include <array> int main() { std::array< int, 3 > a={{1, 2, 3}}; auto values1 = rxcpp::observable<>::iterate(a); values1. subscribe( [](int v){printf("OnNext: %d\n", v);}, [](){printf("OnCompleted\n");}); }
从零开始创建可观察对象
到目前为止,我们已经编写了从范围对象或stl容器创建可观察流的程序。让我们看看如何从头创建一个可观察的流:
//------------------ ObserverFromScratch.cpp #include "rxcpp/rx.hpp" #include "rxcpp/rx-test.hpp" int main() { auto ints = rxcpp::observable<>::create<int>([](rxcpp::subscriber<int> s) { s.on_next(1); s.on_next(4); s.on_next(9); s.on_completed(); }); ints.subscribe([](int v) {printf("OnNext: %d\n", v); }, []() {printf("OnCompleted\n"); }); }
连接可观察到的流
concat
不交错的发射两个或多个Observable的发射物
Concat
操作符连接多个Observable的输出,就好像它们是一个Observable,第一个Observable发射的所有数据在第二个Observable发射的任何数据前面,以此类推。直到前面一个Observable终止,'concat'才会订阅额外的一个Observable。注意:因此,如果你尝试连接一个"热"Observable(这种Observable在创建后立即开始发射数据,即使没有订阅者),'concat'将不会看到也不会发射它之前发射的任何数据。
startwith
操作符类似于'concat',但是它是插入到前面,而不是追加那些Observable的数据到原始Observable发射的数据序列:
merge
操作符也差不多,它结合两个或多个Observable的发射物,但是数据可能交错,而concat
不会让多个Observable的发射物交错。
我们可以将两个流连接起来形成一个新的流,这在某些情况下非常方便。让我们通过编写一个简单的程序来看看它是如何工作的:
//------------------ Concat.cpp #include "rxcpp/rx.hpp" #include <iostream> #include <array> int main() { auto o1 = rxcpp::observable<>::range(1, 3); auto o2 = rxcpp::observable<>::range(4, 6); auto values = o1.concat(o2); values. subscribe( [](int v) {printf("OnNext: %d\n", v); }, []() {printf("OnCompleted\n"); }); }
创建一个发射指定值的Observable
Just将单个数据转换为发射那个数据的Observable:
//------------------ sixth.cpp #include "rxcpp/rx.hpp" #include <iostream> #include <array> int main() { auto values = rxcpp::observable<>::just(1); values. subscribe( [](int v) {printf("OnNext: %d\n", v); }, []() {printf("OnCompleted\n"); }); }
Take
只发射前面的N项数据
使用take
操作符让你可以修改Observable的行为,只返回前面的N项数据,然后发射完成通知,忽略剩余的数据。
如果你对一个Observable使用take(n)(或它的同义词limit(n))操作符,而那个Observable发射的数据少于N项,那么
take
操作生成的Observable不会抛异常或发射onError通知,在完成前它只会发射相同的少量数据。
//------------------ fifth.cpp #include "rxcpp/rx.hpp" #include <iostream> #include <array> int main() { auto values = rxcpp::observable<>::range(1); // infinite (until overflow) stream of integers auto s1 = values. take(3). map([](int prime) { return std::make_tuple("1:", prime); }); auto s2 = values. take(3). map([](int prime) { return std::make_tuple("2:", prime); }); s1. concat(s2). subscribe(rxcpp::util::apply_to( [](const char* s, int p) { printf("%s %d\n", s, p); })); }
其中,apply_to可以替换成一下方式:
subscribe( [](std::tuple<const char*, int> p) { printf("%s, %d\n",std::get<0>(p),std::get<1>(p)); });
从可观察的流取消订阅
下面的程序显示了如何订阅一个可观察流并停止订阅。程序只显示了可用的选项,应该参考文档:
//---------------- Unsubscribe.cpp #include "rxcpp/rx.hpp" #include <iostream> int main() { auto subs = rxcpp::composite_subscription(); auto values = rxcpp::observable<>::range(1, 10); values.subscribe( subs, [&subs](int v) { printf("OnNext: %d\n", v); if (v == 6) subs.unsubscribe(); //-- Stop recieving events }, []() {printf("OnCompleted\n"); }); }
map
对Observable发射的每一项数据应用一个函数,执行变换操作
map操作符对原始Observable发射的每一项数据应用一个你选择的函数,然后返回一个发射这些结果的Observable。
大理石图的顶部显示了两个时间线,这些时间线通过将第二个时间线的内容附加到第一个时间线来组合在一起,形成一个复合时间线。
Map.cpp
//------------------ Map.cpp #include "rxcpp/rx.hpp" #include <iostream> #include <array> int main() { auto ints = rxcpp::observable<>::range(1, 10). map([](int n) {return n * n; }); ints.subscribe( [](int v) {printf("OnNext: %d\n", v); }, []() {printf("OnCompleted\n"); }); }
rxcpp(流)操作符
面向流处理的一个主要优点是,我们可以将函数式编程原语应用于它们。用rxcpp的话说,处理是使用操作符完成的。它们只是流上的过滤、转换、聚合和规约。在前面的示例中,我们已经了解了map、filter和take操作符的工作原理。
平均运算符
平均运算符从可观察的流计算值的算术平均值。所支持的其他统计运算符包括:
- 最小值
- 最大值
- 计数
- 求和
计算原始Observable发射数字的平均值并发射它
下面的程序只演示了平均操作符。对于前面列表中的其他操作符,模式是相同的Average.cpp
//----------- Average.cpp #include "rxcpp/rx.hpp" #include <iostream> int main() { auto values = rxcpp::observable<>::range(1, 20).average(); values. subscribe( [](double v) {printf("average: %lf\n", v); }, []() {printf("OnCompleted\n"); }); }
扫描操作符
扫描操作符对流的每个元素依次应用一个函数,并将该值累积为种子值。下面的程序生成一系列数字的平均值,这些值是在何时累计的。
连续地对数据序列的每一项应用一个函数,然后连续发射结果
Scan操作符对原始Observable发射的第一项数据应用一个函数,然后将那个函数的结果作为自己的第一项数据发射。它将函数的结果同第二项数据一起填充给这个函数来产生它自己的第二项数据。它持续进行这个过程来产生剩余的数据序列。这个操作符在某些情况下被叫做accumulator。
//----------- Scan.cpp #include "rxcpp/rx.hpp" #include <iostream> int main() { int count = 0; auto values = rxcpp::observable<>::range(1, 20). scan( 0, [&count](int seed, int v) { count++; return seed + v; }); values.subscribe( [&](int v) {printf("Average through Scan: %f\n", (double)v / count); }, []() {printf("OnCompleted\n"); }); }
通过管道组合运算符
RxCpp库允许您连接或组合运算符以启用运算符组合。 该库允许您使用管道(|)运算符来组合运算符,程序员可以将一个运算符的输出传递给另一个运算符,就好像它们位于UNIX shell的命令行中一样。 这使我们能够理解一段代码的作用。下面的程序使用| 运算符以映射范围。RxCpp样品含有使用管功能的例子很多
//------------------ Map_With_Pipe.cpp #include "rxcpp/rx.hpp" namespace Rx { using namespace rxcpp; using namespace rxcpp::sources; using namespace rxcpp::operators; using namespace rxcpp::util; } using namespace Rx; #include <iostream> int main() { auto ints = rxcpp::observable<>::range(1, 10) | map([](int n) {return n * n; }); ints.subscribe( [](int v) {printf("OnNext: %d\n", v); }, []() {printf("OnCompleted\n"); }); }
以上的管道操作等价于:
auto ints = rxcpp::observable<>::range(1, 10); auto intsFromMap = ints.map([](int n) {return n * n; });