Как стать автором
Обновить
93.78
Beget
+7 (800) 700-06-08

const fn может делать намного больше

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров95

Привет, Хабр!

const fn в Rust давно перестал быть просто инструментом для сложения чисел на этапе компиляции. Сегодня это мощный инструмент, который умеет циклы, условия, матчинг, парсинг и даже кусочки бизнес-логики — и всё это ещё до запуска программы.

Факториал

Начнём с простого, но показательного примера — вычисления факториала. Того самого n!, который мы все писали на первом курсе рекурсией. Но здесь важно не само число, а то когда оно считается. Благодаря const fn можно посчитать факториал ещё до запуска программы — и не тратить на это ни байта времени в runtime.

Взглянем на код:

// Классический пример вычисления факториала — но на этапе компиляции!
const fn factorial(n: u64) -> u64 {
    let mut result = 1;
    let mut i = 1;
    while i <= n {
        result *= i;
        i += 1;
    }
    result
}

// Вычисляем значение при компиляции — никакой нагрузки в runtime
const FACT_5: u64 = factorial(5);

fn main() {
    println!("5! = {}", FACT_5); // Вывод: 120
}

Функция factorial объявлена как const fn, значит, её можно вызывать в контексте, где значения вычисляются на этапе компиляции — например, при инициализации const-переменной. В данном случае factorial(5) превращается в обычное число 120 уже во время сборки проекта, и в итоговом бинарнике будет просто зашит литерал 120.

Если бы мы делали то же самое без const fn, у нас был бы выбор между:

  • использовать литерал (не всегда возможно),

  • вычислять в main() (и тратить ресурсы),

  • или, что ещё хуже, тащить это в lazy_static! или once_cell.

А так — всё просто, чисто и заранее готово.

Вычисление полинома

Теперь перейдём к чему-то более практичному. Допустим, есть математическая модель или таблица предрасчитанных значений, где полином с фиксированными коэффициентами используется для расчётов. Вместо того чтобы каждый раз на runtime перебирать коэффициенты и выполнять арифметику, можно вынести всё это вычисление в стадию компиляции.

// Вычисление полинома: a0 + a1*x + a2*x^2 + ... + an*x^n
const fn eval_poly(coeffs: &[i32], x: i32) -> i32 {
    let mut result = 0;
    let mut pow = 1;
    let mut i = 0;
    while i < coeffs.len() {
        result += coeffs[i] * pow;
        pow *= x;
        i += 1;
    }
    result
}

const COEFFS: &[i32] = &[3, 2, 5]; // Представим полином: 3 + 2*x + 5*x^2
const VALUE: i32 = eval_poly(COEFFS, 2); // Вычисление: 3 + 2*2 + 5*2^2 = 3 + 4 + 20 = 27

fn main() {
    println!("Значение полинома: {}", VALUE);
}

Функция eval_poly объявлена как const fn, что позволяет вычислить результат во время компиляции. Она принимает срез коэффициентов и значение переменной x, что особенно удобно, когда набор коэффициентов известен заранее. Внутри функции мы начинаем с result = 0 и pow = 1 (где pow представляет текущую степень x), затем в цикле while для каждого коэффициента прибавляем произведение коэффициента на pow, обновляем pow, умножая его на x, и увеличиваем счётчик.

После завершения цикла функция возвращает сумму всех слагаемых полинома. В примере определяем константу COEFFS как [3, 2, 5] (полином 3+2x+5x²) и вычисляем значение с помощью const VALUE: i32 = eval_poly(COEFFS, 2), что даёт 27. В функции main выводится готовый результат, поскольку все вычисления уже выполнены на этапе компиляции, избавляя runtime от лишних операций.

Парсинг строк

Что делать с неизменными конфигурационными данными, представленными в виде CSV, JSON или INI? Вместо того чтобы парсить их каждый раз во время работы программы, можно сделать это один раз — прямо на этапе компиляции.

CSV-парсер

Представим ситуацию: есть строка с данными, разделёнными запятыми, например Rust,Const,Cats. Вместо того чтобы парсить её в runtime, можно заранее разбить её на элементы, используя const fn.

// Простой CSV-парсер, разбивающий строку на 3 элемента
const fn parse_csv(input: &str) -> [&str; 3] {
    let bytes = input.as_bytes();
    let mut result: [&str; 3] = [""; 3];
    let mut start = 0;
    let mut idx = 0;
    let mut i = 0;
    while i <= bytes.len() {
        if i == bytes.len() || bytes[i] == b',' {
            // Unsafe тут используется потому, что мы знаем, что наш CSV – валидный UTF-8.
            result[idx] = unsafe { std::str::from_utf8_unchecked(&bytes[start..i]) };
            idx += 1;
            start = i + 1;
        }
        i += 1;
    }
    result
}

const CSV_DATA: &str = "Rust,Const,Cats";
const PARSED_CSV: [&str; 3] = parse_csv(CSV_DATA);

fn main() {
    for field in PARSED_CSV.iter() {
        println!("Поле: {}", field);
    }
}

Сначала вызываем input.as_bytes(), чтобы получить массив байтов для прямой проверки символов (запятая представлена как b','), затем создаём массив result из трёх пустых строк, куда будем складывать полученные подстроки; далее, используя цикл while, проходим по байтам, и если текущий индекс равен длине массива или встречается запятая, считаем отрезок с start до i отдельным полем, преобразуя его в строку через unsafe { std::str::from_utf8_unchecked(...) } (т.к уверены в корректности UTF-8, что важно для const-контекста), и после каждого разделителя обновляем индекс начала нового поля (start = i + 1) и увеличиваем счётчик idx.

В итоге, всё вычисление происходит во время компиляции, и во время выполнения программы остаётся лишь вывод уже готовых строк.

Конфигурация

Перейдём к другому кейсу — инициализации неизменяемых конфигурационных данных. Допустим, есть фиксированные настройки, например, для подключения к серверу. Вместо того чтобы задавать их где-то в runtime, можно определить структуру конфигурации, инициализировать её с помощью const fn и быть уверенным, что эти данные не изменятся на протяжении всего жизненного цикла приложения.

#[derive(Debug)]
struct Config {
    host: &'static str,
    port: u16,
    use_ssl: bool,
}

const fn create_config() -> Config {
    Config {
        host: "localhost",
        port: 8080,
        use_ssl: false,
    }
}

const CONFIG: Config = create_config();

fn main() {
    println!("Конфигурация: {:?}", CONFIG);
}

Сначала создаём структуру Config, которая включает основные настройки (адрес хоста, порт и флаг использования SSL), и благодаря #[derive(Debug)] легко можем выводить её содержимое; затем функция create_config, объявленная как const fn, позволяет компилятору выполнить инициализацию до запуска программы, гарантируя, что объект Config полностью определён на этапе компиляции, а готовая конфигурация инициализируется через вызов create_config(), после чего в main просто выводится полученная настройка.

Мини-интерпретатор

А теперь заставим компилятор Rust вычислять арифметические выражения из строки ещё до запуска программы:

// Мини-интерпретатор для выражений с + и -
// Ограничения: только цифры и знаки +/-, без пробелов, без скобок
const fn eval_expr(expr: &str) -> i32 {
    let bytes = expr.as_bytes();
    let mut i = 0;
    let mut current_number = 0;
    let mut result = 0;
    let mut current_sign = 1;
    while i < bytes.len() {
        let c = bytes[i];
        if c >= b'0' && c <= b'9' {
            current_number = current_number * 10 + (c - b'0') as i32;
        } else {
            result += current_sign * current_number;
            current_number = 0;
            current_sign = if c == b'+' { 1 } else { -1 };
        }
        i += 1;
    }
    result + current_sign * current_number
}

const EXPR: &str = "12+34-5+6";
const EVAL_RESULT: i32 = eval_expr(EXPR);

fn main() {
    println!("Результат выражения {}: {}", EXPR, EVAL_RESULT);
}

Сначала строка преобразуется в массив байтов с помощью as_bytes(), что позволяет обрабатывать каждый символ вручную (а в const fn нет готовых методов типа .split() или регулярных выражений). При итерации по байтам, если встречается цифра (от b'0' до b'9'), накапливаем число в переменной current_number, умножая предыдущее значение на 10 и добавляя новую цифру.

Как только встречается оператор (+ или -), текущее число закрывается: мы прибавляем его к результату result с учётом текущего знака, сбрасываем current_number и переключаем знак для следующего блока. После завершения цикла, если последнее число осталось необработанным, оно добавляется к result с учётом текущего знака, что и гарантирует корректный итоговый результат.

Компилятор по факту превращается в арифметический движок. Можно зашивать готовые результаты в бинарь, а в runtime просто выводить или использовать их. Без парсинга, без eval, без костылей.

Условные конструкции и циклы в const контексте

Можно использовать условные конструкции прямо в const контексте.

#[derive(Debug)]
struct SimpleConfig {
    host: &'static str,
    port: u16,
}

// Предполагаем, что JSON имеет фиксированный формат.
const fn parse_simple_json(json: &str) -> SimpleConfig {
    // Жестко заданные индексы – для примера
    let host_start = 10;
    let host_end = 19;
    let port_start = 30;
    let port_end = 34;

    let host = unsafe { std::str::from_utf8_unchecked(json.as_bytes().get(host_start..host_end).unwrap()) };

    let mut port = 0u16;
    let mut i = port_start;
    while i < port_end {
        let digit = (json.as_bytes()[i] - b'0') as u16;
        port = port * 10 + digit;
        i += 1;
    }

    SimpleConfig { host, port }
}

const JSON_STR: &str = r#"{"host": "localhost", "port": 8080}"#;
const CONFIG_JSON: SimpleConfig = parse_simple_json(JSON_STR);

fn main() {
    println!("Парсинг JSON: {:?}", CONFIG_JSON);
}

Компилятор сам решает, какой путь выбрать, и выполняет нужные вычисления заранее.

Парсинг сложных форматов

Возьмем, к примеру, JSON-конфигурацию:

{"host": "localhost", "port": 8080}

Ниже пример простейшего парсера, который извлекает значения из такой строки. Конечно, в реальных условиях всё будет куда сложнее, но идея ясна.

#[derive(Debug)]
struct SimpleConfig {
    host: &'static str,
    port: u16,
}

// Предполагаем, что JSON имеет фиксированный формат.
const fn parse_simple_json(json: &str) -> SimpleConfig {
    // Жестко заданные индексы – для примера
    let host_start = 10;
    let host_end = 19;
    let port_start = 30;
    let port_end = 34;

    let host = unsafe { std::str::from_utf8_unchecked(json.as_bytes().get(host_start..host_end).unwrap()) };

    let mut port = 0u16;
    let mut i = port_start;
    while i < port_end {
        let digit = (json.as_bytes()[i] - b'0') as u16;
        port = port * 10 + digit;
        i += 1;
    }

    SimpleConfig { host, port }
}

const JSON_STR: &str = r#"{"host": "localhost", "port": 8080}"#;
const CONFIG_JSON: SimpleConfig = parse_simple_json(JSON_STR);

fn main() {
    println!("Парсинг JSON: {:?}", CONFIG_JSON);
}

Аналогично, рассмотрим пример парсинга INI-конфигурации:

#[derive(Debug)]
struct IniConfig {
    host: &'static str,
    port: u16,
}

const fn parse_ini(input: &str) -> IniConfig {
    // Формат фиксирован:
    // [server]
    // host=localhost
    // port=8080
    let host_start = 20;
    let host_end = 29;
    let port_start = 36;
    let port_end = 40;

    let host = unsafe { std::str::from_utf8_unchecked(input.as_bytes().get(host_start..host_end).unwrap()) };

    let mut port = 0u16;
    let mut i = port_start;
    while i < port_end {
        let digit = (input.as_bytes()[i] - b'0') as u16;
        port = port * 10 + digit;
        i += 1;
    }

    IniConfig { host, port }
}

const INI_STR: &str = "[server]\nhost=localhost\nport=8080";
const CONFIG_INI: IniConfig = parse_ini(INI_STR);

fn main() {
    println!("Парсинг INI: {:?}", CONFIG_INI);
}

Спасибо, что дочитали до конца! Если есть чем еще поделиться, пишите комментарии и делитесь своими кейсами с const fn.

Теги:
Хабы:
+1
Комментарии1

Публикации

Информация

Сайт
beget.com
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия

Истории