Но прежде, чем выписывать общую операцию, немного подумаем, как именно выразить запись очередного значения. Именно, в нашем type-level Fizzbuzz списке лежат элементы двух разных семейств: числа Пеано и Fizz/Buzz/FizzBuzz. Они будут обрабатываться по разному: для чисел Пеано мы записываем их численное представление, а для *zz-типов — фиксированную строку. Вроде всё норм, ничто не мешает нам написать blanket impl для чисел Пеано и по impl-у на каждый из *zz-типов, но... нам же ведь ещё нужно рассчитать размер итогового массива, верно? А там нам нужны будут размеры записываемых строк... Какое-то неприятное дублирование вылезает, а, как известно, в надёжных системах есть только один источник истины.
Что ж, объединим формы представления в единый тип и будем в дальнейшем оперировать его значением:
(сказанное выше насчёт единого источника истины не относится к числам Пеано, поскольку за счёт
Что ж, теперь мы можем написать трейт для представления в виде байтов — и так как мы не хотим затачиваться на конкретный размер, нам нужно обобщить его по размеру итогового буфера:
За счёт реализаций
Что ж, объединим формы представления в единый тип и будем в дальнейшем оперировать его значением:
enum Repr {
Direct(&'static str),
Numeric(usize),
}
Ну и сделаем трейт для представления элементов списка в этом виде:trait AsRepr {
const REPR: Repr;
}
Напишем реализации. Для чисел Пеано они будут опираться на NumericValue, ну а для *zz-типов это просто строки:impl<T: NumericValue> AsRepr for T {
const REPR: Repr = Repr::Numeric(T::VALUE);
}
impl AsRepr for Fizz {
const REPR: Repr = Repr::Direct("fizz");
}
// то же самое для `Buzz` и `FizzBuzz` с нужными литераламиЗаметим, что
Repr даёт нам не только информацию о том, что нужно писать, но и информацию о том, сколько места для этого потребуется. И строки не дублируются. Успех!(сказанное выше насчёт единого источника истины не относится к числам Пеано, поскольку за счёт
NumericValue их числовое значение является их неотъемлемым свойством, а количества места для записи является производным от VALUE значением через n_digits)Что ж, теперь мы можем написать трейт для представления в виде байтов — и так как мы не хотим затачиваться на конкретный размер, нам нужно обобщить его по размеру итогового буфера:
trait WriteBuf<const N: usize> {
const BUF: ([u8; N], usize);
}
База индукции — нетронутый массив:impl<const N: usize> WriteBuf<{ N }> for Nil {
const BUF: ([u8; N], usize) = ([0; N], N);
}
(кстати, конкретное значение начального массива несущественно — всё равно оно потом перезаписывается целиком)За счёт реализаций
AsRepr реализация шага индукции очень проста и из всех нетривиальных операций требует лишь match по Repr:impl<Head, Tail, const N: usize> WriteBuf<{ N }> for Cons<Head, Tail>
where
Head: AsRepr,
Tail: WriteBuf<{ N }>,
{
const BUF: ([u8; N], usize) = {
let (bytes, at) = Tail::BUF;
match Head::REPR {
Repr::Direct(s) =>
write_str_with_delimiter_at(s, bytes, at),
Repr::Numeric(n) =>
write_num_with_delimiter_at(n, bytes, at),
}
};
}
Прежде, чем воспользоваться полученным инструментом, нам нужно рассчитать требуемое место. Делегируем эту задачу ещё одному трейту:trait WriteLen {
const LEN: usize;
}
База индукции — Nil — ничего не пишет:impl WriteLen for Nil {
const LEN: usize = 0;
}
Для записи Cons нужно столько же, сколько под хвост, плюс под значение в голове, плюс разделитель:impl<Head, Tail> WriteLen for Cons<Head, Tail>
where
Head: AsRepr,
Tail: WriteLen,
{
const LEN: usize = Tail::LEN
+ DELIMITER.len()
+ match Head::REPR {
Repr::Direct(s) => s.len(),
Repr::Numeric(n) => n_digits(n),
};
}
Осталось только объединить эти две вещи — и можно прям сразу в константу:const FIZZ_BUZZ_BYTES: &[u8] = &{
let (precalculated, at) = {
const LIST_WRITE_LEN: usize = <List as WriteLen>::LEN;
<List as WriteBuf<LIST_WRITE_LEN>>::BUF
};
// проверим, что буфер перезаписан целиком
assert!(at == 0);
precalculated
};
Вывод результата элементарен — нам даже не нужно переводить в строку, потому что используемое API потребляет байты напрямую:use std::io::Write;
std::io::stdout().write_all(FIZZ_BUZZ_BYTES).unwrap();
Вот и всё. Если теперь открыть ассемблерный код, то среди меток будет строковая константа, которая содержит в себе весь вывод программы.К изложенному выше можно присовокупить кое-что ещё. Внимательный читатель мог заметить, что, по большому счёту, весь итоговый результат требует подачи на вход лишь одного type-level числа, и его одного, в принципе, достаточно, чтобы получить и тип результата, и его значение. А значит, должно быть возможным написать const fn, принимающую на вход типовый параметр и возвращающую итоговый массив.
Внимательный читатель прав... Принципиально. К сожалению, на текущий момент const generics в Rust весьма ограничены. Искомая функция требует написать возвращаемый тип, зависящий от константы, подсчитанной от переданного типа, что в настоящий момент невозможно на stable (это можно обойти при помощи generic-array, но давайте
#![feature(generic_const_exprs)]
#![allow(incomplete_features))] // да, я в курсе, что она не готова
const fn to_buf<N>() -> ([u8; FizzBuzzList::<N>::LEN], usize)
where
N: RangeDownFrom,
MakeRangeDownFrom<N>: ReverseWith<Nil>,
RangeTo<N>: EnumerateFromWithCycle<Z, Three>,
EnumerateFromZeroWithCycle3<RangeTo<N>>: EnumerateFromWithCycle<Z, Five>,
FizzBuzzEnumerate<RangeTo<N>>: TailOf,
Tail<FizzBuzzEnumerate<RangeTo<N>>>: ToFizzBuzz,
FizzBuzzList<N>: WriteLen + WriteBuf<{ FizzBuzzList::<N>::LEN }>,
{
<FizzBuzzList::<N> as WriteBuf<{ FizzBuzzList::<N>::LEN }>>::BUF
}
Эту функцию я написал без ошибок с первого раза путём вдумчивого написания кода... Нет, конечно. Я поступил так, как поступил бы любой нормальный Rust-программист: написал функцию с телом и заголовком, а потом добавлял where clauses, о которых услужливо сообщал компилятор. Итоговый результат выглядит устрашающе, но, поверьте, без типовых алиасов это выглядело ещё хуже.К сожалению, для того, чтобы эта функция компилировалась, нам нужно подправить определение
Three и Five — без них компилятор не поймёт, что EnumerateFromWithCycle<Z, Three> и EnumerateFromWithCycle<Z, S<S<S<Z>>>> — это одно и то же:type Three = S<S<S<Z>>>;
type Five = S<S<S<S<S<Z>>>>>;
Обратите внимание, числа заданы напрямую, а не через операцию суммирования. Это и есть тот самый баг, который я упоминал.Что ж, весь код, как всегда, в гисте. Обратите внимание, в этот раз, помимо кода, там есть ещё и Cargo.toml. В него включены две фичи, которые можно легко менять через
cargo run/cargo build. Фича use_nightly меняет определение FIZZ_BUZZ_BYTES на использующую ночную фичу функцию to_buf выше. Фича compare_with_previous_impl активирует предыдущую реализацию compile-time Fizzbuzz, а также ассерт, который проверяет, что результаты, полученные разными методами, одинаковы. Фичи можно активировать независимо друг от друга, но use_nightly, разумеется, требует +nigthly.GitHub
Tracking Issue for complex generic constants: `feature(generic_const_exprs)` · Issue #76560 · rust-lang/rust
This is a tracking issue for complex generic expressions in constants which is still highly experimental. The feature gate for the issue is #![feature(generic_const_exprs)]. On stable all const gen...
❤4
Блог*
use std::io::Write; std::io::stdout().write_all(FIZZ_BUZZ_BYTES).unwrap(); Вот и всё. Если теперь открыть ассемблерный код, то среди меток будет строковая константа, которая содержит в себе весь вывод программы. К изложенному выше можно присовокупить кое…
Кусочек ассемблерного листинга, подтверждающий сказанное выше
#prog #rust #abnormalprogramming
Тем временем nnethercote занят по настоящему важными вещами.
(а вот и сами шахматы)
Тем временем nnethercote занят по настоящему важными вещами.
(а вот и сами шахматы)
🔥9🤯4❤1
#prog #rust #article
How to speed up the Rust compiler in August 2023
Очередная пачка изменений для ускорения компилятора
How to speed up the Rust compiler in August 2023
Очередная пачка изменений для ускорения компилятора
Nicholas Nethercote
How to speed up the Rust compiler in August 2023
It has been five months since my last general update on the Rust compiler’s performance. Let’s see what has happened in that time.
#prog #rust #article
Learning Async Rust With Entirely Too Many Web Servers
Статья про развитие подобия web-сервера от наиболее простого однопоточного до полноценного асинхронного (в процессе — с переизобретением абстракций для асинхронности)
Learning Async Rust With Entirely Too Many Web Servers
Статья про развитие подобия web-сервера от наиболее простого однопоточного до полноценного асинхронного (в процессе — с переизобретением абстракций для асинхронности)
Ibraheem Ahmed
Learning Async Rust With Entirely Too Many Web Servers
Async Rust is a whole new world of programming. Let's try to understand it by working our way through a series of web servers, learning the details of concurrency and I/O along the way.
👍7