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

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

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

Я знаю, кто - кто-то, кто вставил unwrap() в темные глубины 100500-того крейта, который используется в таком сервисе. /s

unwrap это не аварийное завершение, да это паника, но после нее в логах будет полный стек который позволит найти и починить проблему. А вот неожиданный SegFault в проде это совсем другая история, которая сходу обещает много дней увлекательного дебага.

А посмотреть дамп и стек что то мешает?

Из моего опыта очень часто бывает так что стек поврежден т.к. какой то код внезапно уже успел записать туда мусор. В таком случае бывает очень тяжело восстановить вообще момент возникновения проблемы

Для такого, обычно, делается core dump.

Да, сложновато бывает пару вызовов в стеке восстановить, но, в принципе, решаемая проблема, которая случаетсядовольно редко. Зачастую таки стек есть.

Вот от спецэффектов и падений где-то в glibc на ARM уже сложнее защититься, но жизнь она такая.

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

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

Санитайзеры фактически бесполезны, соглашусь.

unwrap on None в неосновном потоке не приводит к завершению программы.

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

unwrap on None в неосновном потоке не приводит к завершению программы.

Так-то и в основном потоке панику можно перехватить.


Паника безопасна.

Да, но с оговорками. Если нет ансейфа, то UB действительно не случится, но и тогда программа может оказаться в разломанном состоянии. Если код написан грамотно, то такого не случится, но в нормальном коде и анврапов особо не будет.

Вот, кстати, реально не понимаю этой любви к unwrap() у наших домашних Рустовцев. Оно же серьёзно убивает обработку ошибок.

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

Да и сама концепция использования Result/Option сбивает с толку, но кто мешает вместо unwrap() возвращать ошибку из функции?

Концепция использования Result/Option в тех языках, где они появились, достаточно проста - вы тем или иным образом прямо там, где они появились, обрабатываете ошибки.

Либо это может быть просто передача ошибки дальше, с "быстрым выходом" из цепи преобразований (т.н. "монады"), либо просто раскрытие и обработка ошибки на месте, либо замена на "правильное" значение. Но ни в Haskell, ни в Ocaml народ не использует unwrap() как "заметание ошибки под ковёр".

Так ведь unwrap, по-моему, должен бы использоваться только в ситуациях, когда уместен quick'n'dirty подход - типа прототипирования или написания небольших программок. Насколько я понимаю, идея с unwrap'ом примерно в том же духе, что и с unsafe'ом - локализация возможных проблем, так что перед релизом библиотеки или серьёзной программыможно достаточно легко прошерстить код, и убрать unwrap'ы (или проверить, что они не могут срабатывать) и проверить правильность unsafe'ов. Ну или хотя бы при возникновении ошибки достаточно быстро её найти.

Я знаю, кто — кто-то, кто вставил unwrap() в темные глубины 100500-того крейта, который используется в таком сервисе. /s

Ну если так говорить, то в чём разница с вызовом std::abort внутри сторонней библиотеки?.. По большому счёту, "внезапный unwrap" — это баг. Да, язык от всех проблем не защищает, увы.

Да, язык от всех проблем не защищает, увы.

"Можно вывести девушку из деревни..." - кмк, это проблема культурного типа. С другой стороны, возможно язык не поддерживает простоту написания длинных монадических цепочек с Result? Ну как в Хаскеле это просто блок do.

С другой стороны, возможно язык не поддерживает простоту написания длинных монадических цепочек с Result? Ну как в Хаскеле это просто блок do.

Не думаю, что дело в этом. В расте принято обмазываться вопросиками и код в таком стиле писать не сложно, но это не всегда спасает. Вот предположим, что у нас есть вектор в котором гарантированно не меньше пяти элементов. Можно везде писать get(2).and_then(…), но не хочется. Особенно если функция без этого могла бы возвращать просто значение, а не Result/Option. Ответственный человек завернёт такой код в абстракцию и напишет комментарий (и тест заодно). Принципиально эту проблему решают разве что зависимые типы, ну или я чего-то не знаю про хаскель.

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

Значит культурное. Просто во всяких Хаскелях культура такая, что там обязательно обзовут аналог unwrap именем "unsafeUnwrap". Кмк.

В Окамлах, конечно, есть свои приколы насчёт non-tailrec map, но это вроде бы уже почти побеждено (см TMC).

Не прав, есть fromJust, который аналог unwrap. Видимо его меньше используют.

Тем не менее, кошерный способ - это maybe https://hackage.haskell.org/package/base-4.16.0.0/docs/Data-Maybe.html#v:maybe

В Ocaml тоже есть get, аналогично кошерный способ - value.

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

Мне очень нравятся Хаскельные zipWith, take, drop, которые с одной стороны корректно обрабатывают все ситуации, а с другой стороны не выбрасывают исключений в отличие от OCaml'овских map2, iter2.

Просто во всяких Хаскелях культура такая, что там обязательно обзовут аналог unwrap именем "unsafeUnwrap". Кмк.

Тут два момента. Во первых, соглашения — если привыкнуть, то за unwrap/expect глаз цепляется не хуже, чем за unsafe префикс. В этом плане хуже, что паниковать могут другие методы, например, доступ по индексу через квадратные скобки. Это тоже можно порешать, но уже костылём (линтом Clippy). Во вторых, ансейф в расте обозначает конкретное подмножество проблем, а не просто "потенциально опасная операция". Паниковать или допускать утечки — "безопасно".

Кстати, ваш пример уже завернут в chunk_exact.

Некоторый аналог (достаточно близкий) - это ?-нотация.

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

Значит мой опыт Руста слишком мал. Я, в общем, и не претендую.

Кстати, по рассказам Not-Kernel-Guy'а внутри ntkrnlos используется вот тот же "монадический" подход в стиле С. То есть, делается макрос

#define CHECKED_RUN( f, errorMSG)

if( ....()) {
logError...
goto END;
}

и в каждой функции делается метка END.

Но изучать что-то новое, читать реально сложную книгу, практиковаться со сложным языком программирования — это высокая стрессовая нагрузка на нашу систему подкрепления, простите мне мой нейрофизиологический жаргон. Уже через 5-10 минут система подкрепления и все наше естество начнет вопить, что нет, я не хочу этим заниматься, у меня чугунная голова, я хочу смотреть сериалы или программировать на том, что уже знаю. Так что один-два часа в день — это надо прям собраться и выдержать.


Давайте стараться уже уходить от бихевиоризма. Он нам не объясняет, почему подкрепления у каждого свои. Да, обучение это образование новых связей в голове. Удивительная боль на самом деле, вот тут интересно поразмышлять над тем, что мы чувствуем и почему нам больно. Почему эволюция не сделала процесс обучения без боли. Это же было бы гениально.

 Почему эволюция не сделала процесс обучения без боли. Это же было бы гениально.


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

А как объясняет? Не хотелось бы пересматривать такое количество роликов.

Кинул в личку выжимку.

То есть если бы сразу написали на Java/C# то и не было бы проблем с переписыванием? Плюс найти работников на эти языки и связанные с ними фреймворками проще.

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

Rust – это пример такого в вакууме идеального и продуманного языка, который не хотят использовать из-за сложности.
Пойдет по пути Scala и Haskell.

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

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

Haskell вроде бы из-за алгебраических типов, развитого pattern matching и Show почти везде очень хорошо подходит для опердней. И статической типизации.

Лет 5 назад первый раз попытался вкатиться в раст. Не особо получилось, казалось что borrow checker многого не понимает и приходится писать очень странный код чтобы он понял что тут всё хорошо.
Недавно попробовл ещё раз. На этот раз от языка только приятные впечатления, borrow checker не мешает, если какие-то претензии у него имеются, то только по делу. А вот экосистема немного огорчила.
Сейчас в языке целых два актуальных асинхронных рантайма, не совсем совместимых друг с другом, и крейты обычно поддерживают только один из них.
Ещё мне не особо зашло логирование. После .NET грустно смотреть на отсутствие какого-нибудь LogContext который бы позволял в определенном скоупе добавлять в лог данные. Как пример, мне не удалось придумать как в Tide или Warp на уровне middleware создать некоторый request-id, чтобы различать логи от параллельных запросов.
Пока определенно продолжу его изучать, но думаю это пока ещё не язык где можно на расслабоне собрать что-то полезное без костылей.

Сейчас в языке целых два актуальных асинхронных рантайма

Три - ещё smol достоен внимания.

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

Вы имеете в виду String и &str? Это практически аналоги std::string и std::string_view соответственно из плюсов, с такими же сферами применения. У этих двух типов сферы использования пересекаются, но есть случаи когда конкретный тип нужнее. В C++ и в Rust оба типа используются постоянно.

Справедливости ради, здесь все преувеличенно.
Смело все что с u8, можно отсюда убрать, сюрприз-сюрприз, но стринги состоят из байт, фундаментально, в любом языке, даже в питоне приходится иногда туда сюда делать конвертацию. Path по сути к стринге тоже отношение прямого не имеет, по этой логике все, что работает со стрингой можно сюда добавить, а &'static str это частный случай &str. Фундаментально остается 3 вида стринги с их слайсами (slice), 2 из которых нужны для вполне конкретных вещей (внешнего взаимодействия (API)).

НЛО прилетело и опубликовало эту надпись здесь
Как пример, мне не удалось придумать как в Tide [...] на уровне middleware создать некоторый request-id

А чем не устраивают методы ext/ext_mut/set_ext?

Эх, я не до конца выразил мысль: создать так, чтобы во все вызовы логирования внутри реквеста он автоматически подставлялся без явного пробрасывания. Что-то подобное можно сделать с помощью slog_scope, но тогда необходимо использовать логгер из скоупа из-за чего теряется возможность делать локальные логгеры через slog::Logger::new c дополнительной инфой.
В общем проблема в том, что держателем дополнительных данных для логирования является только сам логгер, нет дополнительной штуки (как например в serilog) в которую можно напихать инфы и её залогирует любой логгер который будет вызван в её контексте.

То есть, по сути, вы жалуетесь на отсутствие аналога AsyncLocal?

Не столько AsyncLocal, это обходится тем же инструментированием future как в slog_scope, сколько на недостаточную [для меня] гибкость существующих логгеров.

Использую https://docs.rs/log-mdc/0.1.0/log_mdc/ вместе с log4rs. Возможно это то, что вам нужно. Добавляю что-то в контекст


use log_mdc;

log_mdc::insert("userId", login);

В паттерне прописываю {X(userId)(anon)}. И любой вызов логгера пишет эту переменную или anon.

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

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

Мне кажется, что токио всё-таки победил, но сильно спорить не буду. Хотя графики активности говорят сами за себя:
https://github.com/tokio-rs/tokio/graphs/contributors
https://github.com/async-rs/async-std/graphs/contributors


Просто из любопытства: можно пример нескольких более-менее популярных библиотек, которые не поддерживают токио?


После .NET грустно смотреть на отсутствие какого-нибудь LogContext который бы позволял в определенном скоупе добавлять в лог данные.

Кажется, что tracing решает эту задачу.

Насколько я понял ситуацию с асинхронностью, то токио существовал ещё до того как в rust завезли async/await, оттого и популярность. А async-std вроде как теперь стандарт, и я как новичек хотел бы ориентировался на него, а у него с поддержкой в библиотекой как раз грустно.


Кажется, что tracing решает эту задачу.

Ну в той же мере, что и slog_scope. Я выше уже написал что хочу чего-то более гибкого, но видимо я зажрался в этом своём дотнете :)

А async-std вроде как теперь стандарт

Почему? Только из-за наличия "std" в названии? Может, конечно, я варюсь в пузыре и сильно заблуждаюсь, но больше похоже на то, что эта библиотека умерла. Из того о чём знаю: растовая libp2p изначально использовала как async-std, но и там сделали поддержку токио. О случаях когда токио не поддерживается не слышал.


Ну и я привёл ссылки не количество коммитов, то есть "активность разработки", а не число скачиваний. Хотя и по этому параметру у async-std всё не очень (1,321,707 недавних скачиваний против 7,417,479 у токио).


Ну в той же мере, что и slog_scope.

Всё-таки чуть-чуть эргономичнее из-за атрибутов и прочих мелких удобств. (:

> А async-std вроде как теперь стандарт

Наоборот, де-факто токио всех победил, это сейчас стандарт.

Окей, буду знать, спасибо

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

Тут я, похоже, неправильно выбрал слова и во время интервью, и во время редактуры. Не "занял" как "выпинал из этой ниши всех". А "занял" как "решает задачи этой ниши лучше чем те, кто ее занимает". Наверное, надо использовать слово "вписался" :) Ядра операционных систем, инфраструктурные штуки, системные утилиты, то что называется "toolchain".

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

По собственному опыту, в проекте с ~ 1M LOC C++(userspace) + C(kernel) на backend и ~500k LOC в толстом клиенте с Java FX, переход на Kotlin 3 года назад произошел намного более гладко и незаметно, чем попытки интегрировать Rust в сушествующий продукт.

Да, у JVM огромное преимущество изначально, но даже, если не рассматривать «сложность»(скорее необычность) парадигмы написания нового кода на Rust, интеграция с существцющим отбирает невероятно много ресурсов.

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

В других случаях(опять же по личному впечатлению) интеграция — это боль и мучения.

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

Java в принципе так себе склеивается с чем-либо еще. У меня на прошлом проекте перепробовали кучу вариантов, включая JNI, MQ, и даже CORBA. В итоге победил микросервис.

Кто-то писал интересную фразу о том, что склеивать два языка с разными сборщиками мусора - мучение. А вот языки без сборщиков мусора склеиваются с чем угодно значительно легче (через тот же C ABI).

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

Именно с С, т.к. он без сборщика мусора. А вот склеить Питон и С# уже вроде бы непросто.

Даже в пределах одной ВМ (Java + Kotlin, Java + Scala) проскакивают спецэффекты. А пытаться дружить разными рантаймами - и вовсе особый вид страдания.

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

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

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

Зачем патчить? Микросервис делаем на расте и всех делов ?

А зачем что-то патчить или форкать, если в Ruby расширения в виде разделяемых библиотек — это стандартный и хорошо документированный путь? Не говоря уже о микросервисах.

Не думаю, что Rust потеснит Go. C/C++ - да, всё к тому идёт. Но в случае Go рантайм и сборщик мусора не создают в большинстве типичных для Go приложений таких заметных проблем, чтобы их решение окупалось переходом на значительно более сложный Rust. Просто есть ниши, которые Go из-за рантайма и сборщика мусора занять не смог, а в них тоже было пора прийти более современному языку - и пришёл Rust. Единственный тип приложений, где Rust используется наравне (ну, скорее ещё не наравне, но к тому понемногу идёт) с Go в последнее время - это консольные утилиты. Но здесь причина не в преимуществах Rust, а просто в том, что оба языка неплохо подходят, и разработчики выбирают привычный язык.

Мне самому очень интересно, как Rust будет махаться с Go в перспективе ближайших 10 лет. Я уже запасся попкорном))

как Rust будет махаться с Go в перспективе ближайших 10 лет.

Ни как не будут. Разные сферы применения.

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

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

Возвращаясь к корове… Это не корова точно, скорее осёл которого я действительно и пока безуспешно пытаюсь простимулировать. Увы, против глупости сами боги бороться бессильны.

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

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

Честно говоря, я не очень понимаю Вашу претензию. Где именно документация на сайте rust-lang.org не приведена в соответствие с текущей версией? Насколько я вижу, документация и по языку, и по стандартной библиотеке вполне соответствует самой свежей стабильной версии 1.57.

Судя по Вашим предыдущим комментариям, Вы имеете в виду то, что в книге "The Rust Programming Language" написано "This version of the text assumes you’re using Rust 1.55 or later with edition="2018" in Cargo.toml of all projects to use Rust 2018 Edition idioms." Но в этом лично я не вижу проблемы: различия между версией 1.55 и 1.57 невелики, и для книги, предназначенной для первоначального обучения, несущественны. Разве что можно об этом одним предложением во введении упомянуть, чтобы это было понятно всем.

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

Ага, «различия между версией 1.55 и 1.57 невелики» - конечно, ведь они принципиальны, как и должны быть по самому понятию изданий. Если нарушена обратная совместимость, то есть шанс по старому изданию научиться тому, что по новому работать не будет.

А сколько это изменений в штуках - не важно, как мне конечно не удастся объяснить ввиду противоречия с нежеланием не делать ничего. Если изменения тянут на новое издание и при этом незначительны, то их нужно немедленно отразить везде потому, что это легко. Если значительны - то тем более нужно потому, что значительные изменения отражать необходимо.

Как раз «для книги, предназначенной для первоначального обучения», они существенны. Потому что сейчас первоначально обучающийся должен выписать все изменения из документации на листочек, причём не понимая о чём это поскольку он как раз первоначально обучающийся, и постоянно на этот листочек поглядывать читая книгу.

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

  1. Введён раздробленный захват в замыканиях. Речь о том, что теперь компилятор не будет ругаться, когда разные компоненты структуры захватываются по отдельности. Несохранение совместимости здесь для новичка вообще незаметно - оно заключается только в тонкостях последовательности вызова Drop, и проявляется только для самописных реализаций Drop с внешними эффектами - вряд ли я ошибусь, если скажу, что 99,9% программирующих на Rust'е никогда с этой тонкостью не столкнутся.

  2. При прямом использовании array.into_iter() итерирование теперь происходит по значениям, а не по ссылкам. Причём при нормальном непрямом использовании всё осталось по-прежнему, for &e in &[1, 2, 3] работает как работало. Сделано же изменение для того, чтобы появившаяся в версии 1.53 возможность писать for e in [1, 2, 3] реализовывалась без временного обходного манёвра. На практике такое прямое использование встретить очень сложно, а для новичка оно уж точно неактуально.

  3. $_:pat в macro_rules стало понимать |. Сломало это только некоторые макросы, которые принимали в качестве параметра выражения с | и обрабатывали их сами. Не думаю, что таких макросов было сколько-нибудь заметное количество - не знаю, для каких ситуаций такая логика полезна. Это не то что новичку, это и старичку-то неактуально.

  4. Новый механизм разрешения фич для пакетов, введённый в версии 1.51, стал использоваться по умолчанию. Различия только для пакетов, которые подключаются несколько раз с разными параметрами. Раньше искался общий вариант фич, теперь пакет может компилироваться несколько раз с разным набором фич. Тонкости, очень далёкие от новичка.

  5. Типажи TryInto, TryFrom and FromIterator включили в прелюдию. Соответственно, в некоторых случаях реализации в своих типажах методов их этих типажей (т.е. try_into, try_from и from_iter) может быть неоднозначность того, какую реализацию использовать. Опять-таки, по-моему, крайне маловероятно, что этим столкнётся не то что новичок, а и старичок.

  6. Теперь все макросы panic имеют тот же логичный формат параметров, что и макрос print - раньше был зоопарк с разными их реализациями в std и core . В книге, по-моему, ничего менять не придётся, но это, пожалуй, единственное изменение, с которым мог столкнуться новичок.

  7. Зарезервирован на будущее синтаксис вида any_identifier#, any_identifier"..." и any_identifier'...'- некоторые макросы могли воспринимать это как раздельные токены, а теперь будут единые. Пока ни для чего конкретного не используется, и не уверен, что вообще хоть где-нибудь на практике это изменение хоть что-то сломало.

  8. Предупреждения компилятора bare_trait_objects and ellipsis_inclusive_range_patternsприобрели статус ошибок. В книге ничего менять не надо, затронутые конструкции являются нерекомендуемыми уже в редакции 2018, принципиального значения те конструкции не имеют: в первом случае надо добавить dyn, во втором - заменить ... на ..=. Новичок столкнуться с этим шансов не имеет, если он не полез изучать модуль, написанный в 2015-й редакции.

Вот и всё. Как видите, изменения микроскопические. В документации по языку они отражены, конечно, везде, где нужно. Что касается книги, то, действительно, поменять 1.55 на 1.57, а 2018 на 2021, а также сказать о трёх, а не двух существующих редакциях не сложно, но я вижу несколько моментов:

  1. Это ни на что не влияет в плане получения читателем знаний.

  2. Всё же надо менять не бездумно, а подумав над формулировками, а также перепроверив, что действительно, нигде больше в книге не нужно ничего поменять. Поскольку эта книга - авторский текст, то делать это должен кто-то из двух её авторов. А пропущенные из-за ненужной спешки ошибки могут оказаться долгоживущими, и от них уже может быть вред.

  3. По какой-то причине у обучающегося может быть не совсем самая свежая версия компилятора. Нет необходимости требовать от него последней версии компилятора, если это несущественно для прочтения книги. Хотя я не думаю, что это соображение сыграло роль.

С другой стороны, я, конечно, соглашусь, что это было бы красиво, если бы сразу после выхода версии 1.56, буквально в тот же день, в The Rust Book внесли бы упоминание редакции 2021. Но именно что красиво, не более того, по-моему.

Я не просто прочитал внимательно, я изначально предполагал, что так может быть написано. Поэтому пытался предотвратить объясняя, что если изменения невелики, то и исправить просто, а если просто - то и сделать это нужно обязательно. Когда простые вещи не делаются, это признак того, что команда уже вступила на типичный для опенсорса путь самоуничтожения. Почему Линукс витрина и исключение? Да просто Линус не чурался наорать.

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

  2. Ну да, ещё одно ограничение снято, а как раз новичок смотрит что происходит, а не рад что абы как сработало и ладно. Пиши ссылку - какого дьявола? Вот реакция новичка.

  3. Старичку - не актуально, а новичку с незамыленным глазом - интересно как раз при первом чтении с вероятностью процентов 30. Хотя если он узнаёт неверно, то обнаружит это, скорее всего, не скоро или никогда.

  4. То же самое. Практически не очень важный, но практически очевидный вопрос.

  5. Это да, не интересно.

  6. Тут просто напомню из маркетинга - чтобы не покупать потребителю достаточно одной причины.

  7. Действительно неинтересно.

  8. И это тоже.

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

  1. Я уже писал про глупость этого «не влияет». Такое выясняется после прочтения книги с лежащим рядом листочком с изменениями.

  2. Ну вот, не сделано потому, что кому-то лень. Что ещё не сделано по той же причине? Кто заставил вместо документации использовать авторский текст? И когда уже написано, что авторский текст был изменён сообществом, участие авторов точно не требуется.

  3. Смешно. Версия у облучающегося та, что на play.rust-lang.org. Или та, что скачивают с первой страницы rust-lang.

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

Отвечу на вопрос "кто заставил вместо документации читать авторский текст" - никто не заставлял, это ваш выбор - читать документацию, где отражены самые свежие детали не только самой свежей стабильной версии, а и информация по nightly builds тоже; или читать авторский текст - книгу, в которой авторами выбрано и изложено то, что они считают наиболее важным, и в той последовательности, которую они сочли целесообразной.

По остальным вопросам - скажите, пожалуйста, Вы сами книгу читали? Если нет, то, пожалуйста, прочтите. Если читали - то, пожалуйста, перечислите конкретные изменения, которые Вы предлагаете - в каких именно фразах какие именно изменения. Извините, в ином формате мне продолжение обсуждения видится бесперспективным, заведомо бесплодным - в частности, мне Ваши требования по изложения обсуждаемых деталей кажутся не только бесполезными, а и вредными - книга и так довольно объёмная, и перегружать её этими даже не второстепенными, а десятистепенными деталями, на мой взгляд, совершенно не стоит.

С чувством глубокого удовлетворения отмечаю, что документация исправлена. Моя взяла?

где то кроме блокчейнов применяется? как не посмотришь 99% всего на блокчейнах на нём

Если мы говорим про отдельные проекты — мне сразу в голову приходят Sequoia-PGP, rav1e (который, правда, наполовину ассемблером оптимизирован) и ripgrep (в VS Code используется для поиска по файлам).

Известные компании перепиливают критичные места с JS на Rust для компиляции в WASM.

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

Довольно странное утверждение. Что тут является неожиданной ошибкой? В моем понимании касательно плюсов это либо необработанное исключение, что не так страшно. Либо повреждение памяти. И тут никаким местом плюсы не являются адекватным языком. Он не предоставляет никаких средств для работы с ними в отличии от сравниваемых Go и Rust. Надежные системы можно конечно на всем строить, но таки С++ далеко не лучший вариант для этого. Я бы даже сказал один из худших. По крайней мере в отсутствии внешних тулз, которые бы обуздали его ненадежность.

А что до разделения, как раз в Go оно наиболее правильно и сделано. Это либо паника, которая кладет приложение (можно конечно поймать, но редко когда это делают) предсказуемым образом и выводит лог, по которому практически всегда можно сразу понять причину. Это реально неожиданная ошибка получается, читай, баг в коде. Все остальное это ожидаемые ошибки, которые должны быть обработаны хоть как-то. И в этом плане Rust и Go очень близки - они оба делят ошибки на эти две категории, и ведут они себя примерно одинаково.

Касательно необходимости раста. Я до сих пор не могу найти причин его изучать, кроме как желания быть на волне хайпа с остальными. Язык интересный, но гарантии и от того высокая цена разработки на нем оставляют очень маленький набор кейсов, когда он реально был бы полезен и смотрелся лучше рядом с конкурентами вроде Go, C# или даже Python. Один только взгляд на синтаксис отбивает желание с ним связываться. Я вижу постепенное его проникновение в ядра разных ОС и тут у меня вопросов никаких. Наиболее перспективным видится выдавливание С/С++ из различных областей, где эти языки предоставляют базовые важные компоненты вроде библиотек шифрования, драйверов всего и вся, вэб серверов, JIT и рантаймов других языков. Все то, что составляет основу всех систем и все то, что постоянно страдает от небезопасности этих языков.

Было бы неплохо "из коробки" иметь возможность часть нового кода писать на Rust и связывать его с плюсами. C++ может использовать C, в котором другая система линковки, через extern "C". Почему бы не поддержать в компиляторах extern "Rust" ? Это дало бы буст к юзабельности Rust в реальных проектах и все вышеназванные проекты можно было бы перевезти постепенно.

В той же паре Java/Kotlin можно совершенно бесшовно вызывать код одного языка из другого языка.

Почему бы не поддержать в компиляторах extern "Rust"

А как это должно выглядеть?


Во первых, не представляю вызов дженериков из другого языка (аналогично с плюсовыми шаблонами). Сюда же хитрости с бороу чекером — это получается компилятор условной джавы должен будет проверять, что растовые функции правильно вызываются? Если всё это вырезать, то останется примерно тот же extern "C", что и в расте сделать можно.


Во вторых, у раста нет стабильного ABI.

Всем вышеназванным проектам запросто хватит C ABI. Вы же понимаете, почему Java и Kotlin вызываются бесшовно - у них общий рантайм.

Раст поддерживает C ABI, а других FFI пока толком и не придумали, хотя уже несколько лет ведутся разговоры о более безопасных вариантах, да и спецификации на FFI фактически нет. Есть конечно еще интерфейсы для wasm, но оно не слишком хорошо генерализируется для интеграции в другие языки, хотя вполне можно компилять что угодно в wasm и подключать уже wasm либы (rust, python, c#, c++ и java насколько мне известно имеют соответствующие либы).
А java есть JNI, но оно вроде по сути та же обертка поверх C ABI. Так что не вижу каких-то принципиальных вещей которые могли бы улучшить ситуацию. Разве что кто-то таки возьмется описывать спеку на FFI.

>гарантии и от того высокая цена разработки

Цена "высока" только если измерять отрезок хренак-хренак и в продакшн. Если же брать в сумме с последующим дебагингом и развитием получившегося творения, то ситуация уже далеко не так однозначна и начинает зависеть от множества факторов. Плюс цена разработки падает с увеличением опыта. Да, в первые месяцы Rust будет значительно менее продуктивен по сравнению с языками вроде Go изученными с нуля на том же сроке, но освоившись продуктивность (по моему опыту и отзывам других людей) становится сравнимой. Более того, становится страшно писать что-то крупное на языках без гарантий и возможностей которые даёт Rust.

>Один только взгляд на синтаксис отбивает желание с ним связываться.

Честно говоря, мне это нытьё про синтаксис совершенно непонятно. Да, есть неудачные моменты вроде турборыбы и дублирования ограничений в `impl` блоках, но в основной массе претензии к синтаксису выглядят либо как синдром утёнка, либо жалобами на то что компилятору нужно сообщать больше информации в сигнатурах (отсюда код для незнакомых с языком выглядит излишне "нагруженным"). Информационная плотность сигнатур, после привыкания наоборот становится плюсом.

неудачные моменты вроде турборыбы

https://turbo.fish/

Семь лет назад я подробно об этом рассказал на PiterPy: https://youtu.be/hzVECcMI8ys

И что, простите, я должен почерпнуть из этого рассказа? Да, я пролистал только слайды, но по ним и так понятно - это ликбез в самые основы обработки ошибок. Вы коснулись все тех же двух классов ошибок - ожидаемые и нет. Одни мы ловим и проверяем, другие кладут программу. Сетевые ошибки, отсутствующий файл, разное окружение - это все базовые вещи, которые все и всегда должны проверять. Здесь не может быть неожиданных ошибок. Если самую обычную ошибку вроде FileNotFound не ожидали, то это ошибка в коде, который эту ошибку не проверил и что-то с ней не сделал.

Go воспитал целое поколение программистов, для которых проверка всех и каждой ошибок на каждом вызове функции это норма. Проверяется тип, содержимое ошибок, разворачивается стек ошибок, если используется errors.Wrap и ему подобные. Что-то обрабатывается, что-то игнорируется, что-то возвращается пользователю, что-то логируется, а что-то просто кладет приложение. В чем здесь проявляется печальность обработки ошибок Go (и Rust заодно, раз он использует примерно тот же подход) и в чем здесь C++ хоть чем-то лучше будет? Напротив, я считают исключения довольно противной вещью, которая не способствует понятному и надежному коду, и в наше время это тема очень противоречивая, от чего современные языки и пошли другим путем. Swift тут тоже в тему будет.

Без обид, но я не хочу пересказывать свой доклад текстом. Печальность обработки ошибок Go в том, что нет простого способа по err определить весь диапазон возможных значений и выбрать что мы хотим отнести к классу "ожидаемых" и проверять, а что хотим отнести к классу "неожиданных" и падать. В Rust эта фундаментальная проблема решается типом Result и двумя семантиками языка: Enum'ами и pattern matching. В каждой точке обработки ошибок мы получаем от IDE autocompletion вообще всего, что туда может прийти. Это позволяет принимать решения, а не делать вид что мы, как вы метко написали, "что-то обрабатываем, а что-то нет".

>> что нет простого способа по err определить весь диапазон возможных значений
уже давно есть -- https://pkg.go.dev/errors
можно заворачивать ошибки в ошибки и раскручивать их в вызываемом коде.

Поясните, пожалуйста. Вот я как разработчик микросервиса получил err от какой-нибудь библиотеки взаимодействия с амазоном и хочу посмотреть на весь ассортимент ошибок, чтобы выбрать те, которые в данной точке программы являются для меня ожидаемыми и на которые я могу осмысленно реагировать. Например все, что связано с сеткой. Остальные ошибки - амазона, библиотеки, файлов итд для меня неожиданные, их я обрабатывать не хочу и хочу падать со стектрейосом, чтобы микросервис позвал взрослых. Как /errors мне поможет написать этот if? Как я выберу те значения, с которыми должен сравнить err?

Понятно, ваша позиция - мне нужен список всех ошибок в точке вызова. Непонятно, при чем тут С++, который ничего подобного не дает, но ладно. Я могу сказать одно - на практике такой проблемы просто нет и меня всегда удивляют подобные теоретизирования критиков подхода к ошибкам у Go. Да, в теории это нужно и в первые месяцы пользования языком я тоже задавался вопросом, а как мне узнать все ошибки. Со временем паттерны и упомянутое мной воспитание Go разработчиков собралось в единую картину.

Мне не нужен список всех ошибок. Я сам знаю, какие ошибки меня интересуют на каждом вызове. Если я используют амазон и обращаюсь к S3, то я отлично понимаю, что мне нужны сетевые ошибки, ошибки доступа к бакету, отсутствие объекта. Как правило, все библиотеки возвращают свой кастомный тип ошибки, который упомянутым errors и приведением типов я могу вытащить и получить нужную мне информацию. Если доков не достаточно, то я, внезапно, просто тупо читаю код. В этом смысле у Go огромное преимущество перед тем же C++ - какая бы ни была сложная библиотека, я запросто могу читать ее код и с большим успехом его понимать. Я не могу такого сказать о многих других языках. Все. Проблемы такой просто нет, вообще. Все остальные ошибки для меня являются неожиданными, и они могу идти в логи, возвращаться юзеру, приводить к панике и т.д. и т.п. Меня не интересует их содержание. Если я не знаю о них и не предполагаю их обработку, то мне о них и знать нет никакого смысла. Вот например, запросили объект, а бакета вообще физически нет. Чего мне толку знать это - что я с этой информацией сделаю? Эта ошибка вернется пользователю, и пусть он с ней сам разбирается.

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

В том, что это надо делать руками, тогда как это можно было бы делать на уровне компилятора.

Как ты проверишь на уровне компилятора правильность входных данных?

Вот есть у тебя time.Parse, он кидает ошибку. Как ты предлагаешь её проверять на уровне компилятора?

Ну и в том, что система типов Go недостаточно выразительна, чтобы показывать возможные ошибки на уровне типов и сделать это композабельно.

В Go так же можно описывать ошибки через типы (пишу код по памяти):

type errFoo struct{
 s string
}
func (e errFoo) Error() string {
  return e.s
}

func main() {
  if err := foo(); err != nil {
    switch err {
      case errFoo:
      log.Println("foo error!")
      default:
      log.Println("Unknown error.")
    }
  }
}

https://go.dev/blog/error-handling-and-go

Типы можно ещё проще задать (для очень простых ошибок):

var fooError = errors.New("foo error!")
НЛО прилетело и опубликовало эту надпись здесь

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

var errFooType = errors.New("foo error")

func foo() errFooType {
  return errFooType
}

func bar() errFooType {
  return foo()
}
НЛО прилетело и опубликовало эту надпись здесь

Если типов ошибок больше, ты будешь их все перечислять?

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

Что именно делать на уровне компилятора? За меня ошибки он не обработает. Принимаю решения о важности той или иной ошибки я и никто другой. Список всех возможных ошибок - возможно тут компилятор сэкономил бы мне считанные минуты времени, но не более того. Этой проблемы, как я выше написал, просто нет на практике. Если я использую time.Parse и он мне вернул ошибок, потому что из фронта пришел запрос с кривой датой - что мне делать с этой ошибкой? Да ничего, вернуть обратно фронту. Если я делаю выборку из базы, то меня может интересовать разве что ошибка об отсутствующей строке. Все остальное меня не интересует, я ничего не могу полезного с этим сделать. Я пойду, найду в доке тип ошибки, который библиотека использует, и вытащу из нее ошибку какого-нить постгре и пойму, что в базе нет такой строки. Все.

Поэтому меня всегда удивляли эти теоретическия рассуждения о мощи системы типов. Ну ок, недостаточно выразительна система типов. Это не мешает мне писать код, в котором минимальное число багов встречается и никогда эти баги не были связаны с тем, что система типов недостаточно выразительная. Это не та проблема, на которой нужно заострять внимание. Borrow checker - вот это реальная проблема и упомянутые баги как правило с конкурентным кодом и связаны. Мощь типов - нет, просто нет.

НЛО прилетело и опубликовало эту надпись здесь
Либо повреждение памяти. И тут никаким местом плюсы не являются адекватным языком. Он не предоставляет никаких средств для работы с ними в отличии от сравниваемых Go и Rust.

А что именно имеется в виду под «работой»?
Восстановление после повреждения памяти? Как-то сомнительно, можно примеры?
Или все же предотвращение повреждений памяти в некоторых случаях за счет ограничений количества способов отстрелить ногу?

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

Мне нравится Rust, в первую очередь системой типов (не идеально, но это пока самое крутое, что я видел в менйстримных языках). Тем не менее, ИМХО, "ручное" управление памятью - это удел определенных узких задач, системного и встраиваемого ПО, а чаще всего GC достаточно. От того мне странно видеть, что если где и есть Rust - так это бэкенд, а в более низкоуровневые ниши, для которых Rust, ИМХО, подходит лучше всего, он проникает медленно.

Не бесконечно-далёкая система типов есть в Скале.
Там есть родовые травмы, связанные с Java (а именно проблема с null'ами, от которой полностью не получается уйти), но типизацию с трейтами она позволяет (и проверяет в compile-time)

А вы можете объяснить чем Scala так хороша?


Я вот её не понимаю, откуда взялся интерес к ней, на грани хайпа.
Вроде бы там две родовые травмы, сводящие всю красоту на нет:
1. Нетранзитивность признака "mutable". Ну какой смысл иметь иммутабельную коллекцию (или объект), содержащую в себе мутабельные данные?
2. Довольно много неявных преобраований.

ПС
Вот с первым, к стати, у Раста намного лучше - вплоть до того, что мне кажется примерно так и должна выглядеть (с поправкой на бОльшую высокоуровневость, позволяющую прятать сложность кода в машинерию компилятора) "человеческая версия Хаскеля" - жёстко разделяя данные, которые "могут измениться" и "не могут измениться".

Вот с первым, к стати, у Раста намного лучше — вплоть до того, что мне кажется примерно так и должна выглядеть (с поправкой на бОльшую высокоуровневость, позволяющую прятать сложность кода в машинерию компилятора) "человеческая версия Хаскеля" — жёстко разделяя данные, которые "могут измениться" и "не могут измениться".

Это пока вы не прочитаете про interior mutability.

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

В Хаскеле достаточно строгости в этом месте.
Просто в других местах строгости излишне.

А когда начинают делать условный "best FP + OOP hybrid" - то ослабляют строгость везде. Где надо и где не надо.

Я вот её не понимаю, откуда взялся интерес к ней, на грани хайпа.

Интерес к ней взялся в то время, когда в джаве был ужасный застой. Она дала хороший буст всей jvm экосистеме, как минимум джаве (лямбды, функциональные интерфесы, стримы, Optional, type inference - это что в голову сразу пришло. Естественно в джаве это все появилось не просто так, а благодаря вынужденной конкуренции) и котлину (тут проще будет перечислить отличия, чем сходства).

1. Нетранзитивность признака "mutable". Ну какой смысл иметь иммутабельную коллекцию (или объект), содержащую в себе мутабельные данные?

Тут не понял. А зачем вы нарочно мутабельные данные засовываете в иммутабельные коллекции, если в вашей конкретной ситуации это еще и сильно вредит? Кейс классы по умолчанию иммутабельные. И вот какая-нибудь такая вещь у вас тоже полностью неизменяемая в итоге получается:

case class Item(id: UUID, name: String)
case class Quantity(value: Int) extends AnyVal
  
case class Customer(firstName: String, lastName: String)
case class Purchase(items: Map[Item, Quantity], total: BigDecimal)

val customer = Customer("Kyle", "Johnson")
val purchase = Purchase(
  Map(Item(UUID.randomUUID(), "milk") -> Quantity(2)), 
  BigDecimal(5)
)

val completelyImmutableNestedStructure = List(
  customer -> purchase // <- this is a tuple
)

>> Интерес к ней взялся в то время, когда в джаве был ужасный застой. Она дала хороший буст всей jvm экосистеме, как минимум джаве (лямбды, функциональные интерфесы, стримы, Optional, type inference - это что в голову сразу пришло.
Спасибо.
А вот подскажите ещё такой момент (по вашему мнению, разумеется): то, что популярность Scala хм "стагнирует" на сегодняшний день - у этого только маркетингово-рыночная основа (никто большой за неё не вписался) или всё-таки "по-честному" и технологически у неё нет особых крутых перспектив \ выгодных ниш и т.д. ?

ПС.
>> А тут не понял. А зачем вы нарочно мутабельные данные засовываете в иммутабельные коллекции.
"моей" ситуации нет - я прошёл пол-курса Одерского на курсэре (потому, что захотел посмотреть "современных FP-OOP гибрид" и именно курс одерского мне советовали прям как "вау это нечто").
По поводу "иммутабельной коллекции мутабельных данных":
1. Мне не нравится, что сам язык такое позволяет.
2. Если я правильно понимаю (из общих соображений) компилятор редко может доказать, что в коллекции "транзитивно иммутабельные" данные. И применить хорошие оптимизации.
3. Ну и даже человек всегда должен помнить, что подлянка такого рода возможна (например предок иммутабельный, а в коллекции лежит его мутабельный потомок).

Почему мне это не нравится (п1)
ИМХО проникновение FP в OOP в том числе имело цель запретить плохие паттерны кода, те, в которых программисты чаще делают ошибки (ну и вообще снять ментальную нагрузку, особенно ментальную нагрузку в части неявных случаев), зачастую даже ценой скорости исполнения. Ну и этот случай явно противоречит данной цели.
(а да сам Одерски многократно напирает на замечателность иммутабельных данных. Но сам же, хорошей поддержки в языке "транзитивной иммутабельности" не сделал).

А вот подскажите ещё такой момент (по вашему мнению, разумеется): то, что популярность Scala хм "стагнирует" на сегодняшний день - у этого только маркетингово-рыночная основа (никто большой за неё не вписался) или всё-таки "по-честному" и технологически у неё нет особых крутых перспектив \ выгодных ниш и т.д. ?

Совокупность этого всего. Скажем так, лучшие и самые удобные штуки из нее переняли (ну если не напрямую, то косвенно) в Java, C# и Kotlin. И в итоге при сравнении плюсов/минусов скалы и вот этой троицы при старте проектов, выбор все чаще выпадал в пользу троицы. Два основных минуса скалы на мой взгляд: байт-код несовместимость между версиями (что должны были пофиксить в недавней 3й версии, но уже поздно для возвращения в мейнстрим. Лет 5 назад сделали бы, то еще был бы шанс) и излишняя сложность (приходится учить и ООП, и околохаскелевскую функциональщину). В итоге "рыночек порешал". (а) Сложно стартовать проект, т.к. нет разрабов и каких-то вещей в экосистеме => (b) разрабы не изучают и не пишут инструменты, т.к. становится меньше проектов на рынке => возвращаемся к (a). И так по кругу. Довольно классическая картина затухания.

По поводу того, что никто за нее не вписался - не совсем так. Вписывались, и многие. Твиттер так вообще чуть ли не все переписал на нее лет 10 назад, в волмарте писали на ней куски большие, в линкедине целую кафку сделали :) А уж количество компаний, которые писали на ней небольшие кусочки своего бэка, на порядки больше.

и технологически у неё нет особых крутых перспектив \ выгодных ниш и т.д. ?

На текущий 2021 год лично я вижу у нее 3 ниши. В порядке убывания:

1) Спарк и потоковая обработка. Хотя в batch обработке уже можно уверенно сказать, что по сути победил питон со своим pyspark'ом, но вот в потоковой еще пока нет. И, например, на Flink с использованием Скалы сейчас пишут некоторые вещи в Netflix, в Alibaba. В Tesla вроде есть какие-то сервисы по стриминговой обработке на Скале (но тут могу напутать). В Spotify биндинги для Apache Beam сделали. Как видите, это не классический бэкенд, а компании и их конкретные сервисы с терабайтами и петабайтами данных с требованием обработки в реальном времени, что довольно специфичный случай. Ну и да, не удивлюсь, если года через 4-5 и тут окончательно вытеснится джавой с питоном по вышеуказанным причинам.

2) Full FP style со всеми этими вашими эффектами, тайпклассами и монадами. В основном для более точного, корректного и тестируемого описание данных при моделировании доменной области. Самый крупный представитель, который приходит мне на ум - Disney+. У них крупные части сервисов billing system'ы написаны в таком стиле. Также видел пару статей, как некоторые компаниии обычные микросервисы пишут на этом стеке. По сути, это юзер-френдли хаскель.

3) Акторная модель на Akka.

То есть ниши есть, но в процентном соотношении относительно классического бэкенда - капли в море.

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

А можете пример какой-нибудь привести таких оптимизаций? Там и так иммутабельные коллекции написаны здорово. Построены на всяких trie и HAMT структурах с применением structural sharing. Мне в голову так сейчас не приходит пример, где информация об иммутабельности содержащихся внутри данных может помочь компилятору в оптимизации еще больше. То, что это способно выстрелить потом, когда бесконтрольно из разных частей кода менять состояние - тут да, не спорю, об этом следующий пункт.

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

Это действительно возможно. Но перед этим нужно явно совершить действие с отходом от стандартных практик. Вот тот человек, который явно пропишет var вместо рекмендованного val или вложит внутрь изменяемую коллекцию, предварительно импортировав ее - именно он во время этих действий и должен осознавать данные вещи. А я не знаю, кстати, в хаскеле или расте компилятор умеет такую вложенность проверять?

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

Как хаскелисту интересно — что делает скалу более юзер-френдли?

Больший пул кандидатов + возможность постепенного перехода от привычного си-подобного синтаксиса с ООП к хаскеле-подобному синтаксису. Как следствие, больше вакансий в коммерции

Спарк и потоковая обработка. Хотя в batch обработке уже можно уверенно сказать, что по сути победил питон со своим pyspark'ом, но вот в потоковой еще пока нет

У меня есть задача обработки данных батчем, и от скалы (или какого-нибудь другого jvm) особо не уйдёшь в user-defined функциях - у меня получилось отличие примерно на треть общего времени (не времени одного шага, а вот прямо всего - с вычитыванием данных вначале и последующей довольно тяжелой агрегацией и т.п.) в пользу нативной скалы.

Здесь согласен, в таких сложных местах pyspark еще уступает. Но, к сожалению, суммарная тенденция все равно очевидна по непрямым показателям. Как один из примеров:

Это действительно возможно. Но перед этим нужно явно совершить действие с отходом от стандартных практик. Вот тот человек, который явно пропишет var вместо рекмендованного val или вложит внутрь изменяемую коллекцию, предварительно импортировав ее - именно он во время этих действий и должен осознавать данные вещи. А я не знаю, кстати, в хаскеле или расте компилятор умеет такую вложенность проверять?

Ну вот буквально на днях встречал вот такую проблему:
Заведём мутабельный case class: case class Counter (var count: Int) {def inc: Unit = {count = count +1; }}
После этого неправильно воспользуемся List.fill:
val t = Counter(0)
val l = List.fill(5)(t)
Ну, а после этого попробуем инкрементировать каждый независимо:
l.foreach(_.inc)
И мы, внезапно, получили в каждом из каунтеров 5 (и это, на самом деле, даже один каунтер).

Раст такое отлавливает - либо нужно явно дуплицировать объекты перед складыванием в массив, либо борроу чекер скажет, что вы пытаетесь взять на редактирование ссылку, которая не уникальна.

Заведём мутабельный case class

Вот здесь и кроется опасность :)

Как альтернатива, один из двух вариантов:

case class Counter(count: Int) {
  def inc: Counter = this.copy(count = this.count + 1)
}
  
val a = Counter(1)
val b = a.copy(a.count + 1)
val c = a.inc

А если совсем сложные структуры, то там Monocle как запасной вариант есть.

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

Спасибо за столь подробный ответ.

>> а можете привести пример таких оптимизаций
Из простого, что сразу пришло в голову
- хэшировать какие-то значения (банально Hash \ toString);
- поднимать обращения к внутренностям как можно выше в коде;
- если вы доказали, что в ф-ии нет побочных эффектов, а только чистые вычисления переходить к следующему пункту;
Из продвинутого (для этого нужны "чистые функции" - т.е. всё-таки ещё доп-условия)
- конкуррентный доступ эквивалентен обычному (т.е. никаких доп-инвариантов);
- просто запускать любые вычисления над такой коллекцией в параллельном потоке по готовности входных данных.

>> А я не знаю, кстати, в хаскеле или расте компилятор умеет такую вложенность проверять?
Я бы ответил "да" и "да" (но в Rust меня поправили, что есть interior mutability).

Тем не менее, ИМХО, "ручное" управление памятью — это удел определенных узких задач, системного и встраиваемого ПО, а чаще всего GC достаточно.

На практике на Rust писать почти так же удобно. RAII всё-таки рулит.

Смотрите вот при решении "задачек" по ощущениям кода на Rust примерно вдвое больше, чем на Go ("задачек" - потому, что при желании освоить язык первый (и иногда последний хД) этап: решить N задачек на нём на leetcode).

Т.е. RAII разумеется рулит, но это проблему решает лишь частично.

leetcode тут не очень показательный пример. Обычно на нем задачки имеют довольно небольшой обьем кода и плюс вы решаете задачу до конца за условно 20 минут-1час и после этого забываете это решение на всегда. Rust для такой задачи может быть использован, но не покажет в полном обьеме своих преимуществ. Вот если у вас кодовая база в 50К строк которую нужно поддерживать много лет, вот в ней у вас будет профит в том плане что при добавлении новых фич или рефакторинге вы будете очень много ошибок/проблем видеть еще на этапе компиляции а не в момент запуска в прод. Например добавили новый тип ошибок в enum и теперь надо везде добавить их обработку

Например добавили новый тип ошибок в enum и теперь надо везде добавить их обработку

В Go это контролируется линтером.

Например добавили новый тип ошибок в enum и теперь надо везде добавить их обработку

Компилятор сообщит про это только если в в match'е были перечисленны все типы ошибок. Но почти всегда обрабатываються только конкретные.

match open() {
  Ok(_) => {},
  Err(Errors::NotFound) => { ... },
  Err(err) => return Err,
  // Про новые типы в enum Errors компилятор уже сообщит
}
Интересно было бы сравнить не тройку Rust/Go/C++, а четвёрку Rust/Go/D/C++.
Почему в сейчас не стоит начинать новый большой проект на C++ довольно-таки понятно; накушались все. А вот выбор Rust/Go/D уже не так очевиден.

Если не углубляться в детали языка и историю развития, то основная проблема D в том что он "не взлетел", что лишает его синергетического эффекта, а учитывая возраст языка и наличие сильных конкурентов вероятность "взлететь" в будущем слишком мала дабы серьёзно в него инвестировать.

С Go же, на мой взгляд, ситуация следующая: если вас устраивает наличие сборщика мусора, желательна асинхронщина, разрабатываемая программа не особо крупная (скажем, в пределе нескольких сот тысяч строк) и вы готовы мириться с определённым примитивизмом языка (и тем более если для вас это является достоинством), то вариант действительно очень хороший и, вероятно, выигрывающий у Раста.

У Григория явно мало опыта в Go.

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

Тут не вопрос "простоты", увы ? Про ожидаемые и неожиданные ошибки, а также связанные с этим беды я еще семь лет назад рассказывал на PiterPy: https://youtu.be/hzVECcMI8ys

Спасибо за ссылку. После просмотра постараюсь ответить по существу.

Насколько я понимаю, проблемы boilerplate кода вида


..., err := do_something()
if err != nil {
    return ..., err
}

а также случайного игнорирования тех самых err он не решает никак.
Поправьте пожалуйста если ошибаюсь.

Вопрос был про Go:


У Григория явно мало опыта в Go

Про ситуацию в Rust я в курсе, спасибо.

Много комментов) Главная проблема в Go как я ее вижу не в том, что синтаксис избыточен (так же как в Java это может скрыть IDE) или можно легко облажаться (на это есть линтеры). Фундаментальная проблема в том что, имея на руках err, у нас нет никакого простого способа понять диапазон возможных значений. Вот сделали мы сетевой запрос, и в данной точке кода ожидаемы для нас ошибки - все, что связано с сеткой. Это мы хотим обрабатывать. А вот если наш сервер ответил 500, или прислал невалидный JSON, или память закончилась - это мы обрабатывать категорически не хотим. А хотим, к примеру, позвать взрослых записав стектрейс падения, и перезапуститься с надеждой на лучшее, если мы микросервис. В Go (и всех других мейнстримовых языках кроме Rust) я не знаю другого способа кроме как поплакать над калечной документацией и пойти читать ВЕСЬ код используемой библиотеке. Чтобы потом через пару месяцев обнаружить в проде неожиданное значение, потому что либу порефакторили и теперь она при временной недоступности сетки операционкой возвращает какой-нибудь OSError (не сама, конечно, а на три уровня абстракции вглубь, но нам от этого не легче).

Так вопрос был не к вам, а к QeqReh, который говорит, что с ошибками в Go просто работать. Чего я пока что не наблюдаю.

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

Тоже самое о случайном игнорировании ошибок. Я сам порой удивляюсь этому, но этой проблемы тоже просто нет на практике. Все дело в привычке и культуре конкретного языка. Все опытные Go программисты знают простое правило - везде и всегда ошибка проверяется. А неопытные об этом быстро узнают, когда их тыкают носом первым же комментариев.

В теории - всё примерно так и есть. Но на практике, которая, как обычно, от теории почему-то отличается, хоть вроде и не должна (в теории :)), всё не так.

На практике сделав сетевой запрос, который, да, мог вернуть сетевые ошибки, ошибки возвращённые сервером, и местные ошибки парсинга/валидации ответа - нас просто не интересуют "все возможные классы ошибок". На практике, нас интересуют либо отдельные специфичные классы ошибок (которые вполне реально определять через интерфейсы вроде Temporary() bool и Timeout() bool), либо отдельные конкретные ошибки - в общем, те, которые мы хотим обработать и знаем, как именно их надо обработать. В сухом остатке - "все остальные", с которыми мы всё-равно без понятия что делать специфического, поэтому они все обрабатываются одинаково (повтором запроса, возвратом выше, логированием и игнором, etc.).

Так вот, на практике Go вполне позволяет написать обработчики для тех ошибок или классов ошибок, с которыми мы знаем, что надо делать специфического именно для конкретно этих ошибок. Получить точный список всех возможных оставшихся ошибок - да, мы не можем, но он нам и не особо нужен, потому что обрабатываться они все всё-равно будут одинаково.

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

При этом надо учитывать, что ни Rust ни Go не решают эту проблему на 100%:

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

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

Мы на препати посовещались и решили сформулировать так: "ассортимент ошибок нужен не для того, чтобы обрабатывать их все. А для того, чтобы выбрать из них те, которые в данной точке программы для нашей логики ожидаемы".

Ну вот на мой взгляд ожидаемые нашей логикой ошибки определяются от предметной области и задач нашего приложения, а от используемого языка зависят слабо (только в тех местах, где язык/библиотеки сами добавили какой-то accidental complexity, которую надо учитывать конкретно на этом языке/с этой библиотекой). Или, говоря иначе, все ожидаемые ошибки и логику их обработки мы обычно в состоянии перечислить на этапе написания ТЗ, задолго до того как мы запустим IDE с кодом и та сможет нам подсказать список возможных ошибок. :)

Перечислить в свободной форме естественным языком - да. Выбрать нужные значения/исключения/типы/что-там-язык-предлагает - нет. Вот я до написания программы сформулировал "для меня ожидаемые ошибки все что связано с сетью, неожиданные - все остальное". Как я, имея в Go "err", смогу определить, какие коды ошибок относятся к сетке, а какие "ко всему остальному"? Вангую, что сейчас вы напишите "ну как же, в документации написано!" Увы. Не написано. Авторы библиотеки сами могут не знать что их библиотека может вернуть, потому что библиотека использует другие библиотеки и "пробрасывает" наверх ошибки. Rust отвечает на этот вопрос с помощью Enum'ов. Go, Python, Ruby и все остальные - нет.

Как я, имея в Go "err", смогу определить, какие коды ошибок относятся к сетке, а какие "ко всему остальному"?

Типы ошибок на уровне http протокола: https://pkg.go.dev/net/http#pkg-variables

Определять можно через https://pkg.go.dev/errors#example-Is

Всё несколько проще - это определяется уровнем ниже. Т.е. в тот момент, когда делается сетевой вызов, все ошибки, которые он может вернуть - это ошибки связанные с сетью. Т.е. если стоит задача отличить именно "все сетевые ошибки", то это делается примерно так:

type NetworkError struct {
  err error
}

// где-то на нижнем уровне дёргаем сеть
result, err := someNetworkOperation(params)
if err != nil {
  return NetworkError{err: err}
}

// где-то на верхнем уровне отличаем сетевые ошибки
err := someComplexOperationInvolvingNetwork(params)
if errors.As(err, new(NetworkError)) {
  // обрабатываем все сетевые ошибки
}

Это - не единственный, не самый удобный и не самый часто используемый подход, но это вполне рабочий способ решения проблемы "определить все сетевые ошибки".

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

Авторы библиотек так делают? Когда я, как разработчик, пишу код на Go и вызываю рандомную функцию из рандомной библиотеки - я могу быть уверен, что автор аккуратно обернул NetworkError, FilesystemError, XmlDecodeError, ...? ? Ну и, наконец, даже если такая волшебная библиотека есть - как я за разумное время, находясь в IDE, узнаю, что нужный мне тип называется "NetworkError"? Автор это ВСЕ в документацию вынесет? Как раньше в Java пробовали с checked/unchecked exceptions?

Но это все риторические вопросы просто потому, что никто так не делает: ни язык, ни его экосистема никак не помогают работать с ошибками. В отличии от Rust.

Нет, это делают не авторы библиотек, а мы в своём коде. Так что проблемы с докой нет - свой код мы знаем.

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

Двадцать лет назад я такое слышал про плюсы. "Да, эксепшны сломаны, но вы ведь можете в своем коде обернуть ВСЁ, в чем проблем?" Проблема в том, что обернуть все в своем коде - это чудовищные затраты сил на разработку. В то время как с помщью Rust мы просто пишем Err и выбираем из предложенного IDE нужные нам для обработки ошибки. Все делает компилятор + IDE, нам не нужно самим выстраивать "параллельное" дерево кодов ошибок.

а также случайного игнорирования тех самых err он не решает никак.

Игнорировать ошибки и не надо. Её надо все равно как-то обработать. Хотя бы отправить в log.Debug().

Ваш код я бы написал так:

if err := some(); err != nil {
    return _, errors.Wrap(err, "couldn't do something")
}

errors.Wrap() сохраняет типы ошибок.
И далее можно проверить с помощью errors.Is()

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

Если не сложно, опишите, пожалуйста, пример нагруженного микросервиса на Ruby, который переписали на Rust, чтобы избавиться от бутылочного горлышка. Подход интересный, хочется услышать детали :)

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


Whoops! I Rewrote It in Rust

Можно полистать по TWIR, там тоже были вроде success story по вашей теме.

Хайп вокруг раста вроде начинает проходить. Как уже прошел вокруг го. То есть серебряной пулей не оказался и теперь займет ту нишу для которой предназначен а не для написания всего на свете.

Для написания всего на свете есть C++ и Java)

"Для этого тебе надо прочитать вот такую толстенную книгу, она сложная, ты будешь страдать."

Вы так несколько раз педалируете "толстенность". А про какую книгу речь? Если про "The Rust Programming Language" ("The Rust Book"), то, по-моему, не такая уж она и толстенная, хотя, конечно, и раз в семь больше "The Little Go Book", но зато раза в полтора меньше "The C++ Programming Language". Толстая - да, но толстенная ли? Я имею в виду, что это не книга масштаба "Войны и Мир" или "Искусства программирования" - такие масштабы могут внушать ужас уже сами по себе, а "The Rust Programming Language" по размеру соответствует типичной серьёзной книге, не более того, по-моему.

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

The Rust Book. Я недавно брал интервью у автора. У меня книжка заняла несколько месяцев вдумчивого чтения по вечерам, это с большим C++ бэкграундом. По сравнению с хорошими учебниками по Python/JS она толстая и сложная.

Возможно, кто-то прочтет ее за несколько дней, во всем разберется и вообще ничего сложного. Делюсь тем опытом, что есть у меня :)

Я говорил только про объём. Со сложностью сложнее, да. Кто-то проглотит за несколько дней, кому-то потребуется полгода. Как правильно сказал Клабник в интервью с Вами, очень сильно зависит от опыта человека, с какими языками программирования он уже имел дело - если с какой-то технологией он уже сталкивался, синтаксис её применения в Rust'е он может запомнить за считанные минуты.

А какие конкретно хорошие свободные учебники по Python и JS Вы имеете в виду, с которыми имеет смысл в рамках нашего разговора сравнивать The Rust Book?

Мне нравятся "Eloquent JavaScript" и "Python Crash Course".

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

Rust ничего такого не гарантирует - достаточно просто поделить число на 0 и приложение "упадёт" как и в других языках программирования. Практически все гарантии Rust связанны только с безопасной работой с памятью. А про "падения" надо всегда пояснять, что имеется ввиду корректное завершение приложения, при возникновении необработанных ошибок. Без вредных сайд-эффектов, вроде выполнения стороннего кода или утечки наружу каких-то приватных данных (но это неточно, т.к. через unsafe и указатели скорее всего можно поковыряться в стеке).

Подобные, необдуманные "обещания" создают негативный фон вокруг Rust. Выглядит как втюхивание непримечательного товара маркетологами.

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