Как стать автором
Обновить

Комментарии 52

1.2 — Параллельная кодогенерация: уменьшение времени компиляции всегда являлось главной темой в каждом выпуске Rust, и сейчас трудно представить, что когда-то был короткий период времени, когда Rust вообще не имел параллельной кодогенерации.

Ну, лично у меня этот период и не заканчивался :)


[profile.release]
lto = true
codegen-units = 1

А вообще, с тех пор большой прогресс был сделан. Надеюсь, все же после того как домучают асинк-авейт наконец посмотрят в сторону GAT/Chalk, доделают импл трейты, ну и вообще начнут наконец делать удобнее не одну только асинхронность.

А в Токио есть ржавый дизель.

Какая-то дикость, «Шаблоны срезов», «менеджер инструментальных средств», «объект первого класса»… а чего crates как «ящики» не перевели?
Задача перевода же вроде — сделать текст более понятным.

Переводы раньше: потратить несколько дней в окружении словарей и справочников, чтобы по сути переписать статью заново на своём языке.


Переводы сейчас: скопировать текст в DeepL, скопировать выхлоп на сайт.

НЛО прилетело и опубликовало эту надпись здесь
Короче и по месту

try!(try!(try!(foo()).bar()).baz()) vs foo()?.bar()?.baz()? — согласитесь, второй проще писать и читать.
Ещё пример, код с прокидыванием ошибок выглядит так будто их нет:
diff
Главная цель, чтобы обработку ошибок было делать проще чем их игнорировать.

Главная цель, чтобы обработку ошибок было делать проще чем их игнорировать.
так и до исключений дойдут

Исключения как раз очень удобно игнорировать (для этого вообще ничего не нужно делать) и довольно громоздко обрабатывать.

Вот тут мне интересно. В дотнете, например они по умолчанию стандартными обработчиком — выводятся пользователю.


В моей практике в большинстве ситуаций с исключениями ничего не надо делать особенного — просто передать на уровень абстракции выше. На каком-то уровне они записываются в лог или показываются пользователю или заворачиваются в исключение более высокого уровня.


Очень редко нужно делать что-то кроме этих нескольких вариантов (например сгенерировать дамп при oom, retry при разных ситуациях с сетью и блокировках).


Т.е. умолчания с моей точки зрения позволяют не засорять код монотонным повторением "здесь все как и везде".


А как у вас? Какую особенную обработку исключений вы делаете?

Простой пример, возьмём вызов goto.bar()
Внимание вопрос, кидает ли этот метод исключения, а если да то какие когда и как?
Проблема в том, что он может кидать их как угодно — например крайне редко, или в зависимости от окружения итд. Обычно код исключений не кидает, разработчик запускает код и вроде как всё работает, но предположим что метод не умеет ходить в бар на сетевом диске и приложение начинает крашиться только у заказчика.
Конечно документация и всё такое, но положа руку на сердце — кто её внимательно читает? А при обновлении версии библиотеки кто будет её читать и делать полный код ревью что новый код работает корректно?
Скомпилировалось? Тесты прошли? Релизим.
Другой минус исключений в том что они не бесплатные. В том плане что это не просто накладные расходы на обслуживание разкрутки стека, но ещё и произвольный рантайм оверхед. Для .net это допустим не проблема, но для Rust это проблема, не завозить же рантайм из за этого. И да, в С++ эту проблему не решили, там есть исключения и по сути они тащат с собой мини рантайм для раскрутки стека, со всякими оптимизациями и ухищрениями (например хороший вопрос как кинуть исключение о том что кончилась память если для раскрутки стека нужно выделить память :D ).
Подход Rust больше ориентирован на системное программирование, что хорошо укладывается в парадигму безопасного языка с поведением предсказанным во время компиляции.
А разве паника не вид исключения и не имеет тех же накладных расходов?
Вопрос без подвоха, мне действительно интересно с точки зрения познания.

Я бы сказал, что перехватывание паники это редкая ситуация (а при некоторых ключах компиляции — невозможная), а перехватывание эксепшнов один из основных флоу использования.

Это кстати отличный вопрос. Согласно доке на раст есть 4 уровня исключений
— всё вполне может пойти не так = Optional
— всё обычно ок = Result
— поток накрылся — panic()
— всё совсем плохо — abort()

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

Но это к сожалению всё что даёт документация, если смотреть на простые примеры то там раст и С++ почти одинаковы (раст на ошибки аллокации памяти вызывает abort что логично и даже возможно когда-нибудь будет что-то аналогичное в С++ )
увы, нулём они быть не могут

При статической линковке и настройке panic = abort могут.

Наличие триллиона TryXXX методов показывают, что всё не так просто. Почему Parse не возвращает Option<T>, а бросает исключение? Почему нет способа получить значение из словаря которого там может не быть (кроме того же неуклюжего TryGetValue)?


Короче нет, эксепшны это хорошо когда их не надо обрабатывать, но обычно надо обрабатывать или нет знает только код выше.


Т.е. умолчания с моей точки зрения позволяют не засорять код монотонным повторением "здесь все как и везде".

Это не монотонное засорение. Так, например, если функция раньше всегда возвращала знчение а теперь может упасть, это ломающее изменение. И если у вас там какие-нибудь CommitTransaction() за вызовом этой функции были то очень желательно учесть, что транзакции теперь иногда могут отменяться.

Наличие триллиона TryXXX методов показывают, что всё не так просто. Почему Parse не возвращает Option, а бросает исключение? Почему нет способа получить значение из словаря которого там может не быть (кроме того же неуклюжего TryGetValue)?

Возможно, потому, что nullable появились не сразу.


Короче нет, эксепшны это хорошо когда их не надо обрабатывать, но обычно надо обрабатывать или нет знает только код выше.

Поэтому оно обычно просто передается наверх до того кода, который знает как его обрабатывать.


Это не монотонное засорение. Так, например, если функция раньше всегда возвращала знчение а теперь может упасть, это ломающее изменение.

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


И если у вас там какие-нибудь CommitTransaction() за вызовом этой функции были то очень желательно учесть, что транзакции теперь иногда могут отменяться.

Тут см выше. Транзакции, наверное, бывают разные но в РСУБД, практически все внутри транзакции может упасть.


В-общем, насколько я понял, из-за упора на системность и быстродействие "исключения везде" для раста не подходят, потму, что это не zero cost abstraction.


Я бы тогда согласился с предложением, как здесь сказали, do-нотации, чтобы хотя бы вопросики ставить не на каждуй строчку, а на блок.


Что вы об этом думаете?

Возможно, потому, что nullable появились не сразу.

Nullable не хватит, ведь эксепшн несет в себе информацию об ошибке. Чтобы получить не меньше информации нужен по крайней мере Either, или как он в расте называется — резалт.


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

Ну, в прикладных поменьше надо, да.


Тут см выше. Транзакции, наверное, бывают разные но в РСУБД, практически все внутри транзакции может упасть.

Имеется в виду, что раньше в коде было CommitTransaction() и тарнзакция коммитилась, а теперь там эксепшн падает и до него выполнение не доходит.


В-общем, насколько я понял, из-за упора на системность и быстродействие "исключения везде" для раста не подходят, потму, что это не zero cost abstraction.

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


Что вы об этом думаете?

Try-блоки и так хотят добавить, но они немного не так работают, как вы хотите. В целом, мне кажется явный флоу во многом упрощает понимание. Поставить вопросик — не велика беда. Зато всегда видно, где ошибка может произойти и какая. Ваш вариант еще и не будет работать, потому что тип ошибки конкретный. То есть если у нас есть грубо говоря функция которая с консоли текст читает, там возможна одна ошибка (например, io::Error). Допустим теперь мы начали парсить в этой функции что-нибудь (соответственно ParseError), так что у нас должен измениться тип ошибки функции. С неявным преобразованием и вы тут же получите непонятную ошибку "Cannot convert ParseError to io::Error", и откуда она взялась в случае с неявными флоу — ХЗ. Ошибки в боксах в расте таскать не очень принято.


Из моего недавнего кода:


let user_model: UserModel = request
    .send()
    .await
    .map_err(GenericError)?
    .json::<UserModelRaw>()
    .await
    .map_err(GenericError)?
    .into();

Функции send и json возвращают разные ошибки (ошибку HTTP и парсинга жсона соответственно), и мне нужно явно их оборачивать в общую ошибку, иначе ничего не заработает. То есть функции могут завершиться с одной ошибкой, оборачиваю я её во вторую, а возвращается вообще третий: actix_web::Error (вопросик умеет конвертацию ошибки производить если такое преобразолание задано). Так что без него как минимум в расте не обойтись никак

Nullable не хватит

Мы говорили про методы try* — они не возвращают ничего о причине, потому, что считают ровно одну причину не исключением, а нормальным ходом выполнения.


а теперь там эксепшн падает и до него выполнение не доходит

Если там было что-то связанное с РСУБД то такой ситуации нет — там и раньше могло упасть.


"Cannot convert ParseError to io::Error"

Как в Java checked exceptions, только там, похоже, многие признают это не очень удачной идеей.

Мы говорили про методы try* — они не возвращают ничего о причине, потому, что считают ровно одну причину не исключением, а нормальным ходом выполнения.

Ну так это минус же. В расте я могу если не распарсил получить причину почему так:


use std::error::Error;

fn main() -> Result<(), Box<dyn Error>>{
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    let x: u32 = input.trim().parse()?;
    println!("{0}*{0}={1}", x, x*x);
    Ok(())
}

-24
Error: ParseIntError { kind: InvalidDigit }

Error: ParseIntError { kind: Empty }

А тут у меня либо вариант с дорогим эксепшном, либо просто "упс, нишмагла". Если тип ошибки мне не важен, я могу восстановить поведение Try-методов таким образом:


let x: Option<u32> = input.trim().parse().ok();

Но в обратную сторону оно так не работает.


Если там было что-то связанное с РСУБД то такой ситуации нет — там и раньше могло упасть.

Нет, не связанное. Например, раньше там в какой-нибудь кэш локально что-то записывалось (тут только с оомом если упадёт, а весь код всегда предполагает, что аллокации завершаются без ошибок), а тепепрь начали по сети писать, соответственно появился шанс на сетевую ошибку.


Как в Java checked exceptions, только там, похоже, многие признают это не очень удачной идеей.

Что плохая идея для джавы, то хорошая для раста. Плюс, подход к ошибкам в приложениях и библиотеках различается: в библиотеках стараются как можно более специальный тип ошибки иметь, в приложениях часто идут по пути "что-то". Нужно понимать ведь, что в расте нет рефлекшна, и сдаункастить ошибку из разряда if error is MyException ex { ... } в общем случае не выйдет. Плюс с АДТ я бы сказал что комбинировать их куда проще. Плюс макросы для облегчения бойлерплейта.

Исключения как раз очень удобно игнорировать (для этого вообще ничего не нужно делать)...
ну это уже от реализации зависит. Мы можем потребовать чтобы вызов бросающего foo из небросающего bar без обработки приводил к ошибке компиляции, например.
… и довольно громоздко обрабатывать.
мне кажется обработка исключений как минимум не более громоздкая чем почти любого другого вида ошибок

Да идельно было бы скрестить do-нотанцию из монад (т.к. она достаточно явная и удобная), только чтобы она компилировалась в исключения (т.к. они быстрее). Вообщем-то Result/? это примерно это и есть. В блоге withoutboats про это было, что надо бы сделать по аналогии с Future/async/await некий Result/сatch/?.


На сколько я понимаю С++ (а в других языках тем более) все исключения ловятся по сути через dynamic_cast, а в расте отличие в том что тип Result может быть известен в компайл тайм.


Мы можем потребовать чтобы вызов бросающего foo из небросающего bar без обработки приводил к ошибке компиляции

Тогда прийдётся аннотировать все. и получим ту же вербозность что и в раст. Сейчас исключения везде работают так, что bar абсолютно прозрачен для исключений и как бы ничего о них не знает, что удобно.

… только чтобы она компилировалась в исключения (т.к. они быстрее
ну… не совсем. Там много нюансов и в общем случае без исключений быстрее чем с ними, т.к. из-за потенциальных исключений компилятор не может проводить множество оптимизаций
… а в расте отличие в том что тип Result может быть известен в компайл тайм.
ну Result раста тоже не самый оптимальный вариант, однако чтобы сделать лучше нужны изменения ABI:
Герб Саттер (Herb Sutter) в P709 описал новый механизм передачи исключений. Идейно, функция возвращает std::expected, однако вместо отдельного дискриминатора типа bool, который вместе с выравниванием будет занимать до 8 байт на стеке, этот бит информации передаётся каким-то более быстрым способом, например, в Carry Flag.
цитата из этой статьи. По сути, можно даже далее улучшить этот механизм, генерируя под обработку ошибок отдельный landing pad (так же, как это в текущих исключениях с++), но сохраняя информацию о типе ошибки и, тем самым, избавляясь от dynamic_cast'а. В абстрактном идеальном языке это могло бы выглядеть так:
fn foo() -> ResultType or ErrorType;

Исключения быстрее только если они не происходят. А через резалты удобно производить и обычные "Ошибающиеся" вычисления, вроде упомянутого парсинга. В сишарпе как раз в угоду безопасности есть эти убогие TryXXX функции, с резалтами можно использовать один механизм. И да, ни разу не слышал чтобы у кого-то резалты тормозили, так что реализовывать через исключения то что не надо возможно и не стоит.

Исключения быстрее только если они не происходят
вы кажется проигнорировали то место где я писал что это альтернативный механизм, через который можно было бы реализовать исключения/ошибки.

Давайте такой пример: вот есть у нас тип T, не помещающийся в регистры. Тогда Result, содержащий еще флаг заполненности, будет передан через стек вместе с T. Как минимум, меняя ABI, можно сделать чтобы этот флаг передавался в отдельном регистре, и это точно будет не медленнее, чем положить его на стек в вызываемой функции, а потом прочитать со стека в вызывающей. А еще учтите что существует множество таких T, которые можно передавать в регистрах, но для которых Result будет передан через стек. Простой пример — было бы оптимальнее, если бы square2 принимал аргумент в трех регистрах, верно?

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

Ну как по мне это как раз такая вещь, можно использовать Either в качестве ошибки, а можно что-то другое.

Мы можем потребовать чтобы вызов бросающего foo из небросающего bar без обработки приводил к ошибке компиляции, например.

Если вы встанете на этот путь и пройдёте его до конца, вы получите checked exceptions, которые абсолютно эквивалентны обработке ошибок с помощью возврата алгебраических типов. Отличия чисто синтаксические, да и они минимальны.

Кстати, отличия всё-таки есть и мне кажется, что checked exceptions не такие универсальные.


Разница появляется, если есть несколько сущностей и "исключительная" ситуация не настолько исключительная, чтобы прерывать выполнение, но сообщить о ней всё-таки надо.


Например, мы пытаемся скачать список файлов:
listFiles.map(downloadFile)
Или даже так:
futures = threadPoolExecutor.executeAll(listOfTasks)
Если downloadFile или запущенная задача будет бросать checked exceptions, то нам придётся обрабатывать их прямо в месте вызова. В случае алгебраичечких типов такой проблемы нет — результат можно не распаковывать и сохранить/передать дальше.


Так что получается, что алгебраические типы данных в любом случае нужны, и если они есть — можно не усложнять язык исключениями.

Исключения являются эффектами, а возвращаемые значения — нет. Разница не только синтаксическая:


xs.iter().map(|x| f(x)).collect::<Vec<_>>()

Если f бросает исключение, то это серьезно мешает композиции функций, особенно в ленивых и асинхронных вычислениях.

Если f бросает исключение, будет что-то вроде


xs.iter().map((|x| f(x)).catch()).collect::<Vec<_>>()

Не очень-то и мешает.

Ну вот вы и сами написали ответ: чтобы достичь композируемости нужно избавиться от исключений и преобразовать их в возвращаемые значения.


В вашем примере для (гипотетической) f: Fn(A) throws E -> B по-видимому catch вернёт нечто вроде R<B> и вы получите вектор из этих зиначений: Vec<R<B>>.


А чтобы дальше работать с этими R, нужно будет научиться составлять композицию значений R<A> и Fn(A) -> R<B> для любых R, A и B (хотя R лучше бы удовлетворять трём законам).

Исключения являются эффектами, а возвращаемые значения — нет. Разница не только синтаксическая:
это если предположить что checked exceptions реализуются через тот же механизм, что и обычные. При этом вопрос делать ли исключения эффектами или реализовывать как синтаксический сахар над возвратом типа-суммы — полностью на усмотрение компилятора.

Checked exceptions такие как в джаве не изоморфны алгебраическим типам. Почему?
Попробуйте ответить на вопрос: какая сигнатура будет у функций map и filter в случае checked exceptions?

Примерно такая же, какая была бы у mapM и filterM для Result, а что?

Вы-таки отказываете функции map в параллелизме?


Плюс (раз уж мы о Хаскеле) у нас есть всегда значения типа IO a. Соответственно типы [IO a] и IO [a] довольно сильно различаются. Как будет выглядеть тип таких значений с checked exceptions, но без типа IO мне совершенно непонятно.

Ну это только один пример удобства, хотя только подталкивает все ошибки обрабатывать одинаково.
А вот второй аргумент непонятен, что try! что ? просто возвращают ошибку, разницы никакой.

Ещё ? отличается тем, что его можно вызвать на значении любого типа, реализующего Try, в частности, на Option.

Удобство и более чистый код. Особенно это видно для цепочек вызовов.


let var = try!(try!(try!(
    foo()
)
.bar(),)
.baz());

let var = foo()?
    .bar()?
    .baz()?;

Моё знакомство с Rust началось совсем недавно, узнал я о нём по большей части из дискуссий на этом прекрасном сайте, однако одного знания было мало. Именно изучать его как язык меня сподвигла библиотека orjson для рабочего проекта по Python. Мне захотелось узнать, как она выглядит изнутри и изучить язык. Потом прошёлся по тегу Rust уже с какой-то целью, которой придерживался, и, наконец, установил себе rustup.


Когда я начал писать на нём первые строчки, изучать rustbook(спасибо переводчикам! даже со знанием английского, чтение на родном языке проще), решать алгоритмические задачи просто чтобы получше изучить синтаксис и собрать побольше шишек… появилось странное ощущение удовольствия от языка. Почему-то мне он кажется компромиссом между Python и C/C++, то есть глубокие системные возможности, производительность, но при этом язык не пытается убить меня синтаксисом и показывает вменяемые ошибки, а не плюёт в меня куском памяти из моего прошлого ввода.


Желаю языку дальнейшего развития, популяризации и улучшения в той же степени, в какой себе хотя бы когда-нибудь его понять :)

Вопрос по макросам и кодогенерации в расте. Есть событие, есть подписчики. Можно ли сделать так:
0 подписчиков => удаяляем emitter(dispatcher) на этапе компиляции
1 подписчик => заменяем emitter на прямой вызов обработчика или инлайним его
2+ подписчиков => заменяем emitter на итерацию по коллекции
???

Из вашего описания трудно понять, что именно требуется. Но как вариант, вы можете создать процедурный макрос для отправки сообщений, который будет при компиляции брать информацию о подписчиках из конфигурационного файла, и генерировать разный код, в зависимости от количества подписчиков.

Унесите Царя!

Давно не следил за развитием rust. Раз язык преподносится как язык общего назначения, уже стоит смотреть в его сторону тому, кто пишет низкоуровневый софт для арм? Интересует возможность создания кода под такие платформы как nRF52811/33 от Nordic semi, например. Или пока си и асм вне конкуренции? Учитывая, что свои библиотеки nordic предоставляет на си, можно ли часть своей логики написать на расте? Имеется ли тулчейн наподобие arm-none-eabi?

это всё относится к бекенду компилятора. Если clang умеет компилировать си под эти платформы, значит умеет и раст.

Дёргать из rust библиотеки с C ABI проблемы большой нет, равно как и наоборот предоставлять C ABI наружу. А поддержка конкретной платформы — дело llvm, как выше написал Antervis.


Для ARM всё довольно неплохо и постепенно развивается. Не знаю как ситуация с Cortex-R, но Cortex-M живёт и здравствует с постепенным наращиванием объёма библиотек, улучшение hal и средств отладки. IIRC, какие-то шевеления в рамках nRF были, посмотрите https://github.com/nrf-rs/nrf-hal и соседние проекты, также https://github.com/rust-embedded/wg.


UPD: судя по devkit'у nRF52 там простой Cortex-M4F, который прекрасно поддерживается текущим компилятором rust, так что больших проблем быть не должно, если говорить про низкоуровневую часть.

Ага, большое спасибо, буду изучать.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации