Comments 39
Прежде, чем советовать напропалую заменять for на for_each, лучше бы документацию к этой функции почитали, там про идеоматичность упомянуто.
И да, про идеоматичный раст лучше здесь почитать: https://rust-unofficial.github.io/patterns/idioms/index.html
угу, особенно что там написано что for более идеоматичен в широком смысле а for_each просто более удобен в случае длинных цепочек и иногда просто быстрее работает:
"This is equivalent to using a for
loop on the iterator, although break
and continue
are not possible from a closure. It’s generally more idiomatic to use a for
loop, but for_each
may be more legible when processing items at the end of longer iterator chains. In some cases for_each
may also be faster than a loop, because it will use internal iteration on adapters like Chain
."
В статье какой-то сборник неоднозначных советов:
почему рекурсия под заголовком "выражения"?
тут нет ни отдельной струкруры для строителя, ни необходимости в методе build (который ничего не делает), это вообще не паттерн Строитель
раст позволяет использовать строку как тип ошибки, но они не реализуют автоматически трейт Ошибка, так что будут проблемы при использовании dyn Err. Обычно тут используют структуру, перечисление либо тип ошибки из std.
generics... ок, хорошо, это основные знания
не сказано, что такой код нагорожен ради ускорения чтобы убрать проверку выхода за границы. Но так код никто не напишет же, это совершенно надуманный пример. Для того чтобы показать unsafe нужно брать какой-нибудь параллелизм, или что-то низкоуровневое.
(в слезах) Ну почему опять рекурсия на примере факториала? Да ещё не в виде хвостовой рекурсии?
Прежде, чем такое писать – надо твёрдо знать:
Умеет ли используемый вами компилятор TCO (tail call optimization)?
Гарантируется ли TCO стандартом языка?
Преобразует ли компилятор подобный код без хвостовой рекурсии (последний вызов – не рекурсивный, а умножение на n) в нужный вид, чтобы затем применить TCO?
Знают ли другие пользователи языка ответы на пункты 1-3?
Вы предлагаете делать код "выразительным и компактным" и "чистым и функциональным". А зачем, вы можете объяснить?
Expressions в большинстве случаев сложнее читать, чем императивный стиль. С кошмарными однострочниками из итераторов я тоже уже навозился, спасибо. А плюсов, кроме "так теперь правильно", я в описанных подходах не вижу.
Expressions в большинстве случаев сложнее читать, чем императивный стиль
Мне кажется, вы путаете "сложно" и "незнакомо". Без опыта в погромировании сложно читать императивный стиль. Без опыта в ООП сложно читать код на классах. Без опыта в ФП сложно читать код на функциях высшего порядка и монадах. И так далее.
У меня правило простое, если внутри итератора ожидаются всякие break, return, bail!, то я пишу цикл. Если ничего такого, пишу итераторы, если ради итераторов приходится начинать городить flat_map чтобы внутри как-то разруливать contol flow, читать это может быть сложно.
Но думаю, что основная засада с итераторами знать поведение некоторых функций при работе с Result и Option.
Когда как)) Заметил за собой что много использую try_fold, try_for_each
. Очень полезные и компактные адаптеры, вместо цикла for.
Правда нужно организовать код так, чтобы запихать в try_fold, try_for_each
только односложные замыкания или просто имена функций
Rust - очень странный язык для извращенцев. Такое впечатление я получил после нескольких статей о нем. Конечно, у него есть много преимуществ, которыми болеет C, однако стоят ли они того комфорта и выразительности.
Да простят меня фанаты Rust, но я категорически не понимаю почему настолько намудрили, а красивый синтаксис так и не придумали. Выглядит как мертво-рожденный язык.
В любом случае, спасибо за статью. Но она не добавила ни копейки (цента) в желание хотя бы попробовать язык. Натерпелся с Ruby в свое время, хотя это азиатское изобретение — действительно полюбилось.
Здравствуйте, я извращенец уже несколько лет и хотел бы знать, что вас не устраивает в синтаксисе и концепциях языка
Как бонус, прошу привести пример языка с хорошим синиаксисом и помечаниями, почему он хорош, спасибо
Добавлю, пример с print_value не корректен, не скомпилируется без fn print_value<T: std::fmt::Display>(value: T)
Почему-то про «?» ничего не написали, про trait From, про много чего… идиоматический Rust - это когда автор кода хорошо знает Rust. Если человек только начал писать на Rust, идиоматически не выйдет… но, опыт сын ошибок, как говорится…
Еще напрашивается про .map(f)
для tuple-like объектов с одним параметром и f(x)
вместо `.map(|x| f(x))`. Да и вообще, `cargo clippy` научит.
Билдеры легче через крейт derive_builder делать, а не руками.
Учу раст после питона.
Спасибо за статью
А вот пример из реальной жизни. По условию задачи нужна некая выборка из БД по набору условий. Для каждого выбранного элемента (набор данных) нужно произвести некоторое действие (конкретно - сформировать сообщение в очередь + добавить запись в таблицу что сообщение сформировано и отправлено).
Если следовать описанной выше идеологии RUST, то сначала пишем выражение (expression), которое делает выборку и возвращает вектор с набором отобранных данных.
Затем для этого вектора используем итератор в котором уже выполняем все необходимые действия для каждого элемента.
Я правильно понял?
Извините, но это немножко бред. Концептуально, но не эффективно.
Итератор - это только снаружи красиво. А внутри - тот же самый цикл. Тут на ум приходит цитата Фаины Раневской:
Даже под самым пафосным хвостом павлина, всегда скрывается обыкновенная куриная жопа. Так что меньше пафоса, господа.
Подход RUST концептуален, но даст нам два цикла - первый в выражении (выборка и занесение ее результатов вектор), второй - в итераторе.
Плюс мы не знаем объем выборки - туда может отобраться миллионы элементов. А это расход памяти + накладные затраты на ее динамическое выделение и потом освобождение.
Куда проще сделать "не по RUST-идеоматике" - один цикл. Получили очередной элемент выборки - выполнили все необходимые действия, перешли к следующему элементу. Да, тут не будет ни "выражения", ни "итератора". Будет один цикл в котором все выполняется за один проход. Без лишних затрат памяти (достаточно статического блока под один элемент выборки). Это будет и быстрее и экономнее.
И да. Все это можно оформить как "специфический итератор". Но ценой написания лишнего кода (вся эта формализация, обертки и прочее).
Тут еще одна цитата на ум приходит
Ах, не будьте так серьезны. Серьезное лицо – это еще не признак ума, господа. Все глупости на земле делаются именно с этим выражением лица.
У меня есть подозрение, что цепочка итераторов в Rust будет одним циклом. Т.е. в вашем случае количество элементов в памяти будет ограниченным (может какой-то из операторов будет кэшировать несколько будущих элементов).
Я не уверен, правда. Знатоки языка, что скажете?
Вот весь вопрос в том - "а как оно будет на самом деле?". А как компилятору вступить в голову, так оно и будет.
Беда в том, что мне точно нужно знать как оно будет. Потому что задача не просто в том, чтобы написать красивый внешне код, который что-то делает (а потом на этапе нагрузочного тестирования по PEX-статистике увидеть что он не эффективен и думать как и что там надо переделать), но в том, чтобы написать сразу гарантированно максимально эффективный код. Путь и в ущерб "концептуальности".
Где гарантия что концептуальный с виду код
Vector<T> vec = selectData();
vec.for-each(x, processData(x));
выполнится в одном цикле? В отличии от
exec sql declare selDataCur cursor for ....;
exec sql open selDataCur;
dou sqlCode <> 0;
exec sql fetch selDataCur into :Data;
processData(Data);
enddo;
Вместо скуля тут может быть какая-то функция типа getNextData(Data)
Это кусочек из вполне реальной задачи. Схематично.
Гарантия обычно в исходниках Vector
Это понятно. Но каждый раз копаться в исходниках? Есть задача, есть сроки ее выполнения.
Я же не к тому плохой RUST или хороший. Наверное хороший для каких-то вещей.
Просто пример того, как излишняя "концептуальность" и лишние уровни абстракции могут замедлить выполнение поставленной бизнес-задачи.
По поводу того, что современное ПО становится все тяжелее и тяжелее не сильно прибавляя в функциональности уже только ленивый не говорил. И одна из причин - вот такие вот "концепции". Мало кто ставить эффективность выполнения во главу угла. Мало кто занимается профайлингом и под микроскопом рассматривает узкие места и думает о том, "как бы вот от лишнего цикла избавится"... Основная забота "чтобы код выглядел красиво". Но конечному пользователю все равно как он выглядит. Он его не видит и не увидит никогда. Ему главное чтобы быстро работало и памяти поменьше требовало. Это первично должно быть для разработчика (как мне всегда казалось). А уж чтобы при этом еще и код был "хороший" - это отдельное искусство, но это уже во-вторых.
Итератор - это trait Iterator
, выдающий next()
. Комбинации итераторов работают, внезапно, как итераторы, поэтому все зависит от того, как устроена цепочка, это может быть хоть np-полный перебор, хоть линейный цикл, хоть O(1).
Итератор - это способ выдачи доступа к последовательности чего-то. Его реализация определяет эффективность, это не высечено в камне.
Итераторы ленивые, так что тут всё хорошо. Правда не очень понял пример выше: если мы можем всё записать в виде цепочки итераторов, то тут проблем не будет, если конечно посредине не делать collect
, но в этом нет никакого смысла. Если же мы из базы получили данные в виде вектора, то тут тоже без разницы — обрабатывать их циклом или нет.
Кажется, что Вы на пустом месте соорудили чудовищие Франкенштейна на основе своих заблуждений. Как раз для работы с БД, итератор - идеальное решение. И по факту будет что-то типа:
for r in db.query("SELECT * FROM names").max_rows(10).limit(50).run()? {
handle(r)
}
/// или так
db.query("SELECT * FROM names").max_rows(10).limit(50).run()?.for_each(handle)
И это вообще не про выражения. Я думаю, что автор имел ввиду, что многие конструкции в Rust работают как выражения, например,
let x = if true {
true
} else {
false
};
let x = match a {
1 => "1",
2 => "2",
3 => "3"
}
// вместо
let mut x = false;
if true {
x = true;
}
Вот и вся история. Вы же что-то на пустом месте разнервничались. Еще и афоризмы решили себе в помощь призвать для авторитетности.
Мне кажется, что Вы путаете эффективность со способом оформления кода. Вы можете писать как очень идиоматический код неэффективно, так и совсем не идиоматический.
Ну и отдельно, я считаю, что автор сам начинающий Rust разработчик, который делится своими открытиями. При всем уважении, на эксперта он не тянет. Поэтому не надо нервничать. Дайте человеку позаблуждаться в свое удовольствие.
db.query("SELECT * FROM names").max_rows(10).limit(50).run()?.for_each(handle)
Ну я такой вариант предусматривал. Да, это хорошо. За одним маленьким "но"
Запрос на 99% будет параметрическим. Т.е. содержать ссылки на хост-переменные. Т.е.не
SELECT * FROM names
но (например)
SELECT * FROM names where name like :hNam and age > :hAge
где hNam и hAge - переменные, которые в общем случае при каждом вызове имеют разные значения.
В нашей системе это будет т.н. "статический" SQL - план запроса строится на этапе компиляции, в рантайме будут только открытие-закрытие курсора (можно и динамический SQL использовать - там строка формируется "на лету" и план запроса в рантайме, но это сильно в минус по эффективности).
Есть ли в RUST такие тонкости? Как все это работает?
А rust-то при чем? Изучайте крейты доступа к СУБД.
Зависит от того что используется для подключения к бд. Например, SQLx можно просить проверить запросы во время компиляции. Он в этом случае просто вызывает бд и просит её рассказать о запросе. Если запрос некорректный, код не скомпилируется. Если столбец - строка, а ты пробуешь записать значение в число, код не скомпилируется. Если возвращается INT NULL, а ты пишешь в i32 вместо option, код не скомпилируется. При этом sqlx -не orm. Пишешь обычный sql, а не тратишь время на выяснение какие методы как строят sql.
Есть
let mut stmt = self.conn.prepare("SELECT id, title, created, short_text, markdown \
FROM post INNER JOIN post_tag ON post_tag.post_id = post.id
WHERE is_public = 1 AND post_tag.tag = ?3 ORDER BY created DESC LIMIT ?1 OFFSET ?2")?;
let files = stmt.query_map(
[limit.to_string(), offset.to_string(), tag],
Sqlite::map_small_post_row,
)?;
files.filter_map(std::result::Result::ok).collect()
Чтобы писать программы на Лиспе, гораздо лучше подходит язык Лисп.
fn factorial(n: u32) -> u32 {
if n == 0 {
return 1;
}
n * factorial(n - 1)
}
Тут ничего необычного, лучше избавляться от вложений, если это возможно (хвостовые рекурсии пока в расчет не берем).
Это связано с тем, что исключения могут привести к утечкам памяти и другим проблемам. Кроме того, исключения могут быть трудными для понимания и отладки.
Чо
Вместо исключений Rust использует два типа ошибок:
Option - возвращает значение типа T, если оно доступно, или None, если оно недоступно.
Чо. С каких пор Option
является типом ошибки?
Между Eq
и PartialEq
на самом деле есть разница. Если интересно, это обсуждали, например, здесь: https://users.rust-lang.org/t/what-is-the-difference-between-eq-and-partialeq/15751/11
Один из способов неправильного использования generics - это пытаться написать код, который работает для всех типов данных. Это может привести к коду, который сложно поддерживать и расширять.
Может, я не прав, но это в корне не верно. В данных примерах мы используем примитивы, но если представить, что у нас более сложные типы, то намного лучше предоставить реализацию по умолчанию с T: Trait1 + Trait2
, а там, где необходимо предоставить другие трейты или типы. Таким образом, например, реализованы типы из std (Box
, например).
чтобы обойти систему типов и безопасности
Ключевое слово unsafe
ничего не выключает, чтобы называть это обходом. Например, мы можем сделать каст &T => *mut T
, но обращение к нему все еще может быть только в unsafe
блоке.
В остальном все уже было сказано и сказано оно было хорошо, так что добавить особо нечего.
Чем больше мы погружаемся, тем меньше следим за кислородом. Нужно быть осторожным...
Разделение реализаций
...
Этот код позволяет нам написать код, который работает как для координат с плавающей запятой, так и для координат с целыми числами. Если мы захотим добавить новую реализацию Point для другого типа данных, нам просто нужно добавить новый модуль.
Возможно это в каком-то случае было бы правильно, но в приведённом примере код для f32 и i32 идентичный и выглядит просто как нарушение принципа DRY. И вообще сложно вообразить, для какого же типа distance_to будет выглядеть иначе.
Всегда поражало как люди, которые ничего, по факту, не понимают в настоящем программировании пишут всякие статьи на тему «как лучше». Основное правило программирования - код должен быть понятен даже идиоту и даже идиоты должны быть способны его поддерживать.
Все эти for_each, как, кстати, и написано в документации должны УЛУЧШАТЬ читаемость кода, а не использоваться только из-за идиоматичности.
Если не улучшают - нафиг. Все примеры высосаны из пальца, рекурсия вообще не рекомендуется по множеству причин.
В реальных рабочих задачах вас любой лид бы заставил все переписывать и не выпендриваться больше. А дублирование кода в Point это нормально вообще? Хоть бы примеры подобрали адекватные.
Комбинация итераторов могут работать (и работают на практике) быстрее обычных циклов for, по следующим причинам:
Комбинаторы используют замыкания. В случае, когда код замыкания является чистой функцией, компилятору проще вычислить отсутствие зависимостей данных между итерациями, соответственно можно лучше векторизовать циклы.
При рефакторинге тела цикла в замыкание приходится приводить его к одному из стандартных комбинаторов (map/fold/reduce/filter), что также способствует уменьшению зависимостей между данными и ветвлениями. В тяжелых случаях тело цикла приводится не к одному комбинатору, а к нескольким, что тоже способствует упрощению кода. Упрощенный код легче читать человеку, и проще оптимизировать компилятору.
Итераторы в расте могут передавать по цепочке т.н. size_hint(), которые реализованы для частых случаев - типа итератора по слайсам. Эта информация тоже может учитываться компилятором для построения более эффективного кода.
Пример ускорения: https://ipthomas.com/blog/2023/07/n-times-faster-than-c-where-n-128/
Идиоматический код на Rust для тех, кто перешел с других языков программирования