yafrl-core

Yet Another Functional Reactive Library (yafrl) is a utility library extending the capabilities of kotlinx-coroutines by following an alternative philosophy of how reactive programming should work. In particular, we offer "better-behaved" versions of constructs such as Flow and StateFow,

yafrl is based on the original ideas of functional reactive programming, and is heavily inspired by the Haskell library Reflex.

As such, tutorials on Reflex like this excellent one from Queensland FP Labs would prove to be a useful introduction to the topic, provided one is willing to read some Haskell!

However, for those who are not, we provide a simple introduction below, together with 3 improvements that yafrl offers over kotlinx-coroutine.

Why yafrl?

kotlinx-coroutines (and specifically the kotlinx.coroutines.flow part) is decent for what it is -- and for those who have previously used frameworks like rxJava, it is definitely a breath of fresh air.

Constructs like Flow and StateFlow allow you to define loosely coupled business logic components with reactive state that can be consumed by front-end components in a clean way with patterns such as MVVM (Model-View-ViewModel) and MVI (Model-View-Intent). And Flows also integrate very well with Kotlin's structured concurrency, providing innumerable benefits for application developers.

kotlinx-coroutines is also a massive improvement semantically over "conventional" reactive frameworks in that it actually provides a mechanism for reactive states -- that is, StateFlow. In other reactive frameworks such as rxJava, while similar concepts could be implemented, the lack of a distinct type made trying to implement constructs like that finicky and error-prone. However, kotlinx-coroutines still has some frustrating issues that yafrl solves.

More convenient reactive state operations

One of the issues you may have encountered if you've been using kotlinx-coroutines for a while is the fact that there is no StateFlow<A>.map((A) -> B): StateFlow<B> operator. If we try to map a StateFlow, it will just use Flow<A>.map((A) -> B): Flow<B>, with StateFlow<A> being implicitly upcast to Flow<A>.

map is a generic idiom coming from functional programming technically known as a Functor -- and one of the key requirements for a map operation to be a functor is that when we map something, we get out the same type of container that we put in! (a type of closure property)

Similar issues hold for other methods of Flow / StateFlow -- such as combine. This is actually another functional programming idiom called Applicative, which defines a common API for data structures that can "combined" or "zipped" together with other data structures of the same kind.

kotlinx-coroutines provides the stateIn operator for converting Flows back into StateFlows again to help solve this problem -- but in code where StateFlows are often manipulated with functional operations, this starts to get old very quickly.

Solid mathematical foundations

The bigger issue, however, is that kotlinx-coroutines is built on very operational foundations. In other words, in order to understand how any concept relating to Flows works, you much understand very low-level imperative concerns such as how the Flow interacts with subscribers.

While basic usages of yafrl may look similar to kotlinx-coroutines on the surface -- in practice it is very different because it is based on elegant mathematical foundations that give you an easy to think about mental model of the constructs in the library that you can expect to translate directly into actual behavior at runtime, without having to worry about low-level details.

In fact, the biggest difference you'll notice with yafrl code as compared to kotlinx-coroutines is that while there is a collect operator on States and Events for convenience’s sake (so you can interoperate with other kotlin libraries expecting Flows and StateFlows more easily) -- its use is strongly discouraged in both the business logic and presentation logic layers of your application. If you feel tempted to use collect for anything other than debugging, or integrating with some kind of external framework -- you're probably doing more harm than good, and should ask yourself if there's a better way.

Easy to test

Even if none of the above has convinced you, one of the most damning issues with kotlinx-coroutines might well be the fact that code using Flows and StateFlows have the tendency to be flaky, slow, as well as just plain difficult to test!

We have several examples of this in our test suite under the negative_tests package -- showing examples where kotlinx-coroutines behaves in unexpected ways, requiring things like having to insert manual delays in order to get tests to pass, and even how even the kotlinx-coroutines-test module does not provide sufficent tools to be able to sufficiently ameliorate these problems.

Since yafrl is synchronous by default, writing tests for yafrl code is just as easy as writing tests for normal synchronous Kotlin -- while still providing integration with kotlinx-coroutine's asynchronous features when necessary.

Packages

Link copied to clipboard
common

User-facing (public) APIs for yafrl.

Link copied to clipboard
common
Link copied to clipboard
common
Link copied to clipboard
common

Internal yafrl APIs -- not intended to be used directly by users of the library in most use-cases.

Link copied to clipboard
common
Link copied to clipboard
common
Link copied to clipboard
common