#prog #rust
Хозяйке на заметку
Пара советов по строкам в Rust:
1) Если вам нужно разбить строку по одному из нескольких возможных символов — не спешите расчехлять регулярки, для это задачи вполне хватит стандартной библиотеки. Множество строковых методов навроде {, r}split{, _terminator}, trim{, _start, _end}_matches, find и прочие принимают в качестве аргумента для поиска значение, тип которого реализует пока нестабильный трейт Pattern. В настоящий момент его реализуют
Вызвать `next`?
Нет.
Также вызвать `next_back`?
Нет.
Это всё неполные ответы. Если мы получаем мутабельную ссылку на
Покажу на примере.
Вот первый способ вытащить строку из
Хозяйке на заметку
Пара советов по строкам в Rust:
1) Если вам нужно разбить строку по одному из нескольких возможных символов — не спешите расчехлять регулярки, для это задачи вполне хватит стандартной библиотеки. Множество строковых методов навроде {, r}split{, _terminator}, trim{, _start, _end}_matches, find и прочие принимают в качестве аргумента для поиска значение, тип которого реализует пока нестабильный трейт Pattern. В настоящий момент его реализуют
&str
, &&str
, &String
, impl FnMut(char) -> bool
и (почему-то малоизвестный) &[char]
. Таким образом, разбить строку по нескольким символам легко:let result = "Hello, world!".split(&['o', 'l'][..]).collect::<Vec<_>>();2) Если функция принимает на вход
assert_eq!(result, vec!["He", "", "", ", w", "r", "d!"]);
&mut std::str::Chars
, что она может с ним сделать?Вызвать `next`?
Нет.
Также вызвать `next_back`?
Нет.
Это всё неполные ответы. Если мы получаем мутабельную ссылку на
Chars
, мы можем редактировать произвольным образом, в том числе и поменять его целиком. Chars
внутри себя содержит строки, символы которой он перебирает, и при помощи метода Chars::as_str эту строку можно достать. Таким образом, имея мутабельную ссылку на Chars
, можно вытащить из него строку, вырезать из строки нужный кусок и переписать переданный итератор .chars()
от этого кусочка.Покажу на примере.
Вот первый способ вытащить строку из
Chars
(медленный, требующий аллокаций и не совсем корректный):fn extract_line2(chars: &mut Chars) -> String {Второй способ (побыстрее и не требующий аллокаций):
chars.take_while(|&ch| !matches!(ch, '\r' | '\n')).collect()
}
fn extract_line<'s>(chars: &mut Chars<'s>) -> Option<&'s str> {
let s = chars.as_str();
let line = s.lines().next()?;
*chars = s[line.len()..].chars();
Some(line)
}
doc.rust-lang.org
Pattern in std::str::pattern - Rust
A string pattern.
#prog #article
Статья об антипаттерне использования
да, это статья десятилетней давности, ну и что?
Статья об антипаттерне использования
File.exists
.да, это статья десятилетней давности, ну и что?
Paranoidcoding
The File System is Unpredictable
Блог*
Донаты! Донаты?
Я, конечно, совсем ни на что не намекаю, но вот вам номер моей карты:
4274 3200 5402 8520
4274 3200 5402 8520
Блог*
Я, конечно, совсем ни на что не намекаю, но вот вам номер моей карты: 4274 3200 5402 8520
Не переживайте, я не собираюсь возводить paywall, все посты в обозримом будущем продолжат публиковаться на канале
Forwarded from мне не нравится реальность (waffle 🧇🍓)
Хвастаюсь: мой PR добавляющий
[Tracking Issue]
as_str
методы к разным Split
-строковым итераторам недавно смерджил толян![Tracking Issue]
Как же достали люди, которые носят маски, но при этом не прикрывают нос. Они что, правда не понимают, что это всё равно что без маски ходить?
#prog #article
О том, почему использовать время модификации для систем сборки — плохая идея (и что с этим можно сделать).
О том, почему использовать время модификации для систем сборки — плохая идея (и что с этим можно сделать).
apenwarr.ca
mtime comparison considered harmful
tl;dr: Rebuilding a target because its mtime is older than the
mtimes of its dependencies, like `make` does, is very error prone. redo does...
mtimes of its dependencies, like `make` does, is very error prone. redo does...
#prog #rust #моё
UPD (22.09.2022): пост несколько устарел, держи поправку.
UPD (23.09.2022): как оказалось, я прилично так переусложнил реализацию. Ссылка на итоговый гист показывает последнюю версию. Ссылка на версию на момент написания поста: тык.
Сколько растоманов нужно, чтобы вкрутить лампочку? То есть, я хотел сказать, сколько nightly-фич нужно, чтобы перевести число в строку на этапе компиляции? Как оказалось... Ни одной!
Но обо всём по порядку. Пусть у нас есть константа, и мы хотим перевести её в строку на этапе компиляции. Фигня вопрос:
Так, дайте мнепогуглить разобраться... Ага... Угу...
Итак, это не работает.
Для начала давайте разберёмся, что вообще такое строка? Строка — это массив байт. Не совсем произвольный — в Rust это должен быть массив байт, соответствующих кодировке UTF-8. С другой стороны, для записи чисел нам хватит и символов ASCII, так что на это можно немного забить. Так можем ли мы перевести число в массив байт? Конечно! И с учётом того, что c версии Rust 1.46.0 мы можем использовать if, match, while и loop в const fn, мы можем провести эту манипуляцию на этапе компиляции! Давайте посмотрим, как это сделать, на примере... Ну, скажем,
UPD (22.09.2022): пост несколько устарел, держи поправку.
UPD (23.09.2022): как оказалось, я прилично так переусложнил реализацию. Ссылка на итоговый гист показывает последнюю версию. Ссылка на версию на момент написания поста: тык.
Сколько растоманов нужно, чтобы вкрутить лампочку? То есть, я хотел сказать, сколько nightly-фич нужно, чтобы перевести число в строку на этапе компиляции? Как оказалось... Ни одной!
Но обо всём по порядку. Пусть у нас есть константа, и мы хотим перевести её в строку на этапе компиляции. Фигня вопрос:
const STRING: String = 42.to_string();Всё, всем спасибо за внимание, пост можно зака...
error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variantsА, ну да. Я совсем забыл, что Rust создан не для программирования — он создан для страданий. Ну, окей, перевод числа в строку на этапе компиляции — это невероятно часто используемая фича, наверняка в стандартной библиотеке есть что-то готовое. И, действительно, есть stringify!
--> src/lib.rs:1:24
|
1 | const STRING: String = 42.to_string();
| ^^^^^^^^^^^^^^
const STRING: &str = stringify!(42);
fn main() {
assert_eq!(STRING, "42");
}
Так, теперь точно можно заканчивать. Конечно, хардкодить константы нехорошо, так что дадим ей имя:const THE_ANSWER: usize = 42;
const STRING: &str = stringify!(THE_ANSWER);
fn main() {
assert_eq!(STRING, "42");
}
Да, всё работает, всем спасибо, до новых встре...thread 'main' panicked at 'assertion failed: `(left == right)`
left: `"THE_ANSWER"`,
right: `"42"`', src/main.rs:5:5
Погодите, что?Так, дайте мне
Итак, это не работает.
stringify!
работает исключительно на синтаксическом уровне. Так что же, мы лишены нормального инструмента? Строго говоря, да... Но мы можем сделать свой инструмент!Для начала давайте разберёмся, что вообще такое строка? Строка — это массив байт. Не совсем произвольный — в Rust это должен быть массив байт, соответствующих кодировке UTF-8. С другой стороны, для записи чисел нам хватит и символов ASCII, так что на это можно немного забить. Так можем ли мы перевести число в массив байт? Конечно! И с учётом того, что c версии Rust 1.46.0 мы можем использовать if, match, while и loop в const fn, мы можем провести эту манипуляцию на этапе компиляции! Давайте посмотрим, как это сделать, на примере... Ну, скажем,
u32
:const fn to_ascii(mut n: u32) -> [u8; 20] {
let mut ret = [0; 20];
let mut i = 0;
while n != 0 {
ret[i] = (n % 10) as u8 + b'0';
n /= 10;
i += 1;
}
...
Окей, пока всё идёт неплохо — выделить цифру несложно, равно как и перевести её в символ ASCII. Но у нас строка получилась в обратном порядке. Окей, давайте обратим порядок — достаточно лишь вызвать reverse
: ...
ret[..i].reverse();
...
Теперь нам надо лишь...error[E0723]: mutable references in const fn are unstable
--> src/lib.rs:12:5
|
12 | ret[..i].reverse();
| ^^^^^^^^
|
= note: see issue #57563 <https://github.com/rust-lang/rust/issues/57563> for more information
= help: add `#![feature(const_fn)]` to the crate attributes to enable
Ну ёлки-палки, компилятор продолжает ставить палки в колёса. Выходит, надо писать переворот вручную. И задействовать <[u8]>::swap
мы тоже не можем, потому что эта функция, очевидно, также требует мутабельной ссылки (а ещё потому, что она не const fn
, но это уже не имеет значения). Стиснув зубы, пишем переворот в очень сишном стиле: ...
let len = i;
let mut i = 0; // да, я дважды использую одно и то же имя
// для разных счётчиков, останьте
while i < len / 2 {
let tmp = ret[i];
ret[i] = ret[len - i - 1];
ret[len - i - 1] = tmp;
i += 1;
}
...
Telegram
Блог*
#prog #rust #моё
Тем временем вышла версия Rust 1.64.0. Релиз я разберу подробнее чуть позже, а пока отмечу, что благодаря стабилизации core::slice::from_raw_parts я наконец-то могу переписать свой макрос, который я не трогал два года, так, чтобы он работал…
Тем временем вышла версия Rust 1.64.0. Релиз я разберу подробнее чуть позже, а пока отмечу, что благодаря стабилизации core::slice::from_raw_parts я наконец-то могу переписать свой макрос, который я не трогал два года, так, чтобы он работал…
Теперь нам надо вернуть массив из функции и...
бровей чекер borrow checker бдит даже в const fn, зараза этакая. Выходит, нам надо возвращать вместе с массивом длину строки:
warning: this looks like you are swapping elements of `ret` manually
--> src/lib.rs:14:9
|
14 | / let tmp = ret[i];
15 | | ret[i] = ret[len - i - 1];
16 | | ret[len - i - 1] = tmp;
| |______________________________^ help: try: `ret.swap(i, len - i - 1)`
|
= note: `#[warn(clippy::manual_swap)]` on by default
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_swap
Нет, Clippy, я не могу тут вызвать reverse
, я пытался! Когда ж ты поумнеешь? А пока что — вот тебе кляп: ...
#[allow(clippy::manual_swap)]
while i < len / 2 {
...
На чём я остановился? Ах да, мы возвращаем массив из функции и... Погодите, а как мы узнаем, где заканчивается строка? Сделать слайс не получится, потому что тогда мы будем возвращать ссылку на локальную переменную, а ...
(ret, len)
}
Соответственно, нам надо также поменять и возвращаемый тип у функции:const fn to_ascii(mut n: u32) -> ([u8; 20], usize) {
...
Как нам теперь из этого добра получить &str
? Дело нехитрое: делаем слайс байтов, переводим в str — и дело в шляпе! Вернее, было бы в шляпе, если бы std::str::from_utf8
была бы const fn. А она таковой не является! Что же делать? Ладно, пока я над этим думаю, давайте проверим, что to_ascii
работает правильно — хрен с ними, с константами, будем сами в строку переделывать:fn main() {
for &x in &[42, 1, 0] {
let (bytes, len) = to_ascii(x);
println!("{}: {:?}", x, std::str::from_utf8(&bytes[..len]).unwrap());
}
}
Что оно выводит?42: "42"Ну, по крайней мере, оно хотя ра... Стоп, что?
1: "1"
0: ""
0: ""
Граничный случай! Окей, не вопрос, добавим if
в начале:...Теперь всё работает пра...
if n == 0 {
return (ret, 1);
}
...
42: "42"А, логично, нам нужен именно ASCII-шный символ нуля, а не нуль-символ:
1: "1"
0: "\u{0}"
...Вот теперь всё в порядке:
if n == 0 {
ret[0] = b'0';
return (ret, 1);
}
...
42: "42"Ладно, это всё прекрасно, но что нам делать с превращением всего этого в строку? Ну, раз честным путём пройти не вышло — считерим! В настоящий момент
1: "1"
0: "0"
&str
и &[u8]
имеют одно и то же представление, так что, хотя мы и не можем использовать str::str::from_utf8_unchecked
напрямую (это потребует nightly-фичи const_str_from_utf8_unchecked
), мы можем просто скопировать реализацию (кстати, по не вполне понятным мне причинам transmute
можно использовать внутри константных выражений, но не внутри const fn):const DECOMPOSED: ([u8; 20], usize) = to_ascii(42);Что ж, теперь мы можем спокойно распеча...
const STR: &str = unsafe { std::mem::transmute(&DECOMPOSED.0[..DECOMPOSED.1]) };
error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants...Облом. Опять. Что ж делать-то? Видимо, придётся читерить ещё больше. Если мы не можем сконструировать слайс, то мы можем сконструировать что-то, что имеет такое же представление, как и слайс, и потом универсальным превращателем
--> src/lib.rs:29:49
|
29 | const STR: &str = unsafe { std::mem::transmute(&DECOMPOSED.0[..DECOMPOSED.1]) };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
transmute
преобразовать в строковой слайс. Но тут мы ступаем на шаткую территорию: хотя и известно, что слайс — это пара из указателя и длины, мы не знаем, в каком порядке они идут. Что ж, мы подсмотрим, какой порядок на самом деле и сделаем так же:#[repr(C)] // repr(C) обязателен, чтобы между полями не было паддинга
struct RawSlice {
ptr: *const u8,
len: usize
}
doc.rust-lang.org
from_utf8 in std::str - Rust
Converts a slice of bytes to a string slice.
🔥1
Сделаем слайс по частям и превратим:
Оно работает! Но кое-что меня всё же беспокоит: порядок полей в представлении слайсов не зафиксирован и может поменяться в любой версии. Можем ли мы как-то от этого защититься? Вообще, да... Но нам потребуются фичи nightly (да, я вас немного обманул, когда сказал, что nightly не понадобится, но с другой стороны, для кода, собственно создающего строку, он не нужен):
Есть ещё кое-что, что мне не очень нравится: мы используем массив размером 20 байт в надежде на то, что их хватит для форматирования числа. Что будет, если этого размера не хватит? Давайте понизим размер массива до единицы и проверим:
const DECOMPOSED: ([u8; 20], usize) = to_ascii(42);(В том, что мы берём ссылку от константы, нет ничего плохого из-за static promotion)
const STR: &str = unsafe { std::mem::transmute(RawSlice {
ptr: &DECOMPOSED.0 as *const _, // &[u8] неявно приводится к *const [u8],
// а тот уже кастуется в *const u8
len: DECOMPOSED.1,
})};
fn main() {
assert_eq!(STR, "42");
}
Оно работает! Но кое-что меня всё же беспокоит: порядок полей в представлении слайсов не зафиксирован и может поменяться в любой версии. Можем ли мы как-то от этого защититься? Вообще, да... Но нам потребуются фичи nightly (да, я вас немного обманул, когда сказал, что nightly не понадобится, но с другой стороны, для кода, собственно создающего строку, он не нужен):
mod _sanity_check {Здесь мы создаём сырой указатель на слайс (и, насколько я понимаю, в данном случае вызов
use super::RawSlice;
use std::mem::transmute;
const _RAW_SLICE_HAS_RIGHT_REPR: [(); 1] = {
const SENTINEL: usize = 1342;
const SENTINEL_SLICE: RawSlice = RawSlice {
ptr: std::ptr::null(),
len: SENTINEL,
};
[(); (<*const [u8]>::len(unsafe { transmute(SENTINEL_SLICE) }) == SENTINEL) as _]
};
}
transmute
безопасен, поскольку у сырых толстых указателей значительно менее строгие требования к корректности, чем к ссылкам на слайс) с заданной длиной, а потом вынимаем её при помощи метода len сырого указателя. Если мы угадали с порядком полей, то сравнение длин возвратит true
, которое будет скастовано в 1usize
и, таким образом, образует литерал массива нужного типа. В данный момент код компилируется. Давайте поменяем порядок полей в RawSlice
:#[repr(C)]...и посмотрим, что скажет компилятор:
struct RawSlice {
len: usize,
ptr: *const u8,
}
...expected an array with a fixed size of 1 element, found one with 0 elements
Отлично, ровно то, что мы и хотели (на самом деле мы хотели бы более внятное сообщение об ошибке, но пока что имеем, что имеем).Есть ещё кое-что, что мне не очень нравится: мы используем массив размером 20 байт в надежде на то, что их хватит для форматирования числа. Что будет, если этого размера не хватит? Давайте понизим размер массива до единицы и проверим:
error: any use of this value will cause an errorЧто ж, ожидаемо. Но брать размер с запасом всё же не хочется. Мы не можем знать наперёд, сколько цифр понадобится... Хотя, погодите-ка, можем!
--> src/main.rs:27:9
|
27 | ret[i] = (n % 10) as u8 + b'0';
| ^^^^^^
| |
| index out of bounds: the length is 1 but the index is 1
| inside `to_ascii` at src/main.rs:27:9
| inside `DECOMPOSED` at src/main.rs:50:38
...
50 | const DECOMPOSED: ([u8; 1], usize) = to_ascii(42);
| --------------------------------------------------
|
= note: `#[deny(const_err)]` on by default
const fn digits_len(mut n: u32) -> usize {Отлично, это всё более-менее работает. Но это всё ещё ногострельно! Тут довольно много кода, который можно написать неправильно, а собственно число, для которого делается строка, приходится использовать дважды (да, я в курсе, что можно просто с запасом взять, нет, я не хочу так делать). Можем ли мы всё это как-то инкапсулировать? Можем! При помощи макроса:
if n == 0 {
return 1;
}
let mut n_digits = 0;
while n != 0 {
n /= 10;
n_digits += 1;
}
n_digits
}
const LEN: usize = digits_len(42);
const fn to_ascii(mut n: u32) -> ([u8; LEN], usize) {
...
macro_rules! make_literal {Проверим:
($n:expr) => {{
// ^первая пара скобок является частью синтаксиса macro_rules!,
// а вторая открывает блок
const VALUE: u32 = $n;
// далее весь код почти без изменений,
// только с заменой конкретного значения на VALUE
const STR: &str = ...;
STR
}}
}
const STR: &str = make_literal!(41 + 1);Оно работает!
fn main() {
assert_eq!(STR, "42");
}
GitHub
rfcs/text/1414-rvalue_static_promotion.md at master · rust-lang/rfcs
RFCs for changes to Rust. Contribute to rust-lang/rfcs development by creating an account on GitHub.
...С другой стороны, зачем ограничиваться именно
Проверим:
Оно работает! Хотя... Работает ли?
И...
Ясно, нам нужно что-то иное: остаток от деления отрицательного числа на положительное является отрицательным, а нам нужно положительное число, у которого нужно поменять знак. Что, сделаем для этого функцию и запихнём в макрос:
Другие функции нам тоже нужно поменять, чтобы печатать минус:
Протестируем:
Работает!
...Закончили ли мы на этом? Нет! Во-первых, я не хочу указывать каждый раз тип выражения. Во-вторых, из-за ограничений
Проверим:
Вот теперь всё работает. Как всегда, код в гисте.
u32
? Наш код вполне может работать с другими типами! Давайте это исправим:macro_rules! make_literal {
(($n:expr) : $ty:ty) => {{
// то же, что и было, но заменяем u32 на $ty
STR: &str = ...;
STR
}}
}
Проверим:
const STR: &str = make_literal!((41 + 1): u32);
fn main() {
assert_eq!(STR, "42");
}
Оно работает! Хотя... Работает ли?
const STR: &str = make_literal!((-41 - 1): i32);
fn main() {
assert_eq!(STR, "-42");
}
И...
error: any use of this value will cause an error
--> src/main.rs:42:26
|
42 | ret[i] = (n % 10) as u8 + b'0';
| ^^^^^^^^^^^^^^^^^^^^^
| |
| attempt to compute `254_u8 + 48_u8`, which would overflow
| inside `to_ascii` at src/main.rs:42:26
| inside `DECOMPOSED` at src/main.rs:59:48
Ясно, нам нужно что-то иное: остаток от деления отрицательного числа на положительное является отрицательным, а нам нужно положительное число, у которого нужно поменять знак. Что, сделаем для этого функцию и запихнём в макрос:
const fn extract_digit(n: $ty) -> $ty {
let mut ret = n % 10;
#[allow(unused_comparisons)]
// ^ сравнение не имеет смысла для беззнаковых чисел
if ret < 0 {
// мы не можем написать ret = -ret,
// поскольку унарный минус не определён для беззнаковых
ret = 0-ret;
}
ret
}
Другие функции нам тоже нужно поменять, чтобы печатать минус:
const fn digits_len(mut n: $ty) -> usize {
if n == 0 {
return 1;
}
let mut n_digits = 0;
#[allow(unused_comparisons)]
if n < 0 {
n_digits += 1;
}
...
}
...
const fn to_ascii(mut n: $ty) -> ([u8; LEN], usize) {
#[allow(unused_comparisons)]
let is_negative = n < 0;
...
while n != 0 {
ret[i] = extract_digit(n) as u8 + b'0';
n /= 10;
i += 1;
}
if is_negative {
ret[i] = b'-';
i += 1;
}
...
Протестируем:
const STR: &str = make_literal!((-41 - 1): i32);
fn main() {
assert_eq!(STR, "-42");
}
Работает!
...Закончили ли мы на этом? Нет! Во-первых, я не хочу указывать каждый раз тип выражения. Во-вторых, из-за ограничений
macro_rules!
после expr
нельзя ставить двоеточие, из-за чего мне пришлось внести в синтаксис раздражающие скобки. Как известно, всякую проблему можно решить ещё одним слоем абстракции, поэтому я именно этим и воспользуюсь: я сделаю макрос, который принимает имя и тип и генерирует ещё один макрос с переданным именем, который принимает выражение нужного типа и уже возвращает константу:macro_rules! make_literal_maker {
($name:ident : $ty:ty) => {
macro_rules! $name {
($n:expr) => {{
// весь остальной код без изменений
}}
}
}
}
Проверим:
make_literal_maker!(make_str_literal_from_usize: usize);
make_literal_maker!(make_str_literal_from_i8: i8);
const STR_UNSIGNED: &str = make_str_literal_from_usize!(41 + 3 - 2);
const STR_SIGNED: &str = make_str_literal_from_i8!(-100 - 1);
fn main() {
assert_eq!(STR_UNSIGNED, "42");
assert_eq!(STR_SIGNED, "-101");
}
Вот теперь всё работает. Как всегда, код в гисте.
Gist
Реализация макроса по переводу константных числовых значений в строковые константы
Реализация макроса по переводу константных числовых значений в строковые константы - main.rs
От этого поста телеге немного поплохело: когда я перематывал поле ввода наверх, а потом обратно в конец, часть написанного в конце пропадало и было недоступно для редактирования. К счастью, этот баг был чисто визуальным — переключение между каналами вернуло набранное — но, блин, Дуров, какого хрена?
https://t.iss.one/ihatereality/1610
> совершенно беcсмысленный, возведённый в абсолют бред
АХАХАХАХАХАХАХАХА
Это ты ещё compile-time FizzBuzz не видел.
UPD: я его написал.
> совершенно беcсмысленный, возведённый в абсолют бред
АХАХАХАХАХАХАХАХА
Это ты ещё compile-time FizzBuzz не видел.
UPD: я его написал.
Telegram
Мне не нравится реальность
Антон делает то, что я люблю в программировании больше всего — совершенно беcсмысленный, возведённый в абсолют бред, на который потрачено больше времени, чем стоило бы, но зато... зато красиво!
dereference_pointer_there/1210
dereference_pointer_there/1210