1.83K subscribers
3.24K photos
127 videos
15 files
3.52K links
Блог со звёздочкой.

Много репостов, немножко программирования.

Небольшое прикольное комьюнити: @decltype_chat_ptr_t
Автор: @insert_reference_here
Download Telegram
#prog #rust

TL;DR: GAT теперь не является incomplete feature
#prog #rust #моё

Одна из частых ошибок новичков в Rust — это ошибка при считывание строк из файла. Они вызывают в цикле read_to_string, но забывают очищать строку в конце итерации, из-за чего в строке остаётся то, что было прочитано на предыдущих операциях. Более того, просто воткнуть s.clear(); в конце тела может быть недостаточно: если в цикле есть оператор continue, то до исполнения этой строчки дело может и не дойти — в подобных случаях не только новички делают ошибки. Было бы хорошо иметь что-то, что выполняется автоматически в конце каждой итерации цикла вне зависимости то того, как она выполняется... И такая вещь в Rust есть: Drop::drop aka деструктор!

Не будем решать сугубо конкретную задачу очищения строки и решим сразу задачу выполнения произвольного кода при окончании итерации цикла (на самом деле просто при дропе, но нас сейчас интересует именно цикл). Очевидно, для того, чтобы добиться нужного нам эффекта, требуются ссылка на значение и действие, которое будет на нём вызвано. Для простоты рассмотрим вариант, когда действие одно и тоже для каждой операции дропа — в этом случае можно сохранить значение и это действие в одной структуре и, соответственно, хранить в структуре с интересующим нас дропом одну лишь ссылку на этот агрегат:

struct AutoDo<T, F> {
value: T,
action: F,
}


Теперь выпишем определение интересующей нас структуры — точнее, попытаемся:

struct DoOnDrop<'a, T, F>(&'a mut AutoDo<T, F>);


Нам нужно будет описать поведение этой структуры при дропе, но для этого требуется, чтобы ограничение на тип F — которое неизбежно появится для описания того факта, что это функция — повторялось при определении структуры. Без выполнения этого требования компилятор выдаст ошибку. А как, собственно говоря, должно выглядеть это требование? Это должна быть функция, которая принимает мутабельную ссылку на значение, причём не с каким-то конкретным временем жизни, а с произвольным. В итоге определение выглядит так:

struct DoOnDrop<'a, T, F: FnMut(&mut T)>(&'a mut AutoDo<T, F>);


Помним, что на самом деле это сахар для higher-ranked trait bounds:

struct DoOnDrop<'a, T, F: for<'val> FnMut(&'val mut T)>(&'a mut AutoDo<T, F>);


Так что же в дропе? Просто вызываем действие на значении:

impl<'a, T, F: FnMut(&mut T)> Drop for DoOnDrop<'a, T, F> {
fn drop(&mut self) {
(self.0.action)(&mut self.0.value)
}
}


Скобки вокруг self.0.action тут необходимы, поскольку иначе это будет проинтепретировано, как вызов метода AutoDo::action, коего, разумеется нет.

Для удобства использования — ну и для того, чтобы не нарушать инкапсуляцию — реализуем для DoOnDrop трейты Deref{, Mut}<Target = T>, а также метод AutoDo::get, который будет оборачивать ссылку в DoOnDrop. Реализацию, с вашего позволения, опущу в силу тривиальности.

Теперь покажем, как это работает. Сделаем простую программу: она будет считывать ввод по строкам и печатать их с переводом в верхний регистр, кроме строк, содержащих nope — их она будет просто отбрасывать:

fn main() {
use std::io::Read;

let stdin = std::io::stdin();
let mut stdin = stdin.lock();
let mut buf = AutoDo::new(String::new(), String::clear);

loop {
let mut buf = buf.get();
if let Ok(0) | Err(_) = stdin.read_to_string(&mut buf) {
break;
}
if buf.contains("nope") {
continue; // работает и с continue
}
println!("{}", buf.to_uppercase());
}

let buf = buf.into_inner();
assert!(buf.is_empty()); // буфер в итоге пустой
}


Вот и всё! Как всегда, весь код в гисте.
#prog #meme

Ну ведь правда же!
#prog #rust

Интересный способ обойти (для некоторых случаев) нехватку trait bounds в const fn при помощи GAT.

twitter.com/DrawsMiguel/status/1422770131969716228
#prog #article

Занятная статья об оптимизации базы данных. Один из тех редких случаев, когда промахи кеша инструкций действительно стали сдерживающим фактором. Что удивительно, эту проблему удалось решить довольно небольшими изменениями, которые в итоге привели к снижению задержки (в смысле latency), причём как средней, так и хвостовых перцентилей.
Как называется ситуация, когда посылка из Москвы в Санкт-Петербург ездит по Транссибирской магистрали?

Логистическая регрессия.
Forwarded from const_cast
Зачем уходить из C++?

Самый важный вопрос: зачем вообще что-то менять?
С годами понял такую вещь: если у тебя есть перфекционистские наклонности, то изучать C++ тебе противопоказано. Этот язык манит ощущением полного контроля над происходящим, но на деле все эти обещания, словно узоры на крыльях бабочки, рассыпаются в прах, стоит лишь к ним прикоснуться. В итоге твоя карьера программиста на C++ вырождается в попытки заставить эту бабочку двигаться по твоему маршруту, не касаясь её крыльев, иначе она упадёт и больше не сможет летать.

Например, ты никогда не можешь быть уверен, что твоя программа на C++ не содержит неопределённого поведения, разрешающего оптимизирующему компилятору просто удалить к чертям содержимое какой-нибудь функции полностью. В одной версии всё работает, а в новой — раз, и всё поломалось. Такое случается сплошь и рядом.

Дичайшая разрозненность экосистемы — ещё одна из проблем, разъедающих C++ изнутри. Нет единого способа сделать вообще хоть что-то. У абсолютно каждого «эксперта» в C++ своё мнение о том, как решать те или иные задачи.
Я перестал пытаться делать пет-проджекты на C++, потому что вместо того, чтобы реализовывать свою идею, я всё своё время тратил на подбор подходящих библиотек: эта слишком жирная, тут система сборки непонятная, эту поддерживает один калека, эта не кроссплатформенная, и т.д., и т.п.
Слишком много свободы и слишком мало контроля — всё это вырождается в то, что есть тысячи недоделанных решений, ни одно из которых не подходит тебе полностью — потому что, ну, можно ведь лучше вот тут сделать! И оптимизированнее! И вот ты уже в одном шаге от переизобретения велосипеда. Вместо того, чтобы пилить по кайфу свой пет-проджект.

И язык развивается куда-то не туда. С каждой новой версией стандарта C++ становится всё больше недовольных тем, что фундаментальные проблемы языка не решаются вообще никак. Вместо этого мы просто получаем очередную кучу фич, предназначенных для всех сразу и ни для кого по отдельности. Взять хотя бы многострадальный std::variant — могли бы сделать нормальный паттерн-матчинг, а получилось какое-то дикое говно, которое невозможно читать, а написать корректно ещё сложнее. И ещё пачку новых способов инициализации, куда же без этого.

В результате, в каждой команде для поддержания хоть какого-то порядка будет разрешено только своё собственное подмножество языка, за пределы которого выходить ни в коем случае нельзя, иначе всё вмиг развалится.
И вот реально все по-своему пишут. Единства нет и не предвидится.

Если честно, мне всё это очень надоело. С моим устройством мышления очень сложно оставаться продуктивным в таком болоте: слишком много времени и мозга уходит на технические детали и решение «детских» проблем языка, а не на то, чтобы заставить компьютер делать то, что ты от него хочешь. Мотивация в этом ковыряться уже давно иссякла. Я в программирование не за этим пришёл.
#prog #js #article

- — подходящее имя для пакета на npm, не находите? И да, он существует и скачивается в больших количествах. В статье рассказывается, почему.

(thanks @oleg_log)
#prog #rust #моё

Как вы знаете, в Rust массив можно инициализировать при помощи литералов двумя способами. Первый — перечислить все элементы массива через запятую внутри квадратных скобок. Второй — в квадратных скобках указать значение и после точки с запятой — длину массива: в этом случае массив будет заполнен копиями указанного значения. Так или иначе, нужно задать всё элементы массива сразу. И это не всегда бывает удобно.

В C и C++ есть такая вещь, как zero initialization. Не вдаваясь в подробности, скажу только, что она позволяет инициализировать массив, указав значения не для всех элементов. Остальные значения будут проинициализированы нулём. Что является нулём для каждого отдельного типа — вопрос отдельный, но для числовых типов это будет, собственно, ноль. Можно ли воссоздать данный функционал в Rust? Ну, как вы могли догадаться по факту наличия этого поста — можно!

Но сначала подумаем, как мы можем это организовать. Причём тут есть два вопроса: как заполнять массив и как определять нулевое значение. Решением в духе C++ было бы сделать сначала неинициализированный массив, записать в него данные значения, а потом дописать в него нули (в прямом смысле, через std::mem::zeroed). Такой подход страдает следующими недостатками:
* нужно определить, какие типы можно безопасно инициализировать нулевыми байтами. Можно это сделать через маркерный трейт, но, во-первых, это придётся сделать unsafe трейтом, а во-вторых, это стоило бы сделать auto-трейтом, которые на стабильной версии пока что делать нельзя.
* нужно разбираться с дропом элементов в случае, если создание одного из них запаникует. Не то чтобы это было прям сложно, но это дополнительная возня и дополнительный unsafe.

Но постойте-ка, у нас же есть Rust, мы можем сделать лучше! Заполнять массив будем иначе: сначала размножим нулевое значение, а потом перепишем префикс. Да, у такого подхода есть свои недостатки, и о них обязательно упомяну. Для того, чтобы можно было использовать синтаксис [elem; length], требуется, чтобы elem было либо выражением, имеющим Copy-тип, либо константой. Не будем излишне ограничивать наш подход и будем для нулевого значения использовать ассоциированную константу трейта. Это, в частности, позволит нам использовать "нулевые" значения типов, которые нельзя корректно проинициализировать нулевыми байтами, например, Vec:

trait ConstZero {
const ZERO: Self;
}

Теперь начнём писать макрос для собственно инициализации (ну да, макрос, а вы как хотели?). Хочется, чтобы то, что мы в итоге сделали, можно было использовать и для инициализации констант. Поэтому ограничимся только операциями, которые можно применять в const-контексте. На вход будем принимать имеющиеся элементы массива и итоговую дину — то, без чего никуда:

macro_rules! arr_init {
($($elem:expr,)* ; $len:expr) => {{
// ...
}};
}

Если вы внимательно присмотритесь, то увидите, что, в отличие от обычных списков в синтаксисе Rust, тут запятая после каждого элемента обязательно. Я сделал это намеренно, чтобы в случае, если мы переписываем один элемент, синтаксис не совпадал с нативным синтаксисом для массива из повторяющихся значений, а также чтобы подчеркнуть, что пользователь макроса указывает (потенциально) не все значения массива. Перейдём к основной логике макроса:

let mut ret = [<_ as ConstZero>::ZERO; $len];
let mut i = 0usize;
$(
i += 1;
ret[i - 1] = $elem;
)*
ret

Использование <_ as ConstZero>::ZERO позволяет подтянуть нулевое значение типа, не называя его. Вот она, сила вывода типов!
А вот почему развёрнутый цикл написан столь странным образом? Казалось бы, ничто не мешает написать так:

$(
ret[i] = $elem;
i += 1;
)*

Мешает компилятор. В сгенерированном коде останется инкремент индекса после инициализации префикса. Компилятор будет справедливо ругаться, что значение, записанное в индекс, нигде не используется. Это предупреждение можно подавить, но выглядеть это будет несколько уродливо:

$(
ret[i] = $elem;
#[allow(unused_assignments)]
{
i += 1;
}
)*

Разместить #[allow(unused_assignments)] непосредственно на присваивании нельзя — это будет считаться атрибутом на выражении, что компилятор Rust на стабильной версии не поддерживается. Более того, это было бы не совсем верно в том смысле, что только последняя из операций реально нуждается в подавлении предупреждений. Это можно было бы решить более аккуратным матчингом с явным выделением первого или последнего значения, но, на мой взгляд, это неоправданное усложнение.

Внимательный читатель мог бы также заметить, что в случае, если список, захватываемый фрагментом $($elem:expr,)*, пустой, то компилятор будет жаловаться на мертвый код из-за объявления неиспользуемой переменной i. Но оно и к лучшему: это значит, что в месте, где макрос используется, его вызов выглядит, как array_init![; length], что, на мой взгляд, лучше переписать в виде [TyName::ZERO; length], и предупреждение компилятора будет подталкивать к этому варианту.

Закончили ли мы с ядром логики? Не совсем: что будет, если пользователь перечислит больше элементов, чем длина массива? Ну, будет паника. В рантайме, ага. Но погодите, у нас же вся информация есть на этапе компиляции! Значит, имеет смысл на этапе компиляции же эту ошибку и предотвратить. Для этого мы будем использовать уже ставший традиционным приёмом: сделаем константу, выражение для которой при ошибке не будет тайпчекаться и потому будет стопорить компиляцию. Более конкретно, мы сделаем массив из () той же длины, что и результирующий массив, а попытаемся приписать значение массива из () длины, являющейся максимумом от длины результирующего массива и количества переданных элементов. Если элементов слишком много, то типы слева и справа не совпадут:

const LEN: usize = $len;
const _INIT_LEN: usize = $({stringify!($elem); 1usize} +)* 0;
const _ASSERT_NOT_TOO_MANY: [(); LEN] = [(); if LEN >= _INIT_LEN { LEN } else { _INIT_LEN }];

А вот теперь можно приступить к той части, где мы пытаемся мимикрировать под синтаксис C++ в том плане, что указываем только часть элементов массива. Для этого нам надо распарсить: (опционально) ключевое слово mut, имя переменной, двоеточие, тип массива (то, откуда мы возьмём длину массива), знак равно и литерал массива. Элементы у нас имеются, длина — тоже, а значит, ничто не мешает вызвать array_init:

macro_rules! arr_let {
($name:ident: [$ty:ty; $len:expr] = [$($elem:expr,)*]) => {
let $name: [$ty; $len] = arr_init![$($elem,)*; $len];
};
// аналогично для mut
}

Здесь запятые после элементов также обязательны по указанной выше причине.

Полностью аналогично пишется определение для инициализации констант. Надо только иметь в виду, что const-блоков пока не завезли, так что придётся помещать код внутрь const-функции:

macro_rules! arr_const {
($name:ident: [$ty:ty; $len:expr] = [$($elem:expr,)*]) => {
const $name: [$ty; $len] = {
const fn init_arr() -> [$ty; $len] {
arr_init![$($elem,)*; $len]
}
init_arr()
};
};
}
Написав немного реализаций ConstZero для общеупотребимых типов (с вашего позволения, я это опущу, ибо код тривиальный), мы можем написать, скажем, так:

#[derive(Debug)]
struct Thirteen(u32);

impl ConstZero for Thirteen {
const ZERO: Self = Self(13);
}

arr_const!(ARRAY: [Thirteen; 13] = [Thirteen(15), Thirteen(14),]);

fn main() {
println!("{:?}", ARRAY);

arr_let!(arr: [_; 3] = [([vec![2u64, 3]], (("hey",), (2i32, 3.14f64, ()), [true, true])),]);
let all_but_one_zeros = arr_init![128u128,; 7];

println!("{:?}", arr);
println!("{:?}", all_but_one_zeros);

// не компилируется
// let wrong = arr_init![(),; 0];
}

Вполне работает!

Теперь поговорим о недостатках:
* самый существенный: ввиду избранного способа мы не можем инициализировать значениями типов с нетривиальным дропом (скажем, Vec), даже если их значения можно сконструировать на этапе компиляции.
* пользователю приходится так или иначе сообщать о длине массива, причём даже тип массива в arr_let! нельзя заменить на псевдоним. Честное слово, я пытался обойти это недостаток многими способами — каждый раз я утыкался цикл в выводе типов, который не давал скомпилировать код.
* если переписанных значений достаточно много, то сгенерированный код тратит время в рантайме на запись "нулевых" значений, которые потом будут перезаписаны. Да, компилятор может убрать dead store, но не в том случае, если выражения в макросе потенциально могут паниковать.
* ну и макросы написаны не так, чтобы можно было сразу использовать их в библиотеке (не хватает #[macro_export], $crate и поддержки атрибутов), но это легко поправимо.

На этом всё. Как всегда, весь код в гисте.
Блог*
Написав немного реализаций ConstZero для общеупотребимых типов (с вашего позволения, я это опущу, ибо код тривиальный), мы можем написать, скажем, так: #[derive(Debug)] struct Thirteen(u32); impl ConstZero for Thirteen { const ZERO: Self = Self(13);…
Кстати, не знаю, когда это появилось, но на Rust playground теперь в числе инструментов есть раскрытие макросов (Tools > Expand macros). Может пригодиться для подобных вещей
Forwarded from Segment@tion fault
R&D все-таки штука неблагодарная. Выбегаешь такой к коллегам - пацаны, работает! Rust запускает питоновские concurrent.futures параллельно в GIL-треде и получает await'ом результат через tokio!

А они тебя поздравляют, из вежливости. И догадываются, что раз tokio, значит это что-то с Японией.
Forwarded from Segment@tion fault
Собственно насчёт сумрачной технологии интегрирования питона в раст - получилось оформить в crate. Производительность "всунуть задачу в ThreadPoolExecutor и подождать" - "терпимая", но стабильность - отличная. Главное, что задачи ставятся из любого Rust-потока, в котором работает coro, GIL не мешает.

https://crates.io/crates/pime