Внимание
В посте будет код. На 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 - тот настоящий джедай!
Рекомендую курс по Rust от Алексея Кладова
Автор когда-то поддержку Rust для IDE от JetBrains. Сейчас он делает rust-analyzer с которым все становится лучше.
Еще с изучением Rust мне очень помог Github Copilot и книжка Rust in Action 🙂
Vas3k надо переписать на haskell
Пробовал ли ты чужой код поддерживать на расте? Неопытным глазом выглядит очень write-only
Кстати, ты получил одобрение Rust™ Foundation на использование Rust™?
Имхо rust сейчас язык #2 для data engineers.
Валидация схем? Pydantic v2.
Есть pandas, но на одой машине скучно? Давайте заменим его на Ray. У Ray dataset бекенд на polars.
А может без этого всего? datafusion.
Мне кажется я ждал когда Rust полетит последние 5 лет и сейчас вкатываться уже прям пора. ТС спасибо и лучи поддержки.
Rust уже не модно, переходите на Zig
😱 Комментарий удален его автором...
Ооо, родимый perl
А можно раскрыть мысль? Пока субъективно выглядит как то что я могу добавить к чему угодно нужный трейт, а затем передавать это "что-то" куда угодно. При этом сам объект может быть небезопасным для использования из разных тредов. Т.е. нужно просто не добавлять трейт пока не реализуешь какие-то функции для работы из разных потоков. А если у тебя в них будет ошибка, то получим всё тот же race condition.
Есть ошибка в моих рассуждениях?
еще из интересного, если начали присматриваться в сторону раста
Интересно а где сейчас можно именно применять раст? Вакансии для него крайне редки, писать на нём пет-проджекты не кажется оправданным, оверхед, обычно есть инструменты которые позволяют сделать твою идею намного проще. Реально ли сейчас где-то найти позицию джуна на расте за еду?)
Я думаю, что четвертый кит — умные указатели. С одной стороны тебе придется с ними разобраться разбираясь с владением. С другой — понять эти коробочки отдельно помогает понять что вообще происходит.
а зачем это обходить? выглядит как разумный набор правил
Пример про borrow без перехода на
&str
не полон, всё же!Там где-то потерялись между китами алгебраические типы и паттерн метчинг.
А в целом - да, если разобрался с кодом - чужой читать легче. Начал с контрибьюта в datafusion, вообще проблем с чтением чужого кода нет.