Как я вкатился в Rust (и зачем)

 Публичный пост
6 июля 2023  6153

Внимание

В посте будет код. На Rust. Вам может стать некомфортно. Вам может стать интересно. Я предупредил :)

И зачем?

Мы должны писать надёжный код, который не падает, не течет, не делает data race и не ломается, поскольку ошибки в
нашем коде критичнее некуда. Просто представьте, что Cloud API удалило не тот кластер. Поэтому я изучаю новый
инструмент, который позволит писать более безопасный код. Изучаю сам, чтоб не отвлекать инженеров от текущих задач

© менеджер во мне

А на самом деле зачем? :)

Однажды простого инженера Ваню повысили до лида, а потом ещё раз повысили, до менеджера. И стало Ване некогда писать код
и чинить продакшен, но инженер в нём окончательно ещё не умер. И стал Ваня искать, куда бы применить инженера, чтоб и
коллегам не мешать и интересно было. И нашёл Rust.

Процесс вкатывания

Картинка, отражающая реальность

ну вы поняли
ну вы поняли

Сначала новичку показывают Hello World и рассказывают про borrowing, и выглядит всё достаточно просто, ну примерно вот
так.

fn main() {
    let name = std::env::args().
        nth(1).
        unwrap_or_else(|| "%username%".to_owned());
    println!("Welcome to Rust, {name}!");
}

Потом, почти без предупреждения, это превращается во что-то такое.

impl<T> Stream for BroadcastStream<T>
    where T: 'static + Clone + Send
{
    type Item = Result<T, BroadcastStreamRecvError>;
    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>)
                 -> Poll<Option<Self::Item>> {
        let (result, rx) = ready!(self.inner.poll(cx));
        self.inner.set(make_future(rx));
        match result {
            Ok(item) => Poll::Ready(Some(Ok(item))),
            Err(RecvError::Closed) => Poll::Ready(None),
            Err(RecvError::Lagged(n)) => {
                Poll::Ready(Some(Err(BroadcastStreamRecvError::Lagged(n))))
            }
        }
    }
}

А потом ты сам пишешь такой код и не видишь в этом ничего странного.

А секрет прост - простой советский три кита Rust.

Кит первый - ownership & borrowing

Если ты что-то взял - отдай

© объяснение принципов ownership

У любой переменной в Rust есть владелец. Владение заканчивается в момент передачи переменной другому владельцу.
Обращаться к тому, чем мы уже не владеем - нельзя.
Например, вот так - нельзя.

fn main() {
    let name = "name".to_owned(); // владелец name - текущая функция
    call_fn(name); // мы отдали владение name другой функции
    println!("my name = {name}"); // тут будет ошибка и оно не скомпилируется
}

fn call_fn(name: String) -> String {
    println!("call_fn name: {name}");
}

Но если нам нужно - мы можем отдать владение назад, и тогда будет снова можно.

fn main() {
    let name = "name".to_owned(); // владелец name - текущая функция
    let name = call_fn(name); // мы отдали владение name другой функции
    // и получили его обратно как результат
    println!("my name = {name}"); // тут будет все ок
}

fn call_fn(name: String) -> String {
    println!("call_fn name: {name}");
    name // это просто return без return
}

Или мы можем одолжить (borrow) - то есть передать не владение, а ссылку на владение. И тогда тоже будет можно.

fn main() {
    let name = "name".to_owned(); // владелец name - текущая функция
    call_fn(&name); // мы одолжили нашу name другой функции
    println!("my name = {name}"); // тут все хорошо
}

fn call_fn(name: &String) { // call_fn не владеет name
    println!("call_fn name: {name}");
}

Или мы можем передать копию - и тогда наша переменная останется при нас.

fn main() {
    let name = "name".to_owned(); // владелец name - текущая функция
    call_fn(name.clone()); // мы отдали владение копией
    println!("my name = {name}"); // тут будет всё ок
}

fn call_fn(name: String) { // call_fn владеет своей копией name
    println!("call_fn name: {name}");
}
  • Работать с переменными по ссылкам неудобно - многие функции требуют владения переменной.
  • Работать с копиями - удобно, но дорого с точки зрения памяти и производительности.
  • Передавать владение туда-сюда-обратно - удобно, но требует сильно много думать и планировать все свои движения.

Кто хочет понять суть - реализуйте LinkedList на Rust.

Кит второй - Traits

Trait - это средство языка описать поведение (и ограничения) для типа.
Например, есть Send и Sync, на которые постоянно ругается компилятор при многопоточном программировании:

  • Чтоб отправить объект типа X в другой поток, он должен иметь trait Send
  • Чтоб поделиться объектом типа X с другим потоком, он должен иметь trait Sync

Как это работает?

  • Авторы библиотек для многопоточной обработки данных пишут требования: хочу Send и Sync на вход
  • Разные механизмы безопасной работы с данными (Mutex, например) говорят: я Send или я Sync
  • Программист вынужден использовать эти механизмы, иначе у него проект не компилируется
  • Data race не происходит

Можно ли это обойти?

Да, можно, но это надо прям специально хотеть. Не получится случайно передать в другой поток
недействительную ссылку.

Поначалу бесит, потом становится проще жить: если код скомпилировался, там data race происходить не будет.

Кит третий - async/await/Future

То, зачем 99% программистов используют этот язык - писать асинхронный код. Позволяет выжимать 100500 соединений на
одном ядре и делать blazing fast™.
Почти полностью состоит из бессонных ночей и ошибок компиляции из-за того,
что type X doesn't implement Send/Sync/Unpin и 'a should outlive 'static.

Писать асинхронный код так, чтоб не делать clone() всего - это действительно очень сложно.

Но, к счастью, почти всё уже написано за нас. Мы говорим асинхронный код - и подразумеваем tokio.
Так что, если упростить, то вот так можно написать echo сервер, который держит > 50 000 rps:

use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::spawn;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let listener = TcpListener::bind(("localhost", 1234)).await?;
    loop {
        let (mut client, addr) = listener.accept().await?;
        println!("[+] Incoming connection from {addr}");
        spawn(async move {
            let mut data = Vec::with_capacity(256);
            loop {
                let x = client.read_u8().await?;
                data.push(x);
                if x == b'\n' || x == b'\r' || data.len() == 256 {
                    client.write_all(&data).await?;
                    break;
                }
            }
            Ok::<(), anyhow::Error>(())
        });
    }
}

Прогресс вкатывания

Сначала всё очень сложно. Потом я написал первый LinkedList.
Потом я написал свой currrrl, который быстрее актуальной сборки curl под мой
дистрибутив.
Быстрее аж за 20-50 ms, но быстрее же!
И поддерживает все опции, которые даёт copy as cURL в Dev Mode моего браузера.

А потом у меня появился повод вкатиться по-настоящему, мы начали проект, под стек которого идеально ложится именно
Rust - надо быстро, безопасно и чтоб сначала думали, а потом пушили в master.

Итоги

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

А на самом деле

Да правда неплохо всё, хотя я понял кое-что про себя. Но это уже совсем другая история.

А если я тоже хочу?

  • Learn Rust by Building Real Applications курс для тех, кто любит курсы
  • Rust for Rusteans - настольная книга для тех, кто любит настольные книги
  • Lets get rusty - канал на ютубе для тех, кто любит каналы на ютубе
  • Rust by Example - официальная дока для тех, кто (ну вы поняли)
  • // TODO: add more examples from comments

Outro

Кто первый напишет, что vas3k надо переписать на Rust - тот настоящий джедай!

63 комментария 👇

Рекомендую курс по Rust от Алексея Кладова

Автор когда-то поддержку Rust для IDE от JetBrains. Сейчас он делает rust-analyzer с которым все становится лучше.

Еще с изучением Rust мне очень помог Github Copilot и книжка Rust in Action 🙂

@r13v, класс, запинил :)

  Развернуть 1 комментарий

Vas3k надо переписать на haskell

  Развернуть 1 комментарий

@urtow, одобрямс

  Развернуть 1 комментарий
🕵️ Юзер скрыл свои комментарии от публичного просмотра...

Пробовал ли ты чужой код поддерживать на расте? Неопытным глазом выглядит очень write-only

  Развернуть 1 комментарий

@Stenopolz, да, собственно, я понял, что вкатился, когда начал в чужой код вносить правки и находить там баги глазами.

  Развернуть 1 комментарий

@annmuor, мне сейчас код на расте кажется роднее и понятнее чем на пайтоне. Когда вижу пайтон хочется добавить ресалт, конвеерну обработку, энам...

  Развернуть 1 комментарий

Кстати, ты получил одобрение Rust™ Foundation на использование Rust™?

  Развернуть 1 комментарий

@fCCbp7RUf09pxYkr, они там откатились немножко :)

  Развернуть 1 комментарий

@annmuor, все ещё жду когда язык переименуют в awslang

  Развернуть 1 комментарий

Имхо rust сейчас язык #2 для data engineers.
Валидация схем? Pydantic v2.
Есть pandas, но на одой машине скучно? Давайте заменим его на Ray. У Ray dataset бекенд на polars.
А может без этого всего? datafusion.
Мне кажется я ждал когда Rust полетит последние 5 лет и сейчас вкатываться уже прям пора. ТС спасибо и лучи поддержки.

  Развернуть 1 комментарий

Rust уже не модно, переходите на Zig

  Развернуть 1 комментарий

@fCCbp7RUf09pxYkr, совршенно разные языки в своей цели. Цель Rust - сделать ошибки по памяти невероятными в safe rust, цель zig - писать код как будто ты пишешь на Си в 1989 :)

  Развернуть 1 комментарий
🕵️ Юзер скрыл свои комментарии от публичного просмотра...

@annmuor,

как будто ты пишешь на Си в 1989

Дайте два

  Развернуть 1 комментарий

😱 Комментарий удален его автором...

  Развернуть 1 комментарий
🕵️ Юзер скрыл свои комментарии от публичного просмотра...

call_fn(&name); // мы одолжили нашу name другой функции

Ооо, родимый perl

  Развернуть 1 комментарий

@glader, Perl это немного дедушка Rust. Rust частично вдохновлялся Ruby, ну а автор Ruby вдохновлялся Perl.

  Развернуть 1 комментарий

@glader, а что тут от Перла?

  Развернуть 1 комментарий
🕵️ Юзер скрыл свои комментарии от публичного просмотра...
🕵️ Юзер скрыл свои комментарии от публичного просмотра...

еще из интересного, если начали присматриваться в сторону раста

  Развернуть 1 комментарий
Kirill Chuprov СТО домашнего розлива 10 июля 2023

Интересно а где сейчас можно именно применять раст? Вакансии для него крайне редки, писать на нём пет-проджекты не кажется оправданным, оверхед, обычно есть инструменты которые позволяют сделать твою идею намного проще. Реально ли сейчас где-то найти позицию джуна на расте за еду?)

  Развернуть 1 комментарий
🕵️ Юзер скрыл свои комментарии от публичного просмотра...

@Crypto_mate, не уверен за джуна, Rust требует очень много подготовительных знаний, чтоб на нём писать. Про порог входа не шутка же. То есть в идеале в Rust заходить, когда ты уже поел программирования и тебе не нужно думать о базовых паттернах, типах данных и всяком таком.

Если мы говорим про переходящих сеньёров - да, таких вакансий я вижу по 5 штук в день, типа "мы пишем на Расте, нам нужен раст сеньер или сеньер на C++/Go/Java, на Раст переучим". Но перешедший сеньёр джуном и не будет, в общем-то :)

На рынке есть две ветви, где Раст востребован:

  • Крипта, web3.0, NFT и вот это всё ( потому, что безопасно и мамкины хакеры тебе буффер оверфлоу не сделают )
  • Телекоммуникации и всё такое ( потому что безопасно и очень быстро, 100500 тыщ миллионов коннектов в секунду )

Весь MAANG нанимает Rust'оводов именно за второе, поэтому джуна там не ждут, но переучить переучат.

А пет проекты - на самом деле отличный вариант. Я вот тут сел и написал за ~30 минут резолвер на стотыщ RPS чтоб проверять кросс-валидность всех зон которые мы используем.
Вот такие мелкие проекты отлично учат писать код, их можно делать в рамках работы, они затаскиваются в одно лицо и приносят всем пользу.

  Развернуть 1 комментарий
Dmitriy Rozhkov Distinguished Notion API Consultant 6 августа 2023

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

  Развернуть 1 комментарий

Можно ли это обойти?

а зачем это обходить? выглядит как разумный набор правил

  Развернуть 1 комментарий

@shrddr, иногда надо, когда сам пишешь такие библиотеки, например.
Но чаще не надо.

  Развернуть 1 комментарий

Пример про borrow без перехода на &str не полон, всё же!

  Развернуть 1 комментарий

@ldvsoft, не хотелось грузить магией as_ref / into

  Развернуть 1 комментарий

@annmuor, Ну всё же канонически строчки передают в двух вариантах: String и &str ;)

  Развернуть 1 комментарий

@ldvsoft, да, но чтоб объяснить, как из &String получается &str надо прям постараться.

  Развернуть 1 комментарий

Там где-то потерялись между китами алгебраические типы и паттерн метчинг.

А в целом - да, если разобрался с кодом - чужой читать легче. Начал с контрибьюта в datafusion, вообще проблем с чтением чужого кода нет.

  Развернуть 1 комментарий

😎

Автор поста открыл его для большого интернета, но комментирование и движухи доступны только участникам Клуба

Что вообще здесь происходит?


Войти  или  Вступить в Клуб