#моё #prog #rust
В крейте time есть структура Date с методом format, который возвращает
Как примерно должен выглядеть код? Нам нужно абстрагироваться от конкретных типов по операции "отформатируй сюда дату", чтобы из элементарных форматировщиков можно было собрать составной. В самой операции нам, очевидно, требуются: сам форматировщик, дата и буфер, куда будут записываться данные. Мы не хотим возвращать строку непосредственно, потому что это при объединении привело бы ко множеству ненужных мелких аллокаций. Также мы не хотим, чтобы форматировщики могли удалять из буфера данные, поэтому мы сделаем обёртку над строкой с более узким интерфейсом:
Какие могут быть форматировщики? Очевидно, вы их число должна входить строка, чтобы можно было вставлять в формат разделители между компонентами даты. Напишем реализацию трейта:
В крейте time есть структура Date с методом format, который возвращает
String
со датой, отформатированной согласно переданному формату. Выглядит это примерно так (пример из документации):assert_eq!(date!(2019-01-02).format("%Y-%m-%d"), "2019-01-02");Выглядит неплохо, но будем честны: в подавляющем большинстве случаев строка формата так и остаётся литералом. Метод же, тем не менее, вынужден парсить строку при каждом вызове, и лично я сомневаюсь, что этот код будет специализирован на этапе компиляции (всё-таки rustc не является суперкомпилятором). Многократная компиляция регулярных выражений является известным антипаттерном, и для решения этой проблемы есть инструмент, а для формата даты такого инструмента нет. Сегодня мы напишем подобный инструмент сами.
Как примерно должен выглядеть код? Нам нужно абстрагироваться от конкретных типов по операции "отформатируй сюда дату", чтобы из элементарных форматировщиков можно было собрать составной. В самой операции нам, очевидно, требуются: сам форматировщик, дата и буфер, куда будут записываться данные. Мы не хотим возвращать строку непосредственно, потому что это при объединении привело бы ко множеству ненужных мелких аллокаций. Также мы не хотим, чтобы форматировщики могли удалять из буфера данные, поэтому мы сделаем обёртку над строкой с более узким интерфейсом:
use std::fmt;Сам общий интерфейс форматировщиков:
#[derive(Default)]
pub struct Buf {
inner: String,
}
impl Buf {
pub fn new() -> Self {
Self::default()
}
pub fn append(&mut self, s: &str) {
self.inner += s;
}
// Этот метод позволит нам использовать макрос `write!` на `Buf`.
// Он позволяет не безусловно выделять новую строку,
// а использовать место в уже имеющейся
pub fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result {
self.inner.write_fmt(args)
}
}
use std::fmt;(проблем с тем, чтобы принимать
use time::Date;
pub trait FormatDate {
fn format_date(&self, b: &mut Buf, d: Date) -> fmt::Result;
}
Date
по значению, нет, потому что это Copy
-тип).Какие могут быть форматировщики? Очевидно, вы их число должна входить строка, чтобы можно было вставлять в формат разделители между компонентами даты. Напишем реализацию трейта:
impl FormatDate for &'_ str {Теперь напишем реализацию для, скажем, дня даты:
fn format_date(&self, b: &mut Buf, _: Date) -> fmt::Result {
b.append(self);
Ok(())
}
}
pub struct DayOf;Формат
impl FormatDate for DayOf {
fn format_date(&self, b: &mut Buf, d: Date) -> fmt::Result {
write!(b, "{:02}", d.day())
}
}
"{:02}"
означает, что для печати дня отведено два места, и если в номере дня всего одна цифра, то вывод будет дополнен слева нулями. Форматировщики для номера месяца и года пишутся аналогично, поэтому не будем подробнее на этом останавливаться.docs.rs
time::Date - Rust
API documentation for the Rust `Date` struct in crate `time`.
Как же теперь объединить всё это? Написать реализацию для кортежей форматировщиков, разумеется! Так как вариадических обобщённых типов в Rust нет, а руками писать реализации мне лень, напишем макрос:
macro_rules! impl_for_tuples {Добавим для удобства extension trait:
() => {};
($head:ident $(, $rest:ident)*) => {
impl<$head, $($rest),*> FormatDate for ($head, $($rest,)*)
where
$head: FormatDate,
$($rest: FormatDate,)*
{
fn format_date(&self, b: &mut Buf, date: Date) -> fmt::Result {
#[allow(non_snake_case)]
let &(ref $head, $(ref $rest,)*) = self;
$head.format_date(b, date)?;
$($rest.format_date(b, date)?;)*
Ok(())
}
}
impl_for_tuples!($($rest),*);
};
}
impl_for_tuples!(A, B, C, D, E, F, G, H);
pub trait DateExt {И проверим, как работает:
fn format_into<F: FormatDate>(self, b: &mut Buf, f: F) -> fmt::Result;
fn format_as<F: FormatDate>(self, f: F) -> String;
}
impl DateExt for Date {
fn format_into<F: FormatDate>(self, b: &mut Buf, f: F) -> fmt::Result {
f.format_date(b, self)
}
fn format_as<F: FormatDate>(self, f: F) -> String {
let mut buf = Buf::default();
let _ = self.format_into(&mut buf, f);
buf.inner
}
}
use time::date;Работает!
// Очень удобный макрос
let d = date!(2020-02-18);
let format = (DayOf, "/", MonthOf, "/", YearOf);
// Обратите внимание на ноль впереди в месяце
assert_eq!(d.format_as(format), "18/02/2020");
Блог*
Как же теперь объединить всё это? Написать реализацию для кортежей форматировщиков, разумеется! Так как вариадических обобщённых типов в Rust нет, а руками писать реализации мне лень, напишем макрос: macro_rules! impl_for_tuples { () => {}; ($head:ident…
#моё #prog #rust
Так можем ли мы сделать лучше? Определённо, ведь у нашего решения есть ряд недостатков:
1. Захардкоженные символы для формирования отступов. Не всегда в начале требуются нули.
2. Захардкоженные ширины отступов. Мы не можем легко поменять их.
3. Потенциальная просадка по производительности. В документации к методу Date::month_day сказано, что использовать его эффективнее, чем доставать месяц и день по отдельности. Наш код этого не учитывает.
Третий пункт решается относительно просто: мы делаем у трейта параметр, который и является данными, из которых изымаются компоненты:
Для того, чтобы сделать форматировщик более настраиваемым, нам нужно разделить эту схему форматирования на отдельные части. Этими частями являются:
1. Функция, возвращающая "часть" даты, которая реализует
2. Ширина поля под отформатированные данные;
3. Символ для заполнения незанятой части поля.
Последнее, к сожалению, нельзя передать в стандартные форматирующие макросы параметрами, поэтому абстрагироваться от этого при помощи трейта не получится — чего не скажешь об остальных составляющих.
Выделение нужной части:
Так можем ли мы сделать лучше? Определённо, ведь у нашего решения есть ряд недостатков:
1. Захардкоженные символы для формирования отступов. Не всегда в начале требуются нули.
2. Захардкоженные ширины отступов. Мы не можем легко поменять их.
3. Потенциальная просадка по производительности. В документации к методу Date::month_day сказано, что использовать его эффективнее, чем доставать месяц и день по отдельности. Наш код этого не учитывает.
Третий пункт решается относительно просто: мы делаем у трейта параметр, который и является данными, из которых изымаются компоненты:
pub trait FormatDate<D> {Для удобства сделаем алиас на возвращаемый тип Date::as_ymd:
fn format_date(&self, b: &mut Buf, d: D) -> fmt::Result;
}
pub type Ymd = (i32, u8, u8);
. Если тип умеет форматировать дату в деконструированном виде (Ymd
), то он может форматировать и исходную дату:impl<T: FormatDate<Ymd>> FormatDate<Date> for T {С первыми двумя недочётами разобраться несколько сложнее. Посмотрим на то, как, по идее, выглядит реализация
fn format_date(&self, b: &mut Buf, date: Date) -> fmt::Result {
self.format_date(b, date.as_ymd())
}
}
FormatDate<Ymd>
:impl FormatDate<Ymd> for SomeFormatter {(справка по синтаксису форматных строк)
fn format_date(&self, b: &mut Buf, ymd: Ymd) -> fmt::Result {
let part = some_part_of_date(ymd);
write!(b, "{:0width$}", part, width = SOME_WIDTH)
}
}
Для того, чтобы сделать форматировщик более настраиваемым, нам нужно разделить эту схему форматирования на отдельные части. Этими частями являются:
1. Функция, возвращающая "часть" даты, которая реализует
Display
;2. Ширина поля под отформатированные данные;
3. Символ для заполнения незанятой части поля.
Последнее, к сожалению, нельзя передать в стандартные форматирующие макросы параметрами, поэтому абстрагироваться от этого при помощи трейта не получится — чего не скажешь об остальных составляющих.
Выделение нужной части:
pub trait Extract<Date = Ymd> {Пример реализации:
type Output: fmt::Display;
fn extract(ymd: Date) -> Self::Output;
}
pub struct DayOf;Немного более сложный пример:
impl Extract for DayOf {
type Output = u8;
fn extract((_year, _month, day): Ymd) -> u8 {
day
}
}
pub struct MonthFullWordOf;Ширина поля, по хорошему, должна быть константой, но параметризовывать типы значениями в Rust на stable пока нельзя. Не то, чтобы меня это остановило, но в данном случае проблемы решается достаточно просто и без const generics:
impl Extract for MonthFullWordOf {
type Output = &'static str;
fn extract((_year, month, _day): Ymd) -> &'static str {
match month {
1 => "January",
2 => "February",
3 => "March",
4 => "April",
5 => "May",
6 => "Juny",
7 => "July",
8 => "August",
9 => "September",
10 => "October",
11 => "November",
12 => "December",
// Здесь нормально паниковать, потому что месяц другие номера иметь не может.
// В типах это, к сожалению, не выражено.
_ => unreachable!(),
}
}
}
pub trait Width {Наш составной форматировщик в итоге выглядит следующим образом:
const WIDTH: usize;
}
// Пример реализации
pub struct W2;
impl Width for W2 {
const WIDTH: usize = 2;
}
pub struct Formatter<Extractor, Width, Padding = NoPad>(
std::marker::PhantomData<(Extractor, Width, Padding)>,
);
docs.rs
time::Date - Rust
API documentation for the Rust `Date` struct in crate `time`.
Так как абстрагироваться от символов заполнения толком нельзя, мы просто сделаем несколько несколько маркерных типов и напишем конкретные реализации
FormatDate
для Formatter
, параметризованных различными заполнениями:pub struct PadZeros;Итак, мы приобрели в модульности, но что мы потеряли? Удобство использования! Каждый конкретный форматировщик теперь содержит поле, поэтому проинициализировать его просто по имени уже не получится. К счастью, это обходится достаточно просто: достаточно завести константы с нужными именами:
pub struct NoPad;
impl<Extractor, W> FormatDate<Ymd> for Formatter<Extractor, W, PadZeros>
where
Extractor: Extract,
W: Width,
{
fn format_date(&self, b: &mut Buf, ymd: Ymd) -> fmt::Result {
let part = Extractor::extract(ymd);
write!(b, "{:01$}", part, W::WIDTH)
}
}
impl<Extractor, W> FormatDate<Ymd> for Formatter<Extractor, W, NoPad>
where
Extractor: Extract,
W: Width,
{
fn format_date(&self, b: &mut Buf, ymd: Ymd) -> fmt::Result {
let part = Extractor::extract(ymd);
write!(b, "{:1$}", part, W::WIDTH)
}
}
pub type Day = Formatter<DayOf, W2, PadZeros>;Проверим:
pub const DAY: Day = Formatter(std::marker::PhantomData);
// Аналогично с остальными форматировщиками
let d = date!(2020-02-18);Работает!
let format = (DAY, "/", MONTH, "/", YEAR);
assert_eq!(d.format_as(format), "18/02/2020");
Есть ещё один аспект, который мы можем улучшить: конструирование форматировщика из нескольких. Сейчас это сделано при помощи крайне уродливого и не расширяемого решения: реализации трейта для кортежей. Кортежи всей длины мы охватить не можем, а каждый новый impl добавляет времени к компиляции. Сейчас мы сделаем индуктивное решение: вместо того, чтобы пытаться объять необъятное, мы сделаем cons-список форматировщиков:
pub struct Nil;Разумеется, составлять подобный список руками куда менее удобно, чем кортеж — но мы и не будем! Вместо этого мы сделаем макрос, который будет конструировать список за нас. Мы несколько ужесточим требования к формату — теперь вместо произвольных выражений можно использовать лишь имена и литералы — но это упростит видимый синтаксис, потому что позволяет избавиться от запятых. Сам макрос:
pub struct Cons<T, U>(pub T, pub U);
impl FormatDate<Ymd> for Nil {
fn format_date(&self, _: &mut Buf, _: Ymd) -> fmt::Result {
Ok(())
}
}
impl<T, U> FormatDate<Ymd> for Cons<T, U>
where
T: FormatDate<Ymd>,
U: FormatDate<Ymd>,
{
fn format_date(&self, b: &mut Buf, ymd: Ymd) -> fmt::Result {
self.0.format_date(b, ymd)?;
self.1.format_date(b, ymd)?;
Ok(())
}
}
macro_rules! date_format {Как видите, ничего сложного тут нет 😈. Проверим, как это ведёт себя:
() => { Nil };
($name:ident $($tt:tt)*) => { Cons($name, date_format!($($tt)*)) };
($lit:literal $($tt:tt)*) => { Cons($lit, date_format!($($tt)*)) };
}
let d = date!(2020-02-18);К сожалению, писать имена вплотную после литералов нельзя, потому что это синтаксическая ошибка. В остальном — оно работает!
let format = date_format!(DAY" " MONTH_FULL", " YEAR);
assert_eq!(d.format_as(format), "18 February, 2020");
Можем ли мы теперь объявить торжество zero-cost абстракций? К сожалению, нет: форматировщик, использующий
Ymd
, заставляет вызывать Date::as_ymd
даже в том случае, если используется только одно из значений месяц или день — а переход на Ymd
был совершён именно по соображениям производительности! У меня есть идеи, как можно решить этот недостаток, но... Это потребует несколько более тяжёлой ти́повой наркомании, так что это материал для следующей статьи.Forwarded from мне не нравится реальность
Важный вопрос: прикручивать ли лайки?
(особенно с учётом того, что у канала вроде как есть чат)
(особенно с учётом того, что у канала вроде как есть чат)
Final Results
7%
Да
26%
Да, но только на свои посты
52%
Нет
14%
Кешбери
Блог*
pinned «Важный вопрос: прикручивать ли лайки?
(особенно с учётом того, что у канала вроде как есть чат)»
(особенно с учётом того, что у канала вроде как есть чат)»
Предыдущий пин — не менее важный опрос
Telegram
Блог*
Ради чего вы подписаны на канал?
Мемы / Типострадания на расте / Разбор теоретических концепций (вроде аффинных типов) / Ссылки на (научно-) популярные статьи / Ссылки на статьи по CS / Рекомендации видеоигр / Другое (в @decltype_chat_ptr_t)
Мемы / Типострадания на расте / Разбор теоретических концепций (вроде аффинных типов) / Ссылки на (научно-) популярные статьи / Ссылки на статьи по CS / Рекомендации видеоигр / Другое (в @decltype_chat_ptr_t)
Forwarded from The After Times
Когда твоя программа в целом работает не так, как планировал, но свои функции выполняет
#article #math #prog
Как вы считаете середину отрезка в числах с плавающей точкой? Скорее всего, неправильно. Разбор различных методов сделать это, вместе с доказательствами на ошибки вычислений. И да, 80-битные регистры в x86 опять всё портят.
https://hal.archives-ouvertes.fr/file/index/docid/576641/filename/computing-midpoint.pdf
Как вы считаете середину отрезка в числах с плавающей точкой? Скорее всего, неправильно. Разбор различных методов сделать это, вместе с доказательствами на ошибки вычислений. И да, 80-битные регистры в x86 опять всё портят.
https://hal.archives-ouvertes.fr/file/index/docid/576641/filename/computing-midpoint.pdf