For the past two years I have been using RxJS extensively, in Cycle.js and in other contexts. Recently, TylorS and I have decided to build a new reactive streams library comparable to RxJS, called xstream.
TL;DR: we needed a reactive stream library tailored for Cycle.js. It needs to be “hot” only, so users don’t need to think about subscription semantics hidden in drivers. It should have only a few and intuitive operators, those that are relevant to Cycle.js apps. Also, it needs to be really small in kB size in order to keep the size of drivers small.
RxJS Observables are generally cold. This means that two different calls to
subscribe() will generate two separate side effect free executions of the Observable. Hot, on the other hand, is when different calls to
subscribe() may share the same execution of the Observable.
Hot and cold is an issue that is only noticed when subscribing. On the other hand, in Cycle.js, we only allow
subscribe() to happen inside drivers (think “plugins”). This means that application code is unaware of subscriptions. It becomes hard to understand and debug an application when developers make the assumption that there is only one execution for each of the Observables coming from drivers. In reality, it’s a hidden source of problems that they need to be aware of.
The natural expectation of developers using Cycle.js is that the Observables they are handling are hot. Our goal should be to free developers from thinking about what happens inside a driver with regards to subscriptions. They should be focusing on the contents of the
main() function. We have seen developers, both beginners and experts, trip over Observable “temperature” in Cycle.js. Example: this issue.
In xstream, all streams are hot. They are a hybrid between RxJS’s Subject and a publish-refCount cold Observable. All streams keep a list of listeners, and have operators to create new streams dependent on the source stream. Stream execution is lazy through reference counting, with a synchronous start (“connect”) and an asynchronous and cancelable stop (“disconnect”). This is built to allow for those cases where we synchronously swap the single listener of a stream but we don’t want to restart the stream’s execution. The goal is to have a smart default behavior that “just works” transparently in Cycle.js apps. But we want to keep laziness, to avoid wasting resources.
In general, xstream is more suitable than RxJS when the number of
subscribe() calls in an application is very small, and most of the logic is in the operator chain. On the other side, RxJS is more suitable than xstream when there are multiple calls to
subscribe() of multiple Observables, or when RxJS is mixed with imperative programming, object-oriented programming and other diverse settings.
We made a quick survey in the Cycle.js community to check what are the most common operators used on a daily basis. We even ran a small script to gather a histogram of operators used in a codebase. It turns out, out of all the 175+ operators in RxJS, only about 20 of them are commonly used in Cycle.js apps, such as: map, filter, startWith, combineLatest, merge, of, take, scan, skip, flatMap, flatMapLatest, interval, let, distinctUntilChanged, withLatestFrom, takeUntil, concat, last, debounce.
The presence of all the other 155+ operators in RxJS creates noise when choosing operators. While glancing the list, developers are not sure if a particular operator is commonly used or relevant for their problem at hand.
By limiting the amount of operators to choose from, we essentially focus the programmer’s attention on those operators that are highly likely to be useful for their current problem. In xstream there are only 26 core operators. In case more operators are needed there are more in the
extra module, but those few in
core are the default menu to choose from.
A lot of names of operators in RxJS are legacy from Rx.NET and Haskell. In some ways, this is a very good feature if you do cross-platform development, because once you know Rx.NET, you also immediately know RxJava, RxJS, RxSwift, and others. Rx has been even been called a cross-language “DSL” for asynchronous programming.
Our guideline was that a name should be a really strong hint towards the behavior of the operator. For instance the xstream operator
remember() hints that the operator has some memory, it remembers something. In fact, it keeps in memory the most recently emitted event, and broadcasts that stored event to any newly added listener.
fold which works like RxJS’s
scan. We took inspiration from Elm’s Signal
foldp, dropping the
p for “past” since we would rather choose the explicit name
foldPast or simply
fold since there is no other direction to fold from (“foldFuture”?). RxJS’s equivalent of
last in xstream. Our guideline is to make each operator in the chain easy to grok and read, even if it costs more characters to type.
We also noticed how people without a functional programming background had difficulties understanding
flatMap. It usually becomes clear once it is explained as a
map (a value to a stream) plus
flatten. xstream has
flatten is equivalent to RxJS’s
flattenConcurrently is equivalent to RxJS’s
mergeAll. We believe switching is a safer default with regard to resources consumed and expected behavior, that’s why it deserved the
flatten name. We also apply an optimization to blend together operators
flatten as a hidden “switchMap” operator, so there is no additional cost to having two operators when compared to one operator.
When we need to choose between terseness and readability, we choose the latter. For experienced programmers, the cost of typing a few more characters is at worst a first-world problem. If developers can understand code in xstream without reading the documentation, we will have accomplished our goals. Of course, we will test-drive the API with the community. So far we just have a good guess on what is an intuitive API.
xstream’s shorthand as
xs is a convenient pun, because this is a very small library.
When building the next big version of Cycle.js, called Cycle Diversity, we made it possible to build a driver in one stream library while consuming in in an application built with another stream library.
For instance this would enable one to use the Cycle DOM driver written in RxJS v4 with a Cycle.js app written in RxJS v5. However, the off-the-shelf packages for these libraries are quite large (200+ kB each). Including both of them would be 400+ kB, before any user code is added. There are ways of customizing them to have only what is needed, but there is still some common large boilerplate to bring along, and it is quite common for developers not to customize their consumed variant of RxJS.
We wanted to provide a smart default. xstream is under 30 kB in size, and it is ideal for using as a stream library in a driver. Cycle applications can still be written in RxJS v5, for instance, while utilizing a driver written in xstream.
xstream is not a competitor to RxJS in those cases. It is instead a complementary library, that distills the best of RxJS in the most appropriate API for Cycle.js apps.
If you liked this article, consider sharing (tweeting) it to your followers.