1.83K subscribers
3.3K photos
132 videos
15 files
3.58K links
Блог со звёздочкой.

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

Небольшое прикольное комьюнити: @decltype_chat_ptr_t
Автор: @insert_reference_here
Download Telegram
Как называется ситуация, когда посылка из Москвы в Санкт-Петербург ездит по Транссибирской магистрали?

Логистическая регрессия.
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
xxx: можно же было в стд сделать одеяло-имплементацию

#quotes #трудовыебудни #justrustaceanthings
log(😅) = 💧log(😄)
Согласно мнению одного человека, я — упырь 👌😐
Посуда бьётся к счастью. А сердечки?