
Привет, Хабр!
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.