Реактивное программирование в Rust: библиотеки, примеры и практика

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Реактивное программирование в Rust: от наблюдаемых объектов до асинхронных потоков

Реактивное программирование — это парадигма, ориентированная на обработку асинхронных потоков данных и распространение изменений. В экосистеме Rust она реализуется через комбинацию мощной системы типов, безопасной работы с памятью и современных асинхронных примитивов. В этой статье мы рассмотрим ключевые библиотеки, паттерны и практические примеры реактивного программирования на Rust.

Основные библиотеки для реактивного программирования в Rust

В Rust нет единого «стандарта» реактивного программирования, но существует несколько зрелых библиотек, покрывающих разные уровни абстракции:

  • RxRust — порт популярной ReactiveX (Rx) для Rust. Предоставляет Observable, Observer, Scheduler и широкий набор операторов (map, filter, flat_map, merge и др.).
  • futures-rs — фундаментальная библиотека для работы с асинхронными вычислениями. Stream trait — основа реактивных потоков.
  • async-std и tokio — асинхронные рантаймы, предоставляющие каналы (channels), таймеры и другие примитивы для реактивных цепочек.
  • frunk — библиотека для функционального программирования, включающая реактивные паттерны вроде Lens и State.

Пример 1: Простой Observable с RxRust

RxRust реализует паттерн «наблюдатель»: Observable генерирует события, Observer подписывается на них.

// Cargo.toml: rxrust = "1.0"
use rxrust::prelude::*;

fn main() {
    let observable = observable::from_iter(vec![1, 2, 3, 4, 5]);

    observable
        .filter(|x| x % 2 == 0)
        .map(|x| x * 10)
        .subscribe(|x| println!("Получено: {}", x));
}

В этом примере создаётся поток из чисел 1..5, фильтруются чётные, умножаются на 10 и выводятся. Цепочка операторов выполняется лениво — только после вызова subscribe.

Пример 2: Асинхронные потоки с futures-rs

Библиотека futures-rs предоставляет трейт Stream, который является асинхронным аналогом Iterator. Комбинаторы из StreamExt позволяют строить реактивные цепочки.

// Cargo.toml: futures = "0.3"
use futures::stream::{self, StreamExt};

#[tokio::main]
async fn main() {
    let stream = stream::iter(0..10)
        .filter(|x| futures::future::ready(x % 2 == 0))
        .map(|x| x * 10)
        .take(3);

    let result: Vec<i32> = stream.collect().await;
    println!("Результат: {:?}", result); // [0, 20, 40]
}

Здесь filter принимает асинхронную функцию, что позволяет внедрять вызовы к внешним API или базам данных внутри цепочки. take(3) ограничивает количество элементов — это пример «противодавления» (backpressure).

Пример 3: Реактивные каналы с async-std

Каналы — классический способ реализации реактивных взаимодействий между компонентами. async-std предоставляет многопроизводительные, многопотребительские каналы.

// Cargo.toml: async-std = "1.12"
use async_std::channel;
use async_std::task;

#[async_std::main]
async fn main() {
    let (sender, receiver) = channel::unbounded();

    // Производитель
    task::spawn(async move {
        for i in 0..5 {
            sender.send(i).await.unwrap();
            task::sleep(std::time::Duration::from_millis(100)).await;
        }
    });

    // Потребитель (реактивный)
    while let Some(msg) = receiver.recv().await {
        println!("Получено: {}", msg);
        if msg == 4 {
            break;
        }
    }
}

Каналы идеально подходят для реализации Event Bus, шины событий или простых реактивных систем с несколькими источниками данных.

Продвинутые паттерны: комбинирование потоков

Реактивное программирование раскрывается при комбинировании нескольких источников данных. Рассмотрим слияние двух потоков с помощью futures::stream::select:

use futures::stream::{self, StreamExt};

async fn merge_example() {
    let stream1 = stream::iter(vec![1, 2, 3]);
    let stream2 = stream::iter(vec![10, 20, 30]);

    let merged = stream::select(stream1, stream2);
    let result: Vec<i32> = merged.collect                    

Рекомендаций