
Привет, Хабр!
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] (полином ) и вычисляем значение с помощью
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.
ссылка на оригинал статьи https://habr.com/ru/articles/892920/
Добавить комментарий