# Understanding the RPL (Reactive Programming Library) Pattern in Telegram Desktop

> Discover the RPL reactive programming pattern in Telegram Desktop. Learn how time-varying values and composable streams manage UI state changes efficiently with rpl::producer and rpl::lifetime.

- Repository: [Telegram Desktop/tdesktop](https://github.com/telegramdesktop/tdesktop)
- Tags: internals
- Published: 2026-04-05

---

**RPL is a lightweight reactive programming library that models time-varying values as `rpl::producer<T>` streams, enabling Telegram Desktop's UI components to subscribe to state changes through composable pipelines that automatically detach via `rpl::lifetime` objects.**

Telegram Desktop implements its user interface layer using a custom **Reactive Programming Library (RPL)** found in the `Telegram/SourceFiles/rpl/` directory of the `telegramdesktop/tdesktop` repository. Unlike traditional imperative event handling, this pattern treats any value that changes over time—such as window layout states, animation frames, or user settings—as a push-based stream. Widgets declare their dependencies on these streams using a fluent operator syntax, eliminating manual `connect` and `disconnect` boilerplate while ensuring deterministic cleanup when components are destroyed.

## Core Concepts of RPL

### Producers and Consumers

At the heart of RPL lies the **`rpl::producer<T>`**, a template class representing a stream that emits values of type `T` to any number of subscribers. Producers are lazy; they only begin emitting when a **consumer** subscribes. For example, in [`Telegram/SourceFiles/window/window_top_bar_wrap.h`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/window/window_top_bar_wrap.h) at line 22, the method `rpl::producer<bool> oneColumnValue()` exposes a boolean stream that emits whenever the application switches between one-column and multi-column layout modes.

Consumers are typically anonymous functions passed to **`rpl::on_next`**, which initiates the subscription. The library handles the plumbing between these endpoints, allowing developers to focus on transformation logic rather than state synchronization.

### The Pipeline Syntax (| Operator)

RPL implements a Unix-pipe-inspired syntax using the bitwise OR operator `|` to chain operations. This creates readable **transformation pipelines** where data flows from left to right:

```cpp
std::move(producer) | rpl::filter(predicate) | rpl::map(transformer) | rpl::on_next(callback, lifetime);

```

According to the implementation in [`Telegram/SourceFiles/rpl/producer.h`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/rpl/producer.h), each combinator returns a new producer, enabling lazy evaluation and composition without side effects until the final subscription.

### Lifetime Management

Memory safety in RPL relies on **`rpl::lifetime`** objects passed to `on_next`. When a UI widget is destroyed, its associated `rpl::lifetime` automatically detaches all callbacks, preventing dangling references. This pattern appears throughout [`Telegram/SourceFiles/window/window_session_controller.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/window/window_session_controller.cpp) (around line 3401), where subscriptions tied to widget lifetimes ensure cleanup occurs without explicit `disconnect` calls.

## Key Combinators and Transformations

### Mapping Values with rpl::map

The **`rpl::map`** combinator transforms values from one type to another. In [`Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp) at line 94, the code maps internal `Shift` values to public API types:

```cpp
rpl::producer<PickerAnimation::Shift> PickerAnimation::updates() const {
    return _updates.events();
}

// Usage elsewhere:
std::move(picker->updates()) | rpl::map([](auto shift) {
    return shift.value * 2;
}) | rpl::on_next([](int doubled) {
    // React to transformed value
}, _lifetime);

```

### Filtering Streams

**`rpl::filter`** skips unwanted values based on a predicate. Combined with the **`rpl::mappers`** namespace—which provides common functors like `identity`—it simplifies conditional logic:

```cpp
using namespace rpl::mappers;

std::move(oneColumn) | rpl::filter(rpl::mappers::identity) | rpl::on_next([=](bool one) {
    _topBar->setOneColumn(one);
}, _lifetime);

```

This pattern from [`Telegram/SourceFiles/window/window_session_controller.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/window/window_session_controller.cpp) ensures the callback only executes when the boolean value is true, filtering out `false` emissions.

### Combining Multiple Producers

When UI elements depend on several state sources, **`rpl::combine`** synchronizes their latest values into a single callback. In [`Telegram/SourceFiles/window/window_adaptive.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/window/window_adaptive.cpp) at line 32, layout flags merge as follows:

```cpp
auto oneColumn = oneColumnValue();
auto chatWide = chatWideValue();

std::move(rpl::combine(oneColumn, chatWide))
    | rpl::on_next([=](bool one, bool wide) {
        // Update UI based on both flags simultaneously
    }, _lifetime);

```

The combinator waits for both producers to emit at least once, then calls the handler with the most recent values from each stream.

### Merging Event Streams

**`rpl::merge`** aggregates multiple producers of the same type into a unified stream, useful for handling different input sources identically. The pattern appears in [`Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp):

```cpp
auto contextMenu = contextMenuRequested();
auto lockClick = lockedClicked();

auto merged = rpl::merge(contextMenu, lockClick);
std::move(merged) | rpl::on_next([=](int event) {
    // Handle either context-menu request or lock click uniformly
}, _lifetime);

```

Unlike `combine`, `merge` interleaves values from all sources without waiting for synchronization.

## Architectural Role in the Codebase

RPL decouples data sources from UI consumers by enforcing a **unidirectional data flow**. Widgets expose `rpl::producer` accessors (as seen in [`Telegram/SourceFiles/window/window_adaptive.h`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/window/window_adaptive.h) lines 30-39) without revealing implementation details, while consumers bind to these streams declaratively. This architecture eliminates complex observer patterns and race conditions, as state changes propagate automatically through the reactive graph.

Asynchronous operations—such as network responses or animation timers—integrate seamlessly by returning producers that emit results upon completion. The `rpl::lifetime` mechanism guarantees that callbacks execute only while the consuming widget exists, preventing use-after-free errors common in traditional C++ GUI code.

## Where RPL Lives in the Codebase

| File | Purpose |
|------|---------|
| [`Telegram/SourceFiles/rpl/producer.h`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/rpl/producer.h) | Defines `rpl::producer<T>` and the core subscription API. |
| [`Telegram/SourceFiles/rpl/on_next.h`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/rpl/on_next.h) | Implements the `on_next` starter for consumer attachment. |
| [`Telegram/SourceFiles/rpl/map.h`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/rpl/map.h) | Provides the `map` transformation operator. |
| [`Telegram/SourceFiles/rpl/filter.h`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/rpl/filter.h) | Implements the `filter` combinator. |
| [`Telegram/SourceFiles/rpl/combine.h`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/rpl/combine.h) | Defines `combine` for synchronizing multiple producers. |
| [`Telegram/SourceFiles/window/window_adaptive.h`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/window/window_adaptive.h) | Exposes layout state producers (lines 30-39). |
| [`Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h) | Declares animation update producers (lines 24-28). |

## Summary

- **RPL** treats dynamic values as `rpl::producer<T>` streams that emit to registered consumers via `rpl::on_next`.
- The pipe operator `|` chains transformations (`map`, `filter`, `combine`, `merge`) into composable pipelines.
- **`rpl::lifetime`** objects passed to subscriptions ensure automatic cleanup when UI components are destroyed.
- Found in `Telegram/SourceFiles/rpl/`, this pattern powers Telegram Desktop's declarative UI updates across files like [`window_session_controller.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/window_session_controller.cpp) and [`window_adaptive.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/window_adaptive.cpp).

## Frequently Asked Questions

### What is the difference between rpl::producer and rpl::consumer?

A **`rpl::producer<T>`** is a source that emits values over time, while a consumer is any entity that receives these values—typically a lambda passed to `rpl::on_next`. Producers are passive until a consumer subscribes, at which point the stream activates and begins pushing data through the pipeline.

### How does RPL prevent memory leaks and dangling callbacks?

RPL uses **`rpl::lifetime`** objects that track active subscriptions; when the lifetime object is destroyed (usually when a widget is deleted), all associated callbacks automatically detach. This deterministic cleanup happens in files like [`Telegram/SourceFiles/window/window_session_controller.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/window/window_session_controller.cpp), where subscriptions are tied to widget member lifetimes.

### When should I combine producers versus merging them?

Use **`rpl::combine`** when you need the latest values from multiple producers simultaneously, as it waits for all sources to emit and provides synchronized tuples. Use **`rpl::merge`** when processing interleaved events of the same type from different sources, such as handling clicks from multiple buttons through a single handler.

### Why does Telegram Desktop use a custom RPL instead of Qt's signals and slots?

While Qt provides signals and slots, RPL offers **composable stream operations** (map, filter, combine) through a type-safe pipe syntax and deterministic lifetime management that integrates cleanly with modern C++ lambda expressions. This reduces boilerplate compared to Qt's `QObject`-based connect/disconnect patterns and enables more expressive asynchronous data flow.