Моду на коды ошибок вместо исключений ввели разработчики Rust, причём только потому, что не смогли совместить исключения и Borrow Checker. Других причин не было. Вся чушь, которую написали после: про скорость, или там надёжность с безопасностью — чистая идеологическая хрень. Написать "мы облажались, терпите" означало растерять фан-базу.
Деды программирования наелись кодов ошибок по полной задолго до нас. И, имея в тысячи раз меньше памяти и медленнее процессора, эти деды таки сделали исключения. Что ждёт rust и другие языки — наворачивание обёрток и макросов для "удобного проброса" кодов ошибок и стектрейсов наверх. И в итоге они получат те же исключения. Есть шанс, что, при нормальном метапрограммировании, получится даже круче и фичастее чем то, что сейчас разработчики компиляторов хардкодят. Например, декларативно задавать, какие операции перезапускаемые, или в конфигах описывать какие исключения обязательно должны пойти в лог. Или автоматическое сохранение и передача всяких Trace-Id.
Языки, у которых макросов и метапрограммирования нет, и при этом всё сделано на кодах ошибок — пригодны только для мелких проектов, и, даже если будут какое-то время популярны на хайпе, в итоге вымрут. Просто потому, что на языках, где писать легче, кода напишут больше. А больше кода — это больше проектов в которых нужны программисты, лучше подсказки ИИ, больше информации в интернете, и больше вакансий. С учётом этого, тратить время на языки без исключений и метапрограммирования, можно только за счёт работодателя, и за очень большие деньги. Во всех остальных случаях, любой хайп вокруг них можно смело игнорить.
S>А ведь было же хорошее решение — т.н. проверяемые исключения в Java. Когда компилятор требовал проверки того или иного исключения, но так же была возможно обернуть в RuntimeException, если оно утратило смысл бизнес-логики или ожидаемого
Хельсберг очень подробно когда-то объяснял, почему эту фичу в C# не потащили. Кмк, что реально нужно — возможность указать no throw, и throws(список). Чтобы те, кому нужна железная уверенность что всё перехватывается, могли желаемую надёжность получить. А те, кому достаточно вывести на экран сообщение "коннекта к базе нет", могли писать db.Connect() и радоваться что единственный try/catch в main решает все их проблемы.
Re[3]: Result objects - все-таки победили Exceptions?
S>Не надо ничего кидать. Парсер json возвращает алгебраическую сумму из "ожидаемый тип объекта" и "ошибка синтаксиса". S>Что с этим делать — решает пользователь этого парсера. Он может прямо тут же заматчить ошибку синтаксиса и, скажем, подставить дефолтный конфиг; или сделать retry, или просто прокинуть результат выше по стеку, делегировав принятие решения туда. Компилятор будет бить по рукам за попытку прочитать значение параметра из экземпляра типа "myConfig | BadSyntax".
C учётом того, во сколько прокладок сейчас всё завёрнуто — между местом где ошибка, и местом где её реально обработать не 1 а 4-5-20 промежуточных прокладок. Для того же json — ошибка же не просто в парсере, её вернул какой-нибудь NumberReader, который был вызван ArrayReader, который был вызван PropertyReader, который был вызван ObjectReader, и так ещё 20 раз. И на всех уровнях там будет один и тот же код: "у вас сломалось? значит у нас тоже, пшло наверх!".
Поэтому, закончилось всё макросами, которые уже на автомате пишут, чтобы компилятор по рукам не бил а позволял сразу использовать результат не вникая в ошибку. Пишем тупо в конце каждой функции '?', и всё. Сведение этого бойлерплейта к пустой строке ("") — как раз и есть превращение Result Object в самый обычный Exception.
S>Не знаю насчёт Rust — никогда на нём не писал. Но вот моя практика работы с языками с исключениями — она как раз такая, что провоцирует людей писать в стиле "исключений не существует; бремя доказательства обратного лежит на QA". Это означает, что стоимость работоспособного кода очень сильно возрастает — первоначально написанный "оптимистичный" код уезжает в коммит, чтобы спустя дни и недели попасть в third-line support со словами "тут иногда что-то где-то крэшится, найдите и почините". Это гораздо, гораздо дороже, чем сразу заставить программиста написать код обработки.
С кодами ошибок (или с result object) — проблема та же самая. Вот типовой оптимистичный код (прямо из руководства по Rust):
fn try_to_parse() -> Result<i32, ParseIntError> {
let x: i32 = "123".parse()?; // x = 123
let y: i32 = "24a".parse()?; // returns an Err() immediately
Ok(x + y) // Doesn't run.
}
Для кодеров, которые не пишут обработчики, а пробрасывают всё наверх, разница с исключениями только в том, что в случае исключений match, try! или '?' писать не нужно. Это почти такое же зло как ON ERROR RESUME NEXT в VB. Только теперь это ON ERROR RETURN FALSE. У нас как-бы заявлена, как-бы явная обработка ошибок, только она ...неявная
Поэтому я где-то в этом топике и написал что жду следующую итерацию улучшения Rust, когда все значки '?' расставит макрос для всего метода, и их можно будет из кода совсем убрать. Ребята реально переизобретают исключения.
Ну и у конкретно Rust вторая беда в том, что исключения никуда не делись. Panic это такое исключение, которое должно бы складывать программу. Но так как, внезапно!, обнаружилось, что складывающиеся программы это не очень надёжно, добавили фичу что panic можно перехватить. Но фича, совершенно официально, не всегда работает
S>То есть корректный код не сильно больше по объёму, чем некорректный, но зато он а) явный и б) надёжный.
Эээ, нет. Вот опять из реалий разработки: баги остаются, просто меняют вид. Было "у нас там вылетает, разбираюсь", а стало "откуда-то None (или дефолтное значение) разбираюсь".
В случае исключения — получаешь полный стектрейс, очень близко к месту возникновения ошибки. А в случае с типовым Error Object не получаешь нифига вообще. Где-то наверху кто-то получает Error или None, а место, где создаётся этот Error или None скрыто.
Конкретно Rust — с опозданием в кучу лет, сбоку, сейчас прикручивают хотя-бы стэктрейсы (без гарантий что они будут подключены).
В случае использования отладчика — топ-совет reddit или so от июля 2024-го — в отладчике ставьте брякпойнт на конструктор того Error Object который ловите, но компилируйте без флагов оптимизации, потому что с оптимизациями компилятор может их выкинуть. Без отладчика гораздо легче, ведь у вас каждый return в лог пишется, вот по логу и смотрите LDD — log-driven development!
Я реально вижу только один выход — дать программисту reliable_try: такой блок try, для которого компилятор сгоняет вниз по стеку вызовов, соберёт все возможные исключения, и потом проверит что они все обработаны своими catch. Но и это, скорее всего, закончится тем, что, у очень хорошего программиста, реально обработана будет примерно половина. А вторая половина уйдёт в блок catch(всё неизвестное) — и в этом тёмном углу соберутся все демоны, которых реально не ждали: NRE, OOM, SO, LibraryLoadException и т.д.
Re[3]: Result objects - все-таки победили Exceptions?
AD>Коды ошибок в расте? Интересно...
Если речь о том что Option или Variant сильно отличаются от return 0 в C, или HRESULT в COM/OLE (тут серая рожа того кто знает ) — то для меня разница исчезающе мала. Современными средствами можно накрутить на почти любой язык проверку что на каждый возврат из функции написан if. То что в Rust такая проверка часть языка — просто деталь реализации.
Если же речь о том что я Rust плохо знаю, то мимо. Я люблю всё новенькое, и растом интересовался задолго до того, как у него появилась минимальная популярность. Основные недостатки прочувствовать успел, за обновлениями слежу. Вот, что кстати грустно, всё ещё актуальное обсуждение аж 5-летней давности
. Несмотря на мелкие улучшения, основные проблемы никуда не делись. Всё также жду Rust 2.
Но и ждать некий "Rust 2" или что-то лучше уже довольно уныло. Глобально, мне кажется, ИИ-шечка сейчас надолго угробит и развитие и создание новых языков программирования. Потому что все новые языки попадут в замкнутый круг: язык новый -> нет кодовой базы для обучения -> нет поддержки ИИ/Copilot -> пишут слишком мало кода -> нет кодовой базы для обучения -> круг замкнулся, мы в ловушке. И по новым фичам существующих языков это также ударит.
Re: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>Ну вот рекомендации в новомодных языках: https://docs.flutter.dev/app-architecture/design-patterns/result
S>- все-таки топят за Result objects для бизнес-логики.
S>А ведь это всю цепочку поддерживать. Как-то много лишних букв добавляется.
Что ни делай — результат один и тот же.
Если обрабатывать надо не в точке вызова метода, в котором ошибка, а выше, то так или иначе придется передавать эту ошибку выше. Если использовать исключения, код обеспечит компилятор, если коды ошибок — пиши сам.
Исключения, в общем, проще.
У них ИМХО только один недостаток — их нельзя игнорировать. Коды ошибок можно, если знаешь, что тут что с ошибкой, что без ошибки — все равно.
Я как-то писал на C# удаление 0-го элемента из listbox. Все было замечательно, пока не оказалось, что в листбоксе нет вообще элементов. Я об этом не подумал, так как на Win API если послать сообщение LB_DELETESTRING при нулевом количестве элементов, то, конечно, получишь LB_ERR, но его можно спокойно проигнорировать, так как все, что нужно — чтобы элемента не было. Был он — чтобы его не было, не было — тоже чтобы не было, а какой из этих 2 вариантов сработал — неважно. А на C# пришлось условие ставить.
Здравствуйте, Shmj, Вы писали:
S>Ну вот рекомендации в новомодных языках: https://docs.flutter.dev/app-architecture/design-patterns/result
S>- все-таки топят за Result objects для бизнес-логики.
S>А ведь это всю цепочку поддерживать. Как-то много лишних букв добавляется.
Современные result objects позволяют не описывать все варианты проблем по всей цепочке вызовов, а хранить их "динамически", восстанавливая
тип ошибки в месте обработки. В расте, например, весьма популярны Result<(), Box<dyn Error>> и anyhow::Result<()>
Re: Result objects - все-таки победили Exceptions?
Считаю, что непроверяемые исключения в общем случае это идеальный способ обработки ошибок и все эти result objects не нужны (могут быть нужны в специфических случаях, но не в общем случае). Проверяемые исключения тоже не нужны, от них проблем больше, чем пользы. По крайней мере в том виде, в котором они в Java.
, что отличия настолько исчезающе малы, что их в лупу не разглядишь.
КБ>Когда вы уже поймете что ваш сишный опыт абсолютно уже не релевантен
Много разного опыта позволяет видеть недостатки любой штуки без розовых очков. Например, после scala sbt, на все остальные билд-системы без слёз не взглянешь. А если возвращаться к теме ошибок/исключений, то первый раз столкнувшись с трейтом std::Error в Rust, у меня, среди матюков, проскочила мысль что это было предопределено — миллениалы должны были придумать достойного соперника для HRESULT
Re[14]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Читаем дальше: S>
S>Haskell solves the problem a diplomatic way: Functions return error codes, but the handling of error codes does not uglify the calling code.
S>Там написана неправда?
Там нет примеров более сложной (бизнес-)логики, которая использует хотя бы три-четыре библиотеки от разных разработчиков. В статье идеальный пример, когда все "исключения" могут быть определены в виде одного алгебраического типа. Что хорошо работает в примере и очень плохо работает в реальной жизни. Например, тот же
data ApplicationException =
ReadException
| WriteException
instance ThrowsRead ApplicationException where
throwRead = ReadException
instance ThrowsWrite ApplicationException where
throwWrite = WriteException
Является аналогом
final String content;
try {
content = readFile(...);
} catch (IOException e) {
throw new ReadException();
}
final String content;
try {
writeFile(...);
} catch (IOException e) {
throw new WriteException();
}
Информация о том, почему "не смогла", потеряна. Ладно, можно добавить исключение в ApplicationException:
data ApplicationException =
ReadException IOException
| WriteException IOException
Уже лучше. Хотя... Почему у нас WriteProtected является возможной ошибкой для read? Давайте честно:
Все хорошо? Почти. Теперь у нас усложняется copyFile. Начальное определение ниже уже не работает
copyFile src dst =
writeFile dst =<< readFile src
В первом случае у нас InputException, во втором — OutputException. И нужно их к общему типу приводить. Ну да, есть дальше примеры с ReadException/NoReadException. Только там вроде бы все комбинации исключений (ThrowsWrite (ReadException e) и подобные) определяются. Это хорошо для примера. А что мы будем делать в реальном коде, когда исключений может быть больше? Всякие SQLException, MongoException, RedisException. При этом все эти SQL/Mongo/Redis тоже не просто алгебраические типы, а такие же тайпклассы со сложной структурой. Можно применять примеры из статьи, но это же куча ручного кода! Удобства там мало. А уж если мы будем добавлять полиморфизм...
Давайте сделаем теперь consumeFile, которая получает функцию для обработки содержимого файла:
Что мы будем делать в этом случае? И выйдут ли удобочитаемые сигнатуры? И что будет, если у нас там бизнес-логика (те же хранилища, часть из которых — SQL, а другая — Mongo). Можно оставить вывод типов компилятору. Только вот какие будут сообщения об ошибках, если типы не сходятся? Я не уверен, что это можно будет удобно читать.
Ладно, все выше — это прелюдия. Суровая реальность состоит в том, что Haskell — ленивый язык. Вот возьмем какой-нибудь Prelude.readFile. Давайте пока не будем смотреть на то, что она возвращает банальный IO вместо логичного ExceptionalT ReadException IO (). Давайте посмотрим на результат. А в результате у нас ленивая строка, которая читается при необходимости. Теперь возьмем пример take 5 <$> readFile "/dev/zero". Вопрос — где у нас будет исключение, если вдруг произошла ошибка диска? Произошло ли у нас исключение в take 5? Или только в IO, которое получилось после <$>? Первый вариант потребует много переделок. List будет таскать список ошибок, которые могут возникать при доступе. Все функции работы со списком будут параметризованы этим типом исключений. Второй вариант (исключения только в IO) будет плохо работать в более сложных случаях:
Мы можем отличить ошибки шаблона от ошибок данных. Ну, почти. По нашей логике мы не можем отличить ошибки чтения шаблона и чтения данных. Они же происходят в результирующем IO (результате applyFileTemplate) а не в applyTemplate! Как-то это не очень удобно в обработках. Как мы теперь будем понимать, что не прочиталось?
В общем, в Haskell ситуация с ошибками примерно та же, что и в Java, и в C#, и в большинстве остальных языков с исключениями. Коды возврата удобны для случаев, когда ошибка ожидаемая, частая и обрабатывается обычно по месту вызова. А в остальных случаях начинаются пляски и типами и конвертацией. А все "более сложные" случаи заметаются под ковер https://downloads.haskell.org/~ghc/6.10.1/docs/html/libraries/base/System-IO-Error.html.
Re[4]: Result objects - все-таки победили Exceptions?
_>И вопрос 5-летней давности всё ещё актуален — кто-то пробовал замерять, сколько сейчас все эти проверки жрут времени по сравнению с кодом без проверок но с исключениями?
Я, я! Правда, на Erlang'е, но сути дела не меняет.
С исключениями в среднем получается быстрее. Но при условии, что код писал разумный человек, и исключения бросались именно в исключительных ситуация, а не как часть control flow.
И заодно куда более читабельно. Без этой компиляторной магии с вопросительными значками, без самодельного создания stack trace, без двух десятков записей в лог (типа "не шмогла и на этом уровне, пробрасываю выше"). В общем, это правильное решение реальной задачи. Что бы там Растаманы не делали, а все равно в итоге получатся исключения. С квадратными ли, или восьмиугольными колесами — это уж как повезет. ИЧСХ, я уверен, что если колеса будет квадратными, это будет подано как фича: машине с такими колесами не нужен ручной тормоз, даже если парковаться на уклоне. И трогаться на уклоне без проблем, машина не откатится!
PS: читаю hi_octane, и прямо бальзам на душу. Я примерно лет 10 на этом форуме (и некоторых других, а также в тех компаниях, где успел поработать) высказываюсь аналогично. И казалось бы, ведь в FAANG должны быть семи пядей во лбу. Должны понимать такие вещи и без меня. Ан нет, даже 5 лет назад споры все еще случались, так что приходилось писать гайды на тему "почему bubble wrapping — плохо". Вообще интересно, кто все еще там работает на Erlang, остались ли все те гайды, что я писал, в силе, или кому-то нужно было нанести импакт, и что-нибудь поменяли.
Re[7]: Result objects - все-таки победили Exceptions?
S>Так сделайте наследника StreamException — в чем вопрос
Ага, и тут у разработчиков менеджера сертификатов дилемма — делать CertificateException наследником StreamException, или HTTPException, или послать всех нафиг
При правильном пробросе нижележащих исключений наверх, вызывающий код ловит те исключения, которые к нему реально относятся, и просто их обрабатывает. Заворачивание в обёртки имеет смысл только если вызывающий код абсолютно гарантированно должен быть абстрагирован от деталей.
На примере сериализатора — если вызывающий код сам же передаёт Stream в сериализатор, то вызывающий код вполне может (и даже должен) перехватывать исключения прилетающие от своего собственного Stream. А сериализатор может (и даже должен) пробрасывать эти "чужеродные" исключения наверх. Он их всё равно обработать не может, там снаружи лучше знают что с делать, если вылетел StreamVeryTiredException.
А вот если, на этом же примере, сериализатор внутри создаёт свой собственный BufferedMemoryStream, в который пишет временные данные, а потом сбрасывает эти данные в Stream переданный извне, то внутренние ошибки этого BufferedMemoryStream ни в коем случае не должны "утекать" наверх. Потому что снаружи никто про этот BufferedMemoryStream не знает, и знать не может. В этом случае наружу должен улетать SerializationException (даже не StreamWriteException, потому исключения в этом буфере такая же внутренняя деталь реализации).
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
_>Моду на коды ошибок вместо исключений ввели разработчики Rust, причём только потому, что не смогли совместить исключения и Borrow Checker. Других причин не было. Вся чушь, которую написали после: про скорость, или там надёжность с безопасностью — чистая идеологическая хрень. Написать "мы облажались, терпите" означало растерять фан-базу.
Ох, держите меня семеро. Это ж надо так на Раст наехать. Вы на нём не пишете? Может стоит сначала пописать/попробовать?
В Расте всё это обёрнуто так, что теперь это растаскивают во все языки, включая С++. Означает ли это что Растовцы облажались? Или может наоборот, нащупали что то в пыльной коробке?
Сахарок с вопросительным знаком отлично ложится в синтаксис, что это даёт: Вид ошибки прописан в прототипе функции Явно прокидываем ошибку, а не "я не знаю кинет оно или нет" что частенько приводит к сюрпризам в проде и перечитыванию доков. Просто нужно написать "?". При этом задумываешься. Ровно то же самое когда разворачиваешь optional<> (который тоже обретает лютую популярность в С++): всё что нужно сделать, написать "*", но задумываешься, ведь там объекта может и не быть.
В коде явно видно как ошибка обрабатывается.
Всегда есть опция обработать ошибку на месте, причём без специального очень шумного try/catch/finally/throw синтаксиса а просто получить тип и сматчить его как угодно — часто в одну строку (map, map_or_else, and, or, and_then, ...). Работа с обычной переменной.
Re[2]: Result objects - все-таки победили Exceptions?
Здравствуйте, Privalov, Вы писали:
P>Лень разработчикам указывать все исключения, которые могут прилететь. Я в своё время запарился с таким кодом работать.
Ну так испортить можно любую идею.
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, Pavel Dvorkin, Вы писали: PD>В современных IDE нет никакой проблемы, чтобы указать. IDEA просто не даст откомпилировать метод, в котором выбрасывается checked исключение, потребует либо поставить try-catch, либо добавить throw, и сама это сделает.
Примерно 25 лет тому назад парни из Редмонда проделали простое упражнение: посмотрели на кодовую базу реальных проектов на Java.
И внезапно оказалось, что почти все абьюзят checked exceptions. Они прекрасно выглядят в теории, но на практике никакая IDE не помогает решить их фундаментальную проблему: они запечены навечно в сигнатурах интерфейсов.
И вот тут оказывается, что если у нас на дне дерева вызовов возникает специфическая ситуация, которую мы хотим обработать поближе к корню, то у нас связаны руки.
Середина этого дерева знать не знает про эту специфическую ситуацию; её спроектировали в те времена, когда никому и в голову не приходило, что там внутри может что-то сломаться таким способом.
В мире checked exceptions у нас есть три плохих способа это решить:
1. Пройтись вверх по стеку вызовов и везде подправить сигнатуры, добавив MySpecificException к списку
Это зачастую невозможно (потому что в стеке — четыре библиотеки от семи разных поставщиков, из которых девятеро уже давно ушли с рынка и их код никто не маинтейнит), да и вредно (потому что часть этих библиотек используется ещё в сорока местах нашего проекта, которым совершенно неинтересна обязанность обрабатывать ещё и MySpecificException)
2. Отнаследоваться от какого-то исключения, которое в сигнатурах уже есть
Это если в сигнатурах есть хоть что-то, и это что-то — не final.
3. Завернуть MySpecificException в UniversalException
Это если в сигнатурах есть UniversalException, в котором предусмотрено место для InnerException
4. Выбросить потомка RuntimeException
В итоге практически реализуемым становится четвёртый способ, который сводит всю идею проверяемых исключений на нет.
Именно поэтому в дотнете нету checked exceptions.
А вот если идти по пути "result object", то мы остаёмся в поле традиционной системы типов.
Вот есть у нас библиотечная функция map, которая принимает коллекцию и трансформер f вида Func<T, R>. Нам совершенно всё равно, может ли возникнуть ошибка при вычислении f над конкретным экземпляром T или нет.
Потому что всё это "спрятано" внутри типа R. Если f — это функция 1/x, то она возвращает тип "number | undefined" (или там "number | DivisionByZero"). Тип результата map соответственно будет Iterable<number | undefined> и обязанность принять решение, что с этим делать, остаётся у того, кто вызывает функцию map, а не у автора этой функции. Ровно так, как этого хотели авторы идеи исключений.
А если f — это функция x ^ 0xF, то у неё тип результата — просто number, и вызов map с ней в качестве аргумента получит Iterable<number>.
Написать функцию IEnumerable<R> map(IEnumerable<T> source, Func<T, R> f) c проверяемыми исключениями не представляется возможным. В ней функция f не имеет права ничего выбрасывать, потому что map пообещала ничего не выбрасывать.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[5]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
_>Я в твоём сообщении не могу отделить ошибки от исключений. А хочется определённости — в итоге-то что? Парсеру json, если пришёл битый, что делать — кидать "просто ошибку" или исключение, которое приведёт к закрытию программы?
Не надо ничего кидать. Парсер json возвращает алгебраическую сумму из "ожидаемый тип объекта" и "ошибка синтаксиса".
Что с этим делать — решает пользователь этого парсера. Он может прямо тут же заматчить ошибку синтаксиса и, скажем, подставить дефолтный конфиг; или сделать retry, или просто прокинуть результат выше по стеку, делегировав принятие решения туда. Компилятор будет бить по рукам за попытку прочитать значение параметра из экземпляра типа "myConfig | BadSyntax".
_>Ну и чтоб два раза не вставать — выход за границу массива это ошибка или исключение, которое должно всё сломать? А невозможность выделить память?
Всё то же самое. Корректный тип у функции типа malloc<T>(n: int) — это не T[], а T[] | OutOfMemory. Поэтому перед тем, как пользоваться полученным указателем, программист вынужден его проверять. А не просто надеяться на то, что память всегда выделиться ("у меня никогда не возвращался нулевой указатель"), и получить SegFault в произвольно далёкой от malloc точке. _>Самый прикол, что решение "исключения существуют, и это нормально, главное что мы их можем перехватывать и обрабатывать" — даёт универсальный и единообразный подход к самым разным проблемам. А вот подход — "у нас в языке исключений нет, ну может чуть-чуть, на донышке" — приводит к тупику. Решение rust как раз в духе, в моём понимании, дибилизма — делайте всего по 2.
Не знаю насчёт Rust — никогда на нём не писал. Но вот моя практика работы с языками с исключениями — она как раз такая, что провоцирует людей писать в стиле "исключений не существует; бремя доказательства обратного лежит на QA". Это означает, что стоимость работоспособного кода очень сильно возрастает — первоначально написанный "оптимистичный" код уезжает в коммит, чтобы спустя дни и недели попасть в third-line support со словами "тут иногда что-то где-то крэшится, найдите и почините". Это гораздо, гораздо дороже, чем сразу заставить программиста написать код обработки.
Особенно если язык позволяет минимизировать бойлерплейт — например, при помощи автовывода типов.
Любой словарик из метода get(key: K) возвращает не V, а Maybe<V>, и нужно обязательно проверить, нашлось ли значение по ключу. А для типового случая есть перегрузка get(key: K, default: V), которая позволяет обойтись без паттерн-матчинга результата. Ну, или язык умеет в x: V = mydict.get(k) ?? defX; вместо x: V = mydict.get(k, defX);, и тогда даже и перегрузка не нужна.
То есть корректный код не сильно больше по объёму, чем некорректный, но зато он а) явный и б) надёжный.
Как по мне, так это именно то, к чему следует стремиться.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[8]: Result objects - все-таки победили Exceptions?
try {
<...100500 stack frames...>
totalAmount *= (1 - getDiscount());
<...100500 more frames...>
} catch (BadFileException)
...
catch (TooPoorException)
...
catch (NumberConvertException)
...
catch (Exception)
<тут какой-то совсем generic handler типа "не шмогла")
И не мой личный вкус это наиболее чистый вариант. Код обработки расположен там и только там, где оную обработку сделать можно. Нигде в других местах никаких ? нет.
Re: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>Но, как оказалось, народ идеи не понял. Так и вернулись к понятным дебилоидам кодам возврата, т.к. проверяемые исключения осилить не смогли. Так же и в Kotlin их решили не делать.
Дебилоиды это те, кто не понимает, что каждому инструменту своё применение.
Допустим, ProcessData() это дорогая операция (рендер на 12 часов для десктопной программы или облачные вычисления за 1000$ для серверной). А при попытке SaveData() происходит облом. Если это серверная программа, то база не соединяется, а если десктопная, то файловая операция не проходит. В этом случае надо вернуть код ошибки, чтобы алгоритм можно было переписать так (серверная программа):
while (NotOkay == SaveData(processedData) && Yes == AskUser("Попытаться обратно?"));
А если за каким-то хером внутри SaveData() оказывается, что processedData внезапно null или InvalidObject, то тогда у нас ИСКЛЮЧИТЕЛЬНАЯ ситуация. Весь алгоритм уже пошёл по звезде, и проверять результат бесполезно. В такой исключительной ситуации нужно выбрасывать исключение. Поэтому они и называются исключениями.
Единственная причина полностью отказаться от исключений — технические соображения. Лично я считаю, что в отсутствие виртуальной машины вреда от исключений больше, чем пользы. Хотя бы потому, что если у нас unmanaged OS и проект состоит из разных модулей, написанных на разных языках с разными стандартными классами исключений, их заколебёшься приводить к одному знаменателю. Да и нахрен не всралось исключение, если неперехваченное оно не даёт при желании хромать дальше с восьмой цифры (как бывает без ВМ).
Но поскольку дебилоидов слишком много, то и общаются они на уровне "а давайте всегда выкидывать исключения ДЛЯ КОНСИСТЕНТНОСТИ". Жопу бы им начать вытирать зубной щёткой. Для консистентности.
J>Ох, держите меня семеро. Это ж надо так на Раст наехать. Вы на нём не пишете? Может стоит сначала пописать/попробовать? J>В Расте всё это обёрнуто так, что теперь это растаскивают во все языки, включая С++. Означает ли это что Растовцы облажались? Или может наоборот, нащупали что то в пыльной коробке? Писал на нём ещё до того как это стало мейнстримом
. В 2024-м для продакшена не писал, но с надеждой слежу за обновлениями и иногда "играюсь" с новыми фичами. А знаю что облажались — потому что следил за разработкой с самого начала. Прям следил, читал борьбу разработчиков Borrow Checker, и сколько попыток было сделать нормальные исключения. В итоге получилось смешно — всю кривую "вдохновение->разочарование->использование" растом я прошёл с небольшим опережением. Из-за этого, когда на том же Хабре пошёл пик хайпа и статьи про раст полетели в ежедневном режиме, я был уже на стадии разочарования, и скептически язвил "вы ещё в ракете не смотрели"
J>Сахарок с вопросительным знаком отлично ложится в синтаксис, что это даёт:
Ага. Сначала разрекламировали проверку каждого вызова с match как оправданную плату за доселе невиданную надёжность. Потом сами же поняли что беда, добавили макрос try! в стандартную библиотеку. Потом поняли что и try! — хрень, и добавили "сахарок" с '?'. В 2020-м уже был этот "сахарок", сколько ещё лет нужно чтобы дошёл факт: если к каждому вызову функции нужно применять "сахарок" — то это косяк, ошибка создателей языка. В 90% случаев ошибку надо реально обрабатывать (т.е. что-то осмысленное делать), вовсе не прямо там где она происходит, а сильно выше по стеку. И требование "написать что-то обязательное именно там где ошибка произошла" приводит только к одному — 90% обработок превращаются в бессмысленные затычки и boilerplate.
Кстати, вследствии этого, тенденция очевидна: с каждой итерацией match->try!->'?' — код обработки ошибок всё больше пытаются минимизировать, замести под ковёр, скрыть. Что дальше? Я вот думаю — дальше макрос уровня функции, который сам расставит невидимые '?' везде где они нужны, и, внезапно (нет), мы получим те же исключения, только всё ещё убогонькие и на if-ах под капотом. А ко всему этому в чулане, за дверью которую нельзя открывать, будет ещё безумный родственничек закрыт — panic
И вопрос 5-летней давности всё ещё актуален — кто-то пробовал замерять, сколько сейчас все эти проверки жрут времени по сравнению с кодом без проверок но с исключениями? Или среди фанатов такие вопросы поднимать моветон?
J>Всегда есть опция обработать ошибку на месте, причём без специального очень шумного try/catch/finally/throw синтаксиса а просто получить тип и сматчить его как угодно — часто в одну строку (map, map_or_else, and, or, and_then, ...). Работа с обычной переменной.
I>А если catch только в конце — тогда как понять, что и где произошло, неужели stack разбирать?
В языках с исключениями никто не заставляет делать catch на каждом шаге. Оптимистично делают кучу связанных друг с другом по смыслу шагов, а если какой-то шаг вылетает — у исключения, в норме, есть вполне информативный тип, по которому проверяют, и решают как обработать. Один метод, который забирает из сети, парсит и сохраняет в файл — может не иметь ни одной строчки try, но бросать 3 типа исключений. И снаружи будет всего один код обработки всего сразу и в одном месте. А этот код уже смотрит: SocketException — проблемы с сетью, FileNotFound — проблемы с файлом, и т.д. Даже если исключение абсолютно неспецифичное — можно юзеру показать текст ошибки, а тот уже с помощью гугла найдёт что сделать, чтобы проблемная функция завершилась успешно. А иногда можно и ничего не делать, повторить операцию, и бывает прокатывает.
Этот подход, вообще-то, уже отлично проявил себя в базах данных. Начали большую транзакцию, не вышло — ну и откатили. Но выкати БД, с заявой "в нашей идеологии если транзакция не прошла — то ложится вся база", и, мягко говоря, люди не поймут
Кстати, мало кто знает, почему в Rust трейт std::Error (кто не в теме — аналог интрефеса для ошибки) вполне официально советует сообщение об ошибке давать в нижнем регистре, с минимумом пунктуации. А всё потому, что периодически информацию об ошибке только из сообщения и получают. И это не халтурщики безмозглые, а нормальные прогеры, но вот прилетает откуда-то из недр чего-то ошибка, которую совершенно по канонму наверх передавали с '?'. И никак её отделить нельзя, а обработать именно её особенным образом надо. И тогда, скрипя зубами, делают обработчик по тексту. Вот так вот. Но этого в рекламных материалах не скажут.
I>Try catch на каждом шаге — мало того, что громоздко, оно вроде ещё и по части производительности так себе.
Именно что вроде. Как только начинают проверять реальный код — цифры могут быть очень разными. И программы с исключениями, и программы с кодами ошибок пишутся оптимистично — т.е. из расчёта что ошибок будет минимум (а исключений, соответственно, будет мало). При этом в случае кодов ошибок — проверки идут постоянно, и на каждый чих резервируется память в стеке (потому что место для хранения ошибки должно быть всегда). А в случае исключений — каждый чих оборачивать в if и return нет никакой нужды. Что работает быстрее? А зависит от. Мерять конкретный код надо. Когда придумывали миф что исключения медленные — сравнивали не реальные программы, а выброс, условно, миллиона исключений, с миллионом выходов с ошибкой. Но это нереалистичные сценарии. Никто не кидает исключения в цикле миллионами. Код пишется оптимистично. И то что try/catch выписывать в каждой строке неудобно, как раз подстёгивает использовать их для исключительных случаев.
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, vsb, Вы писали:
vsb>Я не отрицаю некоторого смысла во всей этой идее проверяемых исключений, но конкретно в Java реализация фатально плохая. И я не уверен, что знаю, как сделать лучше.
Основная проблема не в исключениях самих по себе, а в том, что они слишком "другие" и плохо совмещаются с другими фичами ЯП.
Например, был у тебя for-цикл, решил переписать на stream с лямбдами и checked исключения, внезапно, через лямбду уже не передать.
Распараллелить кусок кода, и вот теперь у тебя проблема как пробрасывать исключения между тредами и что с ними делать потом.
В зависимости от контекста одна и та же операция может иметь разную "серьёзность". parseInt в каком-то участке кода — это жуткая проблема и надо сразу падать, а где-то это норма и требуется быстро парсить в цикле. В итоге приходится два варианта функций писать: parseInt throws и tryParseInt с кодом возврата.
С популяризацией подходов функционального программирования с исключениями чаще приходится бороться, чем извлекать пользу.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, Marty, Вы писали:
M>Здравствуйте, Константин Б., Вы писали:
_>>>Моду на коды ошибок вместо исключений ввели разработчики Rust
КБ>>Так облажаться в первом же предложении. Result objects — это не коды ошибок. КБ>>Когда вы уже поймете что ваш сишный опыт абсолютно уже не релевантен.
M>Как я понял, это код ошибки + полезный результат, упакованное вместе. Те же яйца, вид сбоку
Неправильно понял. Это не код ошибки, это объект ошибки. Т.е. как исключение только явно возвращаемое из функции.
Совершенно лишенное недостатков кодов ошибок.
Re: Result objects - все-таки победили Exceptions?
S>Но, как оказалось, народ идеи не понял. Так и вернулись к понятным дебилоидам кодам возврата, т.к. проверяемые исключения осилить не смогли. Так же и в Kotlin их решили не делать.
Я думаю концепция GetLastError()/errno недооценена
Но ее надо чутка расширить, параметром facility.И API сделать таким:
Поясню, с классическими кодами ошибок проблем две
— это всего лишь код
— бойлерплейтный код для их проверок везде подряд
С исключениями эти проблемы решены, но есть другие две
— редкий бойлерплейтный код чтобы их ловить, а если забудут словить — все падает, от чего у джуниоров пригорает и они бегут в раст
— на многих системах они пц тормозные (если используются), но ради справедливости — они почти что zero-overhead если не используются.
Подобные аспектно-ориентированные last-error коды ошибок не имеют ни одной из этих проблем. Я у себя юзал такой подход, очень удобно. Типа пишешь в файл длинную сериализацию чего либо, и в конце просто проверяешь — все ли записи в файл прошли гладко. Удобно.
Для любителей ООП, возможна вариация — вместо глобальной SetError/GetError сделать их методами интерфейс ErrorHolder, который реализуется всеми объектами, которые могут совершить ошибку. И тогда код с сериализацией в файл будет выглядеть так:
FileObject fo("/foo/bar", "w");
fo.WriteString("foo");
fo.WriteString("bar");
fo.WriteInteger(123);
fo.Flush();
if (fo.GetError()) ShowMessage(fo.GetError(true)->Text);
Как много веселых ребят, и все делают велосипед...
S>Не вижу проблемы, если указание типов опционально. То есть хочешь — и всё едет. Не хочешь — пишешь явно "здесь должно быть вот так", и дальше этой строчки изменения не поползут.
Я разрабатывал язык в котором по умолчанию любая функция шаблонная, которая по контексту определяет какие у неё параметры, сколько их и что она возвращает. Так вот это полный треш когда изменение в одной функции по цепочке приводило к тому, что компилятор что-то своё додумал в другом файле проекта и по итогу в лучшем случае ошибка компиляции, в худшем просто хрень выводится.
А ещё малозаметные проблемы когда компилятор из-за незначительных нюансов для двух наборов параметров навыводил разные перегрузки (ну типа в одном случае он решил передавать копию потому что объект временный, а в другом ссылку), и в бинарнике появляются две почти одинаковые функции. Раздувание бинарника на пустом места.
В языке был механизм явно указывать сигнатуру, который изначально был нужен лишь для экспорта функций. Но на деле оказалось что всю бизнес-логику тоже намного проще писать явными сигнатурами, потому что тогда компилятору меньше работы, и если он что-то понял не так, то он сообщает об этом максимально близко к источнику проблемы.
S>Ну, вот это и мешает писать нормально.
А, то есть значок | грубо говоря в зависимости от операндов делает либо
— множество из двух элементов
— множество с добавленным элементов
— объединение множеств
А там может оказаться алгебраическая сумма int64_t | long long int? Или это таки один тип?
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Здравствуйте, Sinclair, Вы писали:
S>Кстати, есть отдельный набор вопросов, не относящихся к теме топика, про ваш язык и опыт его разработки. S>1. Есть ли где-то публичное описание этого языка? S>2. Поверх чего он исполняется (JVM, WASM, натив)? S>3. Компилятор или интерпретатор? S>4. Каким тулчейном вы пользовались при его разработке? Языки программирования, библиотеки/фреймворки для парсинга/семантического разбора?
Попробую ответить на всё сразу. Нормального описания при мне не было, язык мог внезапно измениться на уровне работы базовых бильтинов, при этом мы всей командой тратили день на адаптацию кодовой базы.
При разработке использовали компилятор, написанный на нём же самом. Вернее это был транслятор в текстовый LLVM-IR. Исходники старой версии тут: https://github.com/Matway/mpl-c/tree/master
Язык стековый как Форт. То есть обратная польская запись, стек сущностей, ну и все функции описываются как операции над стеком. Поэтому у него очень маленький синтаксис и минимальный набор конструкций. Очень важно что стек есть лишь при компиляции.
Базовые сущности языка — это числа, строки, блоки кода, анонимные кортежи, именнованные кортежи, и все они могут быть положены в виртуальный стек, который существует лишь на этапе компиляции. Ещё есть слова. Всё остальное строится из них. Любое слово производит какие-то манипуляции над "стеком времени компиляции". Например, слово if со стека снимает булевую переменную и два блока кода, а кладёт результат выполнения блоков кода. При этом если компилятор не может вычислить условиe if то он считает его динамическим и требует чтобы блоки кода одинаково действовали на стек — ну иначе стек станет частью рантайма. Для циклов тоже есть аналогичное требование, что если цикл "динамический" то он должен сохранять стек.
Можно заводить переменные, само собой. Причём переменная может быть блоком кода и передана куда-то в другую функцию. И это на самом деле основной способ задания функций в языке.
Язык позволяет делать адскую метушню, например написать функцию switch, которая принимает анонимный кортеж, в котором по очереди идут блоки кода и константы (например
(1 [doSmth1] 2 [doSmth2]) condVar switch
), и прямо в процессе компиляции превращает его в лесенку ифов
которая оптимизаторов шланга превращается в честный свич
В языке вообще сделан жёсткий упор на то, чтоб всё нахрен вычислить во время компиляции. И то, о чём я говорил про то, что сигнатура функции определяется по аргументам:
foo: [ a: b: ;; // снять со стека две сущности и завести локальные переменные
a 1 = [
c:; // снять со стека ещё сущность
] [] if
];
Тут мы видим if у которого ветки по-разному действуют на стек, поэтому он может скомпилироваться лишь при статически известном условии. При этом содержимое "ложной" ветки игнорируется и может содержать вообще неизвестные имена и невалидные конструкции.
Так вот, если вызвать код так:
1 2 3 foo // первая переменная 1, поэтому создастся инстанс foo который жрёт 3 аргумента
2 3 foo // первая переменная на 1, поэтому создастся инстанс foo который жрёт 2 аргумента
i: 42 dynamic; // создаём переменную i в которой лежит 42 и просим компилятор не делать с ней никаких вычислений времени компиляции
i 2 3 foo // а тут компилятор пытается создать инстанс для foo у которого первый аргумент неизвестен при компиляции. Правда в данном случае это не получится, потому что внутри находится иф, у которого разные ветки по-разному действуют на стек. Так что будет ошибка компиляции
Ну и это вот слово dynamic на самом деле приходилось пихать везде, кроме того 1% кода где реально нужна метушня. Потому что мне похрен на то, что статически известное значение аргумента позволит развернуть один из внешних циклов, даже близко не влияющих на производительность. Зато компилятор будет дольше страдать компилируя нахрен не нужный инстанс, это раздует бинарник, зачем оно.
В целом язык прикольно поиграться, но для больших проектов не подходит ну ваще никак.
Причём часть проблем являются принципиальными и более лучший умный компилятор их не решит. И часть из них я называл ранее. А ещё из-за шаблонности всего-всего ни о каком нормальном автодополнении не может быть и речи. То есть после вызова другой функции ты не имеешь никакого представления что у тебя лежит на стеке, и куда дальше пойдёт выполнение и какие ветки проигнорятся. И чтобы это узнать, надо полностью проанализировать вызываемую функцию. Из-за этого крайне сложно делается рекурсия для функций у которых непонятно какой тип возвращаемого результата, потому что если функция вызывает саму себя, то для того чтобы понять, что дальше вообще лежит на стеке после вызова, надо проанализировать полностью всё тело этой функции, но мы же как раз находимся в процессе. Приходилось делать "полускомпилированные функции" у которых есть "предположительный тип результата" и на основе этого предположения компилировать функцию ещё раз и доходить до "неподвижной точки".
А с автодополнением в лучшем случае студия выдаст просто список ВСЕХ методов всех объектов видимых из этого файла.
Ну и раздельная компиляция идёт нафиг, потому что изменение тела одной функции может внезапно повлиять на компиляцию чего угодно в любом другом файле.
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
_>И вопрос 5-летней давности всё ещё актуален — кто-то пробовал замерять, сколько сейчас все эти проверки жрут времени по сравнению с кодом без проверок но с исключениями? Или среди фанатов такие вопросы поднимать моветон?
https://youtu.be/-pE3T6wsIrM?si=BoUaLvTtE0b06xG2&t=989
вкратце, если исключения не кидать, то коды возврата в два раза медленнее. Если кинуть, то можно сразу стреляться. Мало того, что выброс исключения аллоцирует память в куче, так ещё и захватывает глобальные мьютексы и на большом количестве потоков происходит существенная деградация.
Re[14]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, korvin_, Вы писали: _>>Что именно? OOM там не ловится никак. То же самое с SO — это внутренняя вещь рантайма. S>Ну, то есть panic. Скажем так: в такой среде сложно писать софт, который должен работать 24*7.
Вот интересно, а что вы делаете в Java при получении OOM?
_>>В остальном — по-разному. Например, функция div имеет тип (Integral a => a -> a -> a) без указания какого либо исключения деления на 0, но исключение может возникнуть. S>Хм. То есть кидать можно всё, что угодно и где угодно? S>Я посмотрел вот сюда. Написано S>
S>The great thing about Haskell is that it is not necessary to hard-wire the exception handling into the language.
S>То есть исключений нет, но если хочется прикрутить их поддержку, то можно их реализовать самостоятельно. S>Читаем дальше: S>
S>Haskell solves the problem a diplomatic way: Functions return error codes, but the handling of error codes does not uglify the calling code.
S>Там написана неправда?
Там написано не про это. Насчёт does not uglify the calling code я бы поспорил.
_>>Ну не совсем.
S>>>А что произойдёт, если хаскелл не смог сконструировать Add из Expr и Expr из-за OutOfMemory или StackOverflow? _>>Упадёт. S>Как-то грустненько.
А что вы предлагаете делать при OOM?
S>>>Почему нельзя было просто добавить OOM в определение Expr как одной из альтернатив?
_>>Потому что OOM — это не часть Expr. Сигнатуры в примере с OOM вымышленные. _>>Что нам даст добавление OOM в качестве альтернативы? _>>Эти альтернативы -- просто конструкторы значений. S>А выглядит как алгебраический тип.
Это алгебраический тип, но при чём тут это?
_>>Всё равно, что в Java написать
_>>
_>>var oom = new SomeInterface() {
_>> ...
_>>};
_>>
_>>Как это поможет? S>Поможет не писать везде Expr | OOM, а встроить OOM в Expr.
И что с ним делать в Expr? Давайте добавим. Как это отразится на expr0? Что вообще дальше делать с этим OOM?
_>>
_>>expr0 = If (Mul ...) ... -- где здесь OOM?
_>>
S>В рамках Haskell, наверное, нигде. Язык не готов к работе в условиях дефицита памяти
Где бы он был в Java?
_>>IO так и делает: бросает исключения без указания в сигнатуре. S>Ну, то есть в IO встроена возможность бросать всё что угодно? откуда в div появилась возможность "бросить" divisionByZero?
Здравствуйте, Sinclair, Вы писали:
_>>Это в теории, на практике -- нет. Даже в С/C++ и прочих языках с "ручным" контролем памяти что-то сделать сложно. Поэтому приходит OOM-Killer S>OOM-Killer — это следствие криворукой архитектуры линукса. Программы под винду пишутся на тех же С/С++, но при этом там нет (и не нужно) никакого OOM-Killer.
1. Это не так. Там точно так же может быть переполнение по copy-on-write или по аллокации, вызванной неконтролируемыми затратами из ядра.
Это происходит реже, поэтому не все видели такую ситуацию. Но если происходит, то выйти из неё в разы сложнее — потому что нормальная защита в системе вообще не предусмотрена.
Лучше бы OOM killer был. Но ещё лучше, конечно, если бы в юниксах делали более умную обработку ситуации, а не просто убивать. Вариант с резервом я предлагал здесь.
2. Как минимум в линуксе предусмотрен костыль — overcommit policy может быть выставлено в режим "всегда полностью резервировать". При этом, да, потребуется в надцать раз больше свопа чем рамы, потому что слишком много обычных прикладух уже привыкли аллоцировать огромным куском наперёд.
S>Повторюсь: пример зависит от области деятельности и от того, где именно вылетел OOM. Если я получил OOM при создании шестибайтового объекта — да, скорее всего продолжать вообще смысла нет, и можно просто перезапускать весь контейнер или даже всю ноду. А если я пытаюсь выделить буфер размером в 100М, то шанс велик, что после вылета ООМ у меня всё ещё ~50М свободно. И можно заняться чем-то другим, не столь прожорливым к памяти. Или попробовать выделить буфер вдвое меньше.
Ну если тебя уже пристукнули по OOM, то продолжать сложно.
S> Ну, как пример — сортируем файл интов размером в 100гб. Понятно, что нужно делать сортировку слиянием; и понятно, что чем больше размер чанка, тем быстрее мы закончим.
Плохой пример, как ни удивит тебя. Во-первых, можно сделать просто mmap() файла в память и напустить какой-нибудь стандартный qsort, и он за счёт последовательных движений достаточно неплохо будет оптимизирован с точки зрения менеджера памяти. Во-вторых, массовый переход на timsort и тому подобные штуки показывает, что "чем больше размер чанка" уже давно не актуально. Это во времена написания TAOCP, возможно, было так.
Я бы взял в качестве примера что-то посерьёзнее, типа умножения матриц.
(про Optional уже обсудили)
The God is real, unless declared integer.
Re[7]: Result objects - все-таки победили Exceptions?
·>Это от парадигмы зависит. Вызов метода в ООП — это и есть message passing.
Разве что в теории. На практике, в известных мне реализациях ООП вызов осуществляется синхронно, и синхронно же ждется, пока вызов что-то вернет. Это уж совсем не message passing.
·>В терминах ООП — это уже какой-нибудь декоратор.
Такой декоратор должен быть потокобезопасным и весьма изощренным. То есть, ожидаемо, он должен реализовывать половину того, что может быть реализовано общим образом в синтаксисе и рантайме языка.
·>Проблема исключений, что они нелокальны. Действия в одном куске кода внезапно перескакивают в совершенно другой кусок кода, без явной связи между скопами.
Это не проблема, а назначение! Правила "перескакивания" (та самая явная связь) между областями ответственности как раз и задаются с помощью синтаксиса языка. А где язык этого не может, городят декораторы.
·>error handling — одна из самых сложных проблем программирования.
Ну, ей далеко до нейминга и кэшинга!
Re[9]: Result objects - все-таки победили Exceptions?
Э нет. Message passing по определению async. Как только у тебя появился wait, да к тому же строго ограниченный определенным предыдущим message, то все, это уже никакой не message passing.
·>А зачем обязательно в синтаксисе?
Потому что иначе получается фантастическое уродство, типа как в Go. Смотри, как элегантно можно в Erlang:
actor2 ! {do_smth, arg, 2},
actor3 ! farewall,
receive
{success, Result} -> Result;
%% а тут можно "поймать" исключение из связанного актора, например, так - {'EXIT', Reason} -> <обработка>
after 1000 -> throw {"timed out"}
end.
Очень выразительно. Мне недавно пришлось примерно такое на C# писать, так там 4 экрана кода и всякая магия c DI.
·>Угу, назначение. Как и у оператора goto, например. Проблемой это быть не перестаёт.
У goto проблема в отсутствии (enforced) правил поведения. У исключения такие правила есть.
Re[5]: Result objects - все-таки победили Exceptions?
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Мы же пишем код очень низкий по стеку вызовов.
Мы — это кто? В реальном ентерпрайз-коде типичная глубина стек-трейса — пара сотен фреймов.
PD>А это чаще всего означает, что мы вкладываем некую новую функциональность, которая изначально не была заложена во всех этих четырех библиотеках от семи разных источников. Ну например, работа с данными, которые поступают из файлов, которые лежат в какой-то встраиваемой ФС, а мы решили встроить новую ФС, а в ней нам нужно завести какое-то исключение, которого ни в одной из существующих ФС никогда не было. Такая уж странная ФС.
Ничего странного. Например, предкам не приходило в голову, что данные могут лежать не на диске, а в клауде.
PD>Если его и впрямь нельзя уложить в одно из исключений ранее существовавших ФС, то это значит, что мы работаем с чем-то принципиально новым. И тогда понятно, что прежний код (все четыре библиотеки от семи источников), в общем-то, не рассчитан на это нововведение. И обработать эту ошибку он не может (вариант с его переписыванием в виде (1) не обсуждаем). А вообще-то должен бы именно он это делать. А мы фактически хотим пробросить эту ошибку, минуя все слои, которые могли бы ее обработать, на один из верхних уровней, где обработать ее по существу едва ли удастся, удастся лишь признать неудачу.
С чего бы это? На самом верху — наш код, он знает, чем retryable HTTP error отличаются от non-retryable.
в самом низу — тоже наш код, он как раз порождает разные виды исключений, в зависимости от кода HTTP-респонса.
А вот в середине — треш, угар и содомия возрастом 30+.
PD>В самом деле, ну поймали мы ее там, и что дальше ? Разве что повторить операцию с какими-то изменениями... А это и значит, что возникло исключение, которого код не ждал. То есть фактически unchecked exception. И тут действительно можно выбросить RuntimeException.
Ну, так проблема — в том, что компилятор теперь нам ничего не говорит о том, где это исключение нужно ловить. Нам показалось, что мы его перехватили, а на практике оно вылетело куда-то вовсе не туда, где мы поставили catch.
Это и есть подрыв всей идеологии checked exceptions.
PD>Подозреваю, что не поэтому, а потому что унаследовались от C++, а в нем тогда (не знаю как сейчас) throws в сигнатуре метода ровным счетом ничего не делало. Так, для красоты...
Нет. Никто там от С++ не наследовался. И подозревать причины нет — все ходы записаны, включая ход мысли отцов-основателей.
PD>А с uncheked exception другая проблема. Я с ней именно на C# когда-то столкнулся. Все сделано по документации, все работает, и вдруг вылетает XyzException. В документации ни слова о том, что оно может быть выброшено. Да это и неудивительно, так как оно выбрасывается не вызываемым методом самим, а методом третьей из 4 библиотек семи разработчиков, о которых вызываемый метод и не знает ничего. Ну ладно, чертыхнулся, написал для него catch (хорошо, хоть было понятно, что за исключение и что делать) и задумался — а какие еще исключения этот метод может выбросить не сам, а в одной из этих 4 библиотек ? Дай ответ... Не дает ответа.
В Java у вас будет ровно то же самое. Как раз потому, что в сигнатуре ничего нет, и вдруг вылетает какой-то XyzException, отнаследованный от RuntimeException.
PD>С этим согласен.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[16]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, korvin_, Вы писали: _>>Вот интересно, а что вы делаете в Java при получении OOM? S>Смотря какой код у меня исполняется. Есть применения, где при получении OOM мне нужно не просто 500 Internal Error, а успеть сделать что-то полезное (например, сохранить стейт воркфлоу) перед тем, как умереть.
Это возможно только если под этот твой процесс сохранениня стейта уже была выделена вся необходимая для его работы память, что в языках с GC практически невозможно сделать, соответственно твоё "сделать что-то полезное" точно так же наткнётся на ООМ.
_>>А что вы предлагаете делать при OOM? S>Много что можно сделать, особенно если озаботиться заранеешным созданием объектов, которые потребуются для обработки OOM. Например, можно урезать осетра и попробовать продолжить с буфером меньшего размера (хоть и медленнее).
Это в теории, на практике -- нет. Даже в С/C++ и прочих языках с "ручным" контролем памяти что-то сделать сложно. Поэтому приходит OOM-Killer
Поэтому на практике, что можно сделать: упасть. Гипервизор/оркерстратор перезапустит ноду/сервис/под/контейнер, а мы будем изучать логи, метрики, дампы, и по результату, либо добавим больше памяти, либо изменим параметры масштабирования, либо починим утечку, либо оптимизируем код.
_>>Это алгебраический тип, но при чём тут это? S>То, что OOM могло бы быть одним из его слагаемых.
Так и что бы это дало? Как его использовать? Напиши пример.
_>>И что с ним делать в Expr? Давайте добавим. Как это отразится на expr0? Что вообще дальше делать с этим OOM? S>Обрабатывать или прокидывать. S>Ну, вот что нам делать с None в Optional<T>? Как замена int на Maybe<int> отразится на арифметике? Всё зависит от того, как сделан лифтинг в языке — автоматически, монадически, или никак.
Так Optional -- это другой тип по отношению к int. Теперь, сам Optional же тоже требует памяти, значит Optional<Optional<T>>, и для него тоже нужна память, Optional<Optional<Optional<T>>>, и так далее.
_>>Где бы он был в Java? S>В Java он бы вылетел в виде unchecked exception. В воображаемой Java без исключений, но с not-nullable reference типами мы могли бы заставить new возвращать не T, а T?. В воображаемой Java с алгебраическими типами и наследованием мы могли бы заставить new T возвращать не T, а T | Error, и для общения с кодом, который требует чистый T этот результат пришлось бы очищать.
Так вот в Haskell/Ocaml и т.п. у нас нет new T, у нас сразу T, как 1, 2, true, false, "foo", "bar". Сразу значение.
Re[15]: Result objects - все-таки победили Exceptions?
·>Я не очень понимаю. Как на ерланге написать код вроде
Язык позволяет и result objects, и exceptions, так что — вот пример.
У здорового Эрланг-программиста код будет выглядеть так:
add(Left, Right) ->
parse(Reft) + parse(Right).
Однако есть довольно заметная секта поклонения тайпчекерам/result objects, у них будет так:
add(Left, Right) ->
case parse(Left) of
{ok, ParsedLeft} ->
case parse(Right) of
{ok, ParsedRight} ->
ParsedLeft + ParsedRight;
{error, Reason} ->
{error, {right, parse_failed, Reason}}
end;
{error, Reason} ->
{error, {left_parse_failed, Reason}}
end.
Я уж и не знаю, как от этого отучать "умудренных опытом программистов на <.вставьте свое.>".
·>Ну вот в случае parse — где им положено?
Там, где эту ошибку можно обработать. Очень часто это на самом высоком уровне, вплоть до "упасть с сообщением об ошибке". Обычно оно так и есть, но в некоторых случаях бывает, например, что можно попробовать другой парсер (в качестве fallback).
·>Про erlang я очень плохо знаю. Поэтому не очень понял суть этого высказывания.
Концепция простая, если разобраться. Представь, что для каждой функции ты можешь задать, сколько раз ее retry'ить, и что делать, если в течение N секунд происходит более M ретраев (классический вариант — ретрайнуть функцию уровнем повыше). Мда, корявое вышло объяснение. Для более осмысленного, лучше прочитать диссер Джо Армстронга, про error handling как раз — https://erlang.org/download/armstrong_thesis_2003.pdf
·>Так сложно вести беседу. Получается: "А вот исключения такие-то", а ты "но вот в криво отформатированном коде всё плохо". Отформатируй код по-другому. Делов-то..
Исключения позволяют писать более простой и понятный код. Как только начинается boilerplate в этих result objects, да умноженное на "копроративное форматирование кода", получается швах.
Здравствуйте, T4r4sB, Вы писали: TB>OOM-Killer это лучшее что можно сделать если кто-то пережрал памяти. Я очень сильно страдаю на домашней винде из-за того, что в винде его нет. TB>Тупая винда вводит в состояние комы всю систему нахрен вместо того, чтобы убить один процесс, это может быть браузер, в котором чисто случайно uBlock затупил на рекламном блоке, это может быть моя программа, в которой я неверно задал параметры модели. TB>В гугле по запросу "Windows OOM Killer" выводится "он там не нужен потому что там очень умная система выделения памяти".
Всё верно написано. TB>Тухлая лапша из говна, чтоб вешать лохам на уши. Я с завидной регулярностью ощущаю, как он якобы "не нужен".
Хм. У вас, очевидно, талант. Я на домашней винде в последний раз видел что-то подобное настолько давно, что и не помню.
Подозреваю, что вы можете улучшить ситуацию, сняв галочку "Allow system to manage page file size".
И я очень сомневаюсь, что в линуксе в аналогичной ситуации вам бы помог OOM-киллер.
TB>Причём тупорылые дауны из микрософта до сих пор сука не догадались сделать сочетание Ctrl+Alt+Delete самым приоритетным, которое сразу останавливает нахрен всё и выводит диспетчер в максимально экономном режиме.
Я обычно просто нажимаю Ctrl-Shift-Esc.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
— все-таки топят за Result objects для бизнес-логики.
А ведь это всю цепочку поддерживать. Как-то много лишних букв добавляется.
Но! Exception как бы не определены в контрактах. Если ResultObject — это контракт, его понятно что нужно проверить и компилятор даже контролирует эту проверку. А Exception разве что в документации описан, которую могут не читать.
А ведь было же хорошее решение — т.н. проверяемые исключения в Java. Когда компилятор требовал проверки того или иного исключения, но так же была возможно обернуть в RuntimeException, если оно утратило смысл бизнес-логики или ожидаемого
Но, как оказалось, народ идеи не понял. Так и вернулись к понятным дебилоидам кодам возврата, т.к. проверяемые исключения осилить не смогли. Так же и в Kotlin их решили не делать.
S>Так в Java есть не проверяемые — это наследники RuntimeException.
Делить исключения на проверяемые и нет — ошибка. В разных частях проекта даже одинаковые исключения могут быть как проверяемые так и нет.
Re[4]: Result objects - все-таки победили Exceptions?
TB>try {
TB> var result = some_func();
TB>} catch (error) {
TB> // kak-to obrabotat
TB> throw error;
TB>}
TB>на каждый чих — это бест практис
Если нет обработки — то просто возвращаете результат. А в случае с кодами ошибок — вы возвращаете обертку. Постоянно перед глазами бойлерплейт код этой обертки.
Re[2]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
_>Моду на коды ошибок вместо исключений ввели разработчики Rust, причём только потому, что не смогли совместить исключения и Borrow Checker. Других причин не было. Вся чушь, которую написали после: про скорость, или там надёжность с безопасностью — чистая идеологическая хрень. Написать "мы облажались, терпите" означало растерять фан-базу.
Мне кажется, это прямое влияние Go. А там оно из Alef-а, который прямой предшественник Go.
И сделали они это так, потому, что им исключения не нравятся по сути своей. Тем, что разрывают поток исполнения программы, делая его нелинейным, а потому трудным для понимания человеком.
_>Деды программирования наелись кодов ошибок по полной задолго до нас.
Go/Alef — это даже не деды, а прадеды. Ричи, Томпсон, Пайк...
Re[2]: Result objects - все-таки победили Exceptions?
Здравствуйте, Privalov, Вы писали:
P>Лень разработчикам указывать все исключения, которые могут прилететь. Я в своё время запарился с таким кодом работать.
В современных IDE нет никакой проблемы, чтобы указать. IDEA просто не даст откомпилировать метод, в котором выбрасывается checked исключение, потребует либо поставить try-catch, либо добавить throw, и сама это сделает.
With best regards
Pavel Dvorkin
Re[3]: Result objects - все-таки победили Exceptions?
S>Так нельзя писать. Во-первых, всегда нужно указывать типы исключений. Но беда в том, что вы обычно не думаете об этом — у вас нет четкой картинки в голове какие исключения может вызвать функция. Т.е. вы теряете контроль. А у меня всегда 100% я знаю какие исключения возможны, я всегда это держу в голове для каждой функции.
S>Далее. Исключения нужно отлавливать для каждого шага отдельно — т.е. не все в куче — а везде, на каждом из шагов где могут возникнуть исключения — нужно конкретные отлавливать и обрабатывать, если возмжно.
Исключения нужны, чтобы прятать обработку и "спрямлять" поток выполнения. Это, кстати, роднит их с async/await, которые тоже "спрямляют" поток выполнения. И как и в случае с async/await, тут тоже огромный простор для злоупотреблений. (Все пишут про callback hell. Надо порвать шаблон и как-нибудь написать про async/await hell, который читается хуже, чем парочка честных колбеков).
Иногда не надо скрывать проверки — когда они часть нормального потока выполнения, алгоритмически. Тогда исключения нахрен не нужны. Если делать все проверки в общем catch, будет потеря результатов (как показано в примере выше). А если каждую строчку завернуть в try, просто вывернешь программу наизнанку, с потерей читаемости, и больше ничего.
Между прочим, пример выше я взял из одной статьи на Хабре. Её написал программист, а не астронавт. Говнокодер, правда, зато по космосу не летает. Он так и пишет: я хочу написать простой прямой алгоритм в три строки:
const data = LoadData();
const processedData = ProcessData(data);
SaveData(processedData);
чтобы видеть, что происходит. С обработкой ошибок где-нибудь в подвале, чтоб не мешалась. А противный Go мне не даёт! И поэтому я сконструлил вот такую хохоряшку... (далее следует издевательство над Go).
Что, по-моему, доказывает, что быть говнокодером лучше, чем астронавтом. Говнокодер всё понимает, но делает неправильно. По крайней мере, про спрямление потока он понимает точно, и даже может найти для этого грязные хаки. Со временем жизнь его научит так не делать, а делать правильно. А астронавт... "Воровка никогда не станет прачкой".
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
_>Самый очевидный пункт: допустим у нас есть сериализатор, который пишет в абстрактный Stream. Этот Stream может быть диском, файлом, памятью, и т.п. Какие исключения объявить сериализатору, если он сам не знает, какой Stream ему дадут на вход? Заставить все Stream кидать абстрактный StreamWriteException?
Конечно StreamWriteException — это очевидно, причем во внутрь в innerException уже вкладывают то что породило данное исключение. В этом и есть смена акцентуации.
На более низком уровне — это может быть проблема с диском или обрыв подключения — но в данном контексте это вторично (т.е. в inner).
_>А почему совершенно не имеющие отношения к сериализатору классы, вдруг должны его учитывать и от него зависить, причём зависеть неформально?
Не должны зависеть — они могут выбрасывать наследника StreamExceptin, а сериализатор перехватывать все наследники и пихать в innerException своего StreamWriteException.
_>И это только один из сценариев.
Этот сценарий я разобрал и показал как правильно делать, давайте далее.
_> А есть ещё версии (например NetworkStream может начать поддерживать новый протокол, и кидать новые типы исключений), и так далее.
Слышали ли вы что-нибудь про наследование?
Re[5]: Result objects - все-таки победили Exceptions?
S>На более низком уровне — это может быть проблема с диском или обрыв подключения — но в данном контексте это вторично (т.е. в inner).
Так проблема в том, что лазить по дебрям Inner Exceptions неправильно. В какой-то день писатель сериализатора захочет разделить его на 2 компонента — собственно писатель в стрим, и форматтер. И бах — у нас уже иерархия из 2-х Inner Exceptions, потому что каждый на своём уровне что-то во что-то завернул. Или для скорости форматтер теперь многопоточный — и у нас InnerException стал AggregateException. К любому такому сюрпризу вызывающий код готов не будет. Проверит что в InnerException неведомая хрень, и либо сломается от неожиданности там где ломаться не надо, либо проигнорит.
S>Не должны зависеть — они могут выбрасывать наследника StreamExceptin, а сериализатор перехватывать все наследники и пихать в innerException своего StreamWriteException.
InnerException это вообще штука информативная только для разработчика на работе. Чтобы разбирать тихонько лог, и думать что сделать чтобы так падало пореже. Если внешний код серьёзно начинает полагаться на разбор InnerException — беда неминуема.
S>Этот сценарий я разобрал и показал как правильно делать, давайте далее.
Только один нюанс — если мы передаём сериализатору абстрактный Stream, который может кидать любые невиданные ранее исключения, а заворачивает их в StreamWriteException сам сериализатор, то все объявления в секции checked у Stream не имеют смысла. Потому что сериализатору в общем-то пофиг что там, он всё равно с этим сделать ничего не может, и вынужден заворачивать в свою обёртку всё, что бы там ни было. И сериализатор тут только простой наглядный пример. Так-то во всех местах где один код вызывает другой, и есть какая-то иерархия классов или интерфейсов — секция checked начинает вот так размазываться до бесполезной. Что и произошло с Java.
S>Слышали ли вы что-нибудь про наследование?
Слышал. Вот есть базовый Stream, у него checked фиксированный набор исключений. И вдруг появляется новый NetworkStream, которому надо добавить новый CertificateException. А добавить низзя: базовый стрим менять не положено — на его checked уже весь клиентский код завязался. У пользователей уже свои checked зависят от тех checked что были у старого Stream. Приехали. В Java это проблему решили "элегантно" — забили на checked. Могли бы иначе — решили бы иначе.
Re: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>Но, как оказалось, народ идеи не понял. Так и вернулись к понятным дебилоидам кодам возврата, т.к. проверяемые исключения осилить не смогли. Так же и в Kotlin их решили не делать.
На деле, индустрия развивается в сторону писать как можно меньше кода. В случае с Java — проверяемые исключения это именно что ошибка языка, а особенно то, что там половина стандартной библиотеки их кидает. Как результате, код получается весьма многословным, на практике приходится их чаще всего тупо преобразовывать в непроверяемые чтоб не городить кучу строк кода на ровном месте. И большинство тех, кто кроме Java никаких языков не знает, блин именно что городит кучу кода на ровном месте для простейших случаев. В Котлин именно что исправили это недоразумение, в результате не надо быть мегасеньером чтоб не городить лютейший говнокод на ровном месте, даже у типичного быдлокодера строчек становится на порядок меньше в результате.
В расте же с кодами ошибок — это полный отстой. Вроде жить можно и вроде как даже с этим всем сильно лучше чем в Java, но мне крайне не понравилось — грабли блин на ровном месте. Собственно это весьма неплохая причина малопопулярности раста, обработка ошибок там отстой, кто додумался еще по умолчанию кидать паник который вообще падает нафиг, это жуть, там вообще чтоль не предполагается что на раст будут писать что то больше тысячи строк?
Как на чистом Си народ без исключений живет, который не желает говнокодить лютейшие однотипные простыни на ровном месте — самому интересно. Макросами чтоль выкручиваются? Могли бы макросы соответствующие в стандартные либы запихнуть в этом случае и сделать бест практикой, вроде до чих пор такого нет. Проверять блин коды возврата это идиотизм полный, когда функция возвращает код возврата, а реальный результат по ссылке — это полный писец. Фигачить везде дополнительный аргумент типа ошибки, передавая ее по ссылке — и тоже явно проверять, хоть даже и макросом — тоже лютейший писец на ровном месте.
Потому звиняй, но исключения как были мейнстримом, так и остались. Есть конечно любители обработки ошибок через монады, но я вот сильно не уверен что это верный путь. Как минимум из за оверхеда на ровном месте, лютых стектрейсов, а также весьма специфической читабельности, крайне далекого от естественного языка. И вообще ИМХО монады и все такое — это тупо пока неизбежное зло для асинхронности в тех языках, где асинхронность не сделали на уровне синтаксиса языка, в случае async await и их более простых будущих аналогов не вижу смысла в коде на монадах.
Re[6]: Result objects - все-таки победили Exceptions?
Здравствуйте, vsb, Вы писали:
vsb>·>Распараллелить кусок кода, и вот теперь у тебя проблема как пробрасывать исключения между тредами и что с ними делать потом. vsb>Проверяемые исключения были плохой идеей до лямбд. vsb>В принципе я не вижу ничего, что мешало бы сделать лямбды с нормальными проверяемыми исключениями. Немного язык доработать пришлось бы, но ничего сверхестественного. И сейчас можно писать interface Runnable<E> { void run() throws E; }, но это работает только с одним исключением. Нужно было сделать непредставимый тип вроде SQLException | IOException, который бы присваивался типу E. Ну и спроектировать стримы с учётом этой фичи. И было бы нормально. Но это так, к слову.
Очень всё становится сложным и хитрым. Допустим, даже все исключения непроверяемые.
Было:
List<Result> doSomething(List<File> files) throws WhatExceptions??
{
var results = new ArrayList<Result>();
for(var f : files)
{
var data = readData(f);
var parsed = parseData(data);
if(!good(parsed)) continue;
var result = process(parsed);
results.add(result);
}
}
Здравствуйте, SkyDance, Вы писали:
AD>> Лично мне нравится подход с ? тем, что визуально просматривая код, ты всегда видишь где ошибка передаётся на уровень выше.
SD>Исключения по сути делают ? по умолчанию. Потому что это более частый случай, очень редко когда ошибку можно обработать на уровне вызывающего кода. Куда чаще ее нужно пробросить выше.
Я имел виду, что ? показывают программисту места в коде, где ход потока выполнения может измениться, а в случае исключений этого не видно (если не считать всякие checked exceptions).
Хорошо ли это? Я думаю да. Знак вопроса не создаёт излишнего визуального шума и даёт при этом полезную информацию тому, кто читает код. Тут могут возразить те сторонники исключений, кому нафиг не сдалась инфа о том, что функция может бросить исключение. Но мне эта инфа нужна и знак ? в этом сильно помогает.
AD>Хорошо ли это? Я думаю да. Знак вопроса не создаёт излишнего визуального шума и даёт при этом полезную информацию тому, кто читает код. Тут могут возразить те сторонники исключений, кому нафиг не сдалась инфа о том, что функция может бросить исключение. Но мне эта инфа нужна и знак ? в этом сильно помогает.
Дело в том, что любая функция в любой момент может бросить исключение. Не OutOfMemory, так OutOfStack. И обработать это исключение можно только многими уровнями выше. Поэтому для меня это лишь бессмысленный шум, своеобразный карго-культ.
Re[4]: Result objects - все-таки победили Exceptions?
S>В теории, можно было бы, скажем, делегировать львиную долю проверок согласованности реляционной базе, и ловить конкретные ситуации типа "нарушения внешнего ключа" или там "нарушение check constraint". Но это потребовало бы от разpаботчиков JDBC-драйвера усилий по поддержанию развитой системы типов исключений, поэтому в реале никто так не делает (см. например https://github.com/xerial/sqlite-jdbc/issues/367).
Если люди забивают, то с кодами ошибок они будут возвращать один код SQLError, и проблема ничуть не изменится
В COM/OLE тоже порядок был только реализациях от Microsoft, видимо, потому что у них внутренний стандарт требовал и они за этим следили. А огромная куча сторонних интерфейсов и компонентов, даже от весьма именитых фирм, на любые проблемы возвращала универсальный E_FAIL.
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>Здравствуйте, vsb, Вы писали:
vsb>>Считаю, что непроверяемые исключения в общем случае это идеальный способ обработки ошибок и все эти result objects не нужны (могут быть нужны в специфических случаях, но не в общем случае). Проверяемые исключения тоже не нужны, от них проблем больше, чем пользы. По крайней мере в том виде, в котором они в Java.
S>А какие проблемы с ними?
Проблема в совместимости с более поздними инструментами. Выше уже писали про неудобство checked exceptions в ФП-стиле.
Но, имхо, основной косяк — несовместимость с AOP, в т.ч. в Spring.
@Transactional
public EmployeeDto createEmployee(@NonNull EmployeeDto dto) {
...
Если бы код аспекта выкидывал бы какой-нибудь проверяемый SQLException, методу createEmployee (и всем остальным) пришлось бы проставить throws.
А в бизнес коде оно не выбрасывается. И как это анализировать? А если не ставить, то какой смысл вообще в проверяемых исключениях, если они от непроверяемых не отличаются?
Вот, видимо, разработчики Спринга плюнули на это и сделали все исключения непроверяемыми, даже если они проверяемые по сути.
Про инструментирование вообще не говорю, там даже теоретически анализ невозможен.
Re[6]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
_>>Ну и чтоб два раза не вставать — выход за границу массива это ошибка или исключение, которое должно всё сломать? А невозможность выделить память? S>Всё то же самое. Корректный тип у функции типа malloc<T>(n: int) — это не T[], а T[] | OutOfMemory. Поэтому перед тем, как пользоваться полученным указателем, программист вынужден его проверять. А не просто надеяться на то, что память всегда выделиться ("у меня никогда не возвращался нулевой указатель"), и получить SegFault в произвольно далёкой от malloc точке.
Тогда это становится по сути аналогом checked exceptions. В каждом методе придётся пробрасывать всю это портянку T | OutOfMemory | StackOverflow | ByteCodeVerificationError | DivisionByZero | NullPointer | ....
Если у меня есть большая портянка кода, которая считает скидку — ну если что-то где-то там навернулось, я это могу поймать на верхнем уровне и не применить скидку, чем просто терять весь заказ или вообще завалить весь процесс с panic.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[8]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, ·, Вы писали: S>·>Тогда это становится по сути аналогом checked exceptions. В каждом методе придётся пробрасывать всю это портянку T | OutOfMemory | StackOverflow | ByteCodeVerificationError | DivisionByZero | NullPointer | .... S>Во-первых, "пробрасывать" в таком случае гораздо легче, чем в случае checked exceptions. Потому что мне не нужно выписывать всю сигнатуру. S>Достаточно обработать happy path а остальное вернуть как есть: S>
S>·>Если у меня есть большая портянка кода, которая считает скидку — ну если что-то где-то там навернулось, я это могу поймать на верхнем уровне и не применить скидку, чем просто терять весь заказ или вообще завалить весь процесс с panic. S>Совершенно верно. S>
Тип discount будет вырвиглазный. Для ЯП с статической типизацией — фу.
А если положить этот discount в поле класса?
Ещё, само нутро getDiscount() будет кишеть этими isNumber и ? обработкой ошибок. Можно писать так:
double discount(Order o)
{
double s = o.getStuff();// | OutOfMemory | StackOverflow | ByteCodeVerificationError | DivisionByZero | NullPointer | ....double x = db.loadX(o);// | + SQL Exceptionsdouble y = rest.goSomewhere(x, s);//| + Rest Exceptionsreturn s + x / y; // | ArithmeticOverflow | DivisionByZero
}
а ты как предлагаешь?
S>Точно ли вы хотите писать вместо этого
S>} catch(Exception e) {}
S>?
А что в этом плохого?
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы пи
S>И вот тут оказывается, что если у нас на дне дерева вызовов возникает специфическая ситуация, которую мы хотим обработать поближе к корню, то у нас связаны руки.
И вот тут оказывается, что у нас на дне дерева вызовов возвращается Result с каким-то Error для которого нет места в сигнатурах всех прочих функций.
S>Середина этого дерева знать не знает про эту специфическую ситуацию; её спроектировали в те времена, когда никому и в голову не приходило, что там внутри может что-то сломаться таким способом. S>В мире checked exceptions у нас есть три плохих способа это решить:
В мире Result у нас есть три плохих способа это решить:
S>1. Пройтись вверх по стеку вызовов и везде подправить сигнатуры, добавив MySpecificException к списку
Пройтись вверх по стеку вызовов и везде подправить сигнатуры, добавив MySpecificError к списку возможных Error (это конечно если Error вообще предусматривает различные ошибки)
S>2. Отнаследоваться от какого-то исключения, которое в сигнатурах уже есть S>Это если в сигнатурах есть хоть что-то, и это что-то — не final.
Заимплементить SomeTrait если в сигнатурах есть Box<dyn SomeTrait>
S>3. Завернуть MySpecificException в UniversalException S>Это если в сигнатурах есть UniversalException, в котором предусмотрено место для InnerException
Завернуть MySpecificError в UniversalError или скастить в UniversalError.
S>4. Выбросить потомка RuntimeException
Выбросить панику
S>Написать функцию IEnumerable<R> map(IEnumerable<T> source, Func<T, R> f) c проверяемыми исключениями не представляется возможным. В ней функция f не имеет права ничего выбрасывать, потому что map пообещала ничего не выбрасывать.
А нельзя тип исключения принять в качестве параметра шаблона?
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[10]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, korvin_, Вы писали:
_>>Где здесь должно быть указано, что new Foo может вернуть не объект класса Foo, а OutOfMemory? В сигнатуре конструктора Foo? В "сигнатуре" оператора new? S>Примерно там же, где сейчас указано, что new Foo может выбросить OutOfMemoryError. То есть — в "сигнатуре" оператора new.
Сейчас это нигде не указано, кроме, разве что документации. Но вы не про документацию писали, а систему типов.
_>>Тот же вопрос про StackOverflow: где он должен быть указан? В сигнатуре каждой функции (метода)? Операция () вызов метода автоматически должна его добавлять к каждому методу? S>Если идти по строгому пути — то да, в сигнатуре каждой функции.
Удачи убедить хоть кого-нибудь писать на таком языке.
_>>Теперь Haskell. Имеем такой код, например:
_>>
_>>data Expr
_>> = Val Int
_>> | Add Expr Expr
_>> | Mul Expr Expr
_>> | If Expr Expr Expr
_>>eval :: Expr -> Expr
_>>eval e = ...
_>>expr0 = If (Mul (Val 1) (Val 0))
_>> (Add (Mul (Val 4) (Val 10))
_>> (Val 2))
_>> (Mul (Add (Val 7) (Val 14))
_>> (Val 2))
_>>print (eval expr0) -- Val 42
_>>
_>>Где тут должено быть указано OutOfMemory? S>Хороший вопрос. Надо полагать — везде.
И как это должно выглядеть?
data Expr
= Val :: Int -> (Expr | OOM)
| Add :: Expr -> Expr -> (Expr | OOM)
| Mul :: Expr -> Expr -> (Expr | OOM)
...
eval :: Expr -> (Expr | OOM)
...
expr0 = If (Mul (Val 1) (Val 0)) -- упс, (Val 1) возвращает (Expr | OOM), а Mul ожидает чистый Expr
...
print (eval expr0) -- и снова: expr0 у нас имеет тип (Expr | OOM), а eval ожидает чистый Expr
Попробуем по-другому:
data Expr
= Val :: (Int | OOM) -> (Expr | OOM)
| Add :: (Expr | OOM) -> (Expr | OOM) -> (Expr | OOM)
| Add :: (Expr | OOM) -> (Expr | OOM) -> (Expr | OOM)
...
eval :: (Expr | OOM) -> (Expr | OOM)
...
expr0 = If (Mul (Val 1) (Val 0))
...
-- ну и print, конечно, должен иметь сигнатуру вида
-- print :: Show a => (a | OOM) -> (IO () | OOM)
То есть фактически OOM придётся писать буквально везде. Добавим сюда также StackOverflow, DivisionByZero и чёрт знает что ещё.
Удачи убедить хоть кого-нибудь писать на таком языке, да и самому не свихнуться от такого.
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, Alekzander, Вы писали:
A>Исключения нужны, чтобы прятать обработку и "спрямлять" поток выполнения.
Исключения нужны чтобы ошибки не обрабатывать. Просто игнорируем, в худшем случае процесс упадет. А по том по логам с продакшена уже расставляются кэчи где надо.
А если хочется ошибки правильно обработать заранее прямо при написании кода — исключения никуда не годятся.
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
AD>>Коды ошибок в расте? Интересно... _>Если речь о том что Option или Variant сильно отличаются от return 0 в C, или HRESULT в COM/OLE (тут серая рожа того кто знает ) — то для меня разница исчезающе мала. Современными средствами можно накрутить на почти любой язык проверку что на каждый возврат из функции написан if. То что в Rust такая проверка часть языка — просто деталь реализации.
ну давайте тогда скажем что исключения — это просто подход с goto. Ведь разница исчезающе мала.
Не надо относиться к сахару как шелухе. Он значительно меняет подходы. С — это просто обёртка или сахар над асмом. Но он перевернул программирование.
Так и обработка ошибок в Rust — абсолютно другое. Лишены всех недостатков кодов возратов: многословности и необязательности обработки. Ну как можно говорить что разница исчезающе мала? Да она кардинальна. Без этих недостатков исключения вчистую проигрывают.
Re[19]: Result objects - все-таки победили Exceptions?
Здравствуйте, korvin_, Вы писали:
_>Это проблематично сделать, учитывая, что даже String (любая конкатенация == аллокация) и любой массив в Java -- Object. При этом нужно быть осторожным с использованием любого библиотечного кода, который может что-то аллоцировать, в том числе и неявно, и даже языковых конструкций, типа for (var x : array) { ... }, т.к. она приводит к аллокации итератора.
Да полно low или даже zero gc кода. Это не бином ньютона, по крайней мере в мире Java.
_>Удачи "выделить сразу всё на старте".
Не rocket science.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[7]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали:
S>>И вот тут оказывается, что если у нас на дне дерева вызовов возникает специфическая ситуация, которую мы хотим обработать поближе к корню, то у нас связаны руки. TB>Ну и почему это такое случается? TB>И почему если такое может случиться с Checked Exceptions то не может случиться с Result? Какая нафиг разница-то?
Потому что checked exceptions прибиты там гвоздями. Нет в языке никакой возможности сказать "а если меня позвали с параметром, вызов которого бросает неизвестный X, то добавьте этот X к моей сигнатуре". S>>Автовывод решает эту проблему. Сигнатуры в явном виде пишутся там, где нам принципиален тип. И да, там нужно обрабатывать ошибку. TB>Как автовывод поможет тебе поправить сигнатуры всех промежуточных функций? Надеешься что они шаблонные?
Почему шаблонные? Просто у них не указан вообще никакой тип. В каком-нибудь flow9 это прекрасно работает. Несмотря на отсутствие шаблонов
TB>Чтобы вернуть новый еррор для которого нет места в иерархии промежуточных функций.
Конечно есть место.
TB>А нафига тогда вводили все эти Result?
Для того, чтобы обойтись без паники там, где мы хотим обойтись без паники.
TB>Оба. Для вывода бросаемого лямбдой исключения чтоб суммировал типы исключений, бросаемые функциями, вызываемыми лямбдой.
Да, это было бы значительно лучше. Возможно, в эту сторону и нужно копать — ваши с korvin_ примеры убеждают меня в том, что локально-понятные result object на большой сodebase приводят к примерно такому же бардаку, как и локально-понятные checked exceptions.
Впрочем, наверняка есть какой-то способ изящно объединить обе парадигмы во что-то одно. TB>Кстати, а что делает Раст если лямбда вызывает функции, которые возвращают Result с разными Error? Правильно, шлёт нафиг и требует смаппить к одному варианту.
Ну, а вот в каком-нибудь TypeScript просто произойдет сложение типов.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[18]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали: TB>Да, это сочетание я тоже знаю. Но оно не работает потому что система отчаянно трещит пытаясь запихать в своп мою модель, ролик с ютубаю, менеджер полупрозрачных окошек и прочую хрень, которая уже не нужна, ведь важнее выживание системы. Но вызов диспетчера плетется где-то там в конце общей очереди. Зачем давать диспетчеру высший приоритет, пусть пользователь помучается!
Ну так дайте ему высший приоритет руками. Он же у разработчиков всё равно стоит в автостарте системы.
TB>Так вот нет в винде никакого умного менеджера. Есть кучка педерастов решивших что они за пользователя лучше знают что ему нужно.
Конечно нет. Его никто и не обещал. Обещали нормальную архитектуру — где вы можете поставить предел размера виртуальной памяти, и приложение с утечкой будет обломано, вместо того, чтобы мешать нормальным приложениям работать. TB>А в Линуксе я регулярно ловлю смерти по OOM. Как бы ты не вылизывал код, заказчик будет скармливать ему всё большие и большие объёмы данных, это нормально. И смерть одного процесса лучше чем смерть всей системы.
Поставьте своей винде лимит виртуальной памяти, и виновное приложение ограничат.
Ещё на всякий случай напомню, что основное, для чего нам нужна борьба с ООМ — это не десктоп (при его штатном использовании ООМ не случается примерно никогда, а когда случается — есть интерактивные способы вмешаться в события), а сервер. Где нет никакого таск менеджера и Ctrl-Shift-Esc.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[22]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Откуда вы знаете, что они "используют не все"? Может быть, программа в какой-то момент собирается "использовать все", просто вы не попадаете в этот сценарий.
Потому что я играл в Старкрафт и примерно знаю сколько он жрёт
S>Если Старкрафт перестал запускаться — значит, ему нужно больше виртуальной памяти, чем вы накрутили в настройках.
Программа может в своём воображении иметь сколько угодно много виртуальной памяти, пока не понадобилась физическая страница для работы с ней. В Линуксе OOM Killer работает на резидентную память (я верно назвал?), а в винде — сразу на виртуальную? Чето говно какое-то неюзабельное
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[2]: Result objects - все-таки победили Exceptions?
Здравствуйте, SomeOne_TT, Вы писали:
SO_>Современные result objects позволяют не описывать все варианты проблем по всей цепочке вызовов, а хранить их "динамически", восстанавливая SO_>тип ошибки в месте обработки. В расте, например, весьма популярны Result<(), Box<dyn Error>> и anyhow::Result<()>
А что не современные этого не позволяли? В чем разница? Что принципиально нового появилось?
Коды возврата использовали еще деды. Основная претензия была — из можно было забыть проверить.
С проверяемыми исключениями вам доводилось сталкиваться, как в Java?
Re: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>А ведь было же хорошее решение — т.н. проверяемые исключения в Java. Когда компилятор требовал проверки того или иного исключения, но так же была возможно обернуть в RuntimeException, если оно утратило смысл бизнес-логики или ожидаемого
Я как-то показывал типичный код на Java
void MyCoolMethod(int a, int b) throws Exception
{
int p;
try
{
// 100500 строк кода
}
catch (Exception ex)
{
}
}
Лень разработчикам указывать все исключения, которые могут прилететь. Я в своё время запарился с таким кодом работать.
Re[2]: Result objects - все-таки победили Exceptions?
Здравствуйте, Privalov, Вы писали:
P>Лень разработчикам указывать все исключения, которые могут прилететь. Я в своё время запарился с таким кодом работать.
...Pokémon exception handling after the show's catchphrase "Gotta Catch ‘Em All!"
Re[2]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
_>Хельсберг очень подробно когда-то объяснял, почему эту фичу в C# не потащили. Кмк, что реально нужно — возможность указать no throw, и throws(список).
Так в Java есть не проверяемые — это наследники RuntimeException.
Re[2]: Result objects - все-таки победили Exceptions?
A>try
A>{
A> const data = LoadData();
A> const processedData = ProcessData(data);
A> SaveData(processedData);
A>}
A>catch
A>{
A> log.Error("Всё пропало");
A>}
A>
Так нельзя писать. Во-первых, всегда нужно указывать типы исключений. Но беда в том, что вы обычно не думаете об этом — у вас нет четкой картинки в голове какие исключения может вызвать функция. Т.е. вы теряете контроль. А у меня всегда 100% я знаю какие исключения возможны, я всегда это держу в голове для каждой функции.
Далее. Исключения нужно отлавливать для каждого шага отдельно — т.е. не все в куче — а везде, на каждом из шагов где могут возникнуть исключения — нужно конкретные отлавливать и обрабатывать, если возмжно.
Re[2]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
S>>Так в Java есть не проверяемые — это наследники RuntimeException. _>Делить исключения на проверяемые и нет — ошибка. В разных частях проекта даже одинаковые исключения могут быть как проверяемые так и нет.
Проверяемые — это часть контакта. Т.е. они хороши для бизнес-логики или основной логической ошибки утилиты.
В точке где исключение меняет акцент — переходит из проверяемого в непроверяемого — оборачиваете в RuntimeException.
Просто многие не разобрались с парадигмой и она стала только во вред.
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>В современных IDE нет никакой проблемы, чтобы указать. IDEA просто не даст откомпилировать метод, в котором выбрасывается checked исключение, потребует либо поставить try-catch, либо добавить throw, и сама это сделает.
Я в курсе. Те, кто писали код в стиле, который я показал, тоже были в курсе. Из всей команды только один парень отмечал все checked исключения в своих методах. Еу и я тоже.
Re[2]: Result objects - все-таки победили Exceptions?
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Исключения, в общем, проще. PD>У них ИМХО только один недостаток — их нельзя игнорировать. Коды ошибок можно, если знаешь, что тут что с ошибкой, что без ошибки — все равно.
вроде как наоборот. Исключение можно проигнорировать и замолчать (и оно потом выстрелит когда-то в runtime) А тип Result в том же rust невозможно не обработать.
Здравствуйте, sergii.p, Вы писали:
SP>вроде как наоборот. Исключение можно проигнорировать и замолчать (и оно потом выстрелит когда-то в runtime) А тип Result в том же rust невозможно не обработать.
Я не имел в виду rust, а имел в виду саму идею возвращать. В C/C++/Pascal/Java/C# вполне можно
With best regards
Pavel Dvorkin
Re: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>Ну вот рекомендации в новомодных языках: https://docs.flutter.dev/app-architecture/design-patterns/result
S>- все-таки топят за Result objects для бизнес-логики.
S>А ведь это всю цепочку поддерживать. Как-то много лишних букв добавляется.
S>Но! Exception как бы не определены в контрактах. Если ResultObject — это контракт, его понятно что нужно проверить и компилятор даже контролирует эту проверку. А Exception разве что в документации описан, которую могут не читать.
S>А ведь было же хорошее решение — т.н. проверяемые исключения в Java. Когда компилятор требовал проверки того или иного исключения, но так же была возможно обернуть в RuntimeException, если оно утратило смысл бизнес-логики или ожидаемого
S>Но, как оказалось, народ идеи не понял. Так и вернулись к понятным дебилоидам кодам возврата, т.к. проверяемые исключения осилить не смогли. Так же и в Kotlin их решили не делать.
Исключение показывает что мы вышли за область определения контракта объекта, т.е. что при таком состоянии объекта при таких входных данных его поведение не определено и чисто теоретически может быть любым. Это как область определения функции из алгебры. Выброс Exception тут очень удобный механизм. Конечно выброс exception можно использовать не только для защиты контракта объекта но и для других целей, например для наивной валидации, но это уже не лучший прием.
Программа – это мысли спрессованные в код
Re[2]: Result objects - все-таки победили Exceptions?
Q>Исключение показывает что мы вышли за область определения контракта объекта, т.е. что при таком состоянии объекта при таких входных данных его поведение не определено и чисто теоретически может быть любым. Это как область определения функции из алгебры. Выброс Exception тут очень удобный механизм. Конечно выброс exception можно использовать не только для защиты контракта объекта но и для других целей, например для наивной валидации, но это уже не лучший прием.
То что на одном уровне нарушение контракта ацкое нарушение, то на уровне выше вполне себе обрабатываемая ситуация. Например, есть сетевые протоколы, в которых в обе стороны гоняются бесконечные потоки json-пакетов. На уровне парсера — это беда и исключение, ведь битый json это реально нарушение контракта. На уровне сервера — это просто клиент отвалился, закрываем коннект и забываем о нём. На уровне клиента — нужно reconnect и перепослать. А на уровне какой-нибудь утилиты читающей свой конфиг для старт — это снова ад и исключительная ситуация.
Получается разработчикам json-библиотек надо делать 2 версии — одну для тех кому нарушение протокола проблема, а другую для тех для кого это просто ошибочка, не более. Так что ли?
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
Q>>Исключение показывает что мы вышли за область определения контракта объекта, т.е. что при таком состоянии объекта при таких входных данных его поведение не определено и чисто теоретически может быть любым. Это как область определения функции из алгебры. Выброс Exception тут очень удобный механизм. Конечно выброс exception можно использовать не только для защиты контракта объекта но и для других целей, например для наивной валидации, но это уже не лучший прием.
_>То что на одном уровне нарушение контракта ацкое нарушение, то на уровне выше вполне себе обрабатываемая ситуация. Например, есть сетевые протоколы, в которых в обе стороны гоняются бесконечные потоки json-пакетов. На уровне парсера — это беда и исключение, ведь битый json это реально нарушение контракта. На уровне сервера — это просто клиент отвалился, закрываем коннект и забываем о нём. На уровне клиента — нужно reconnect и перепослать. А на уровне какой-нибудь утилиты читающей свой конфиг для старт — это снова ад и исключительная ситуация.
_>Получается разработчикам json-библиотек надо делать 2 версии — одну для тех кому нарушение протокола проблема, а другую для тех для кого это просто ошибочка, не более. Так что ли?
Тут мы подходим к тому, что у ошибок может быть разная сематика. Тот случай который я описал, если возникло исключение — это означает, что в программе баг и правильным способом будет закрыть программу с записью в лог об исключении. В случае если у объекта часть состояния находится за пределами программы(сетевые подключения, файлы и т.д) т.е. то за что программа не может нести ответственность, тут обычно требуется более сложная логика обработки ошибок. Тут мы можем определить разные классы exception и производить обработку для каждого вида ошибок.
Программа – это мысли спрессованные в код
Re: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>Ну вот рекомендации в новомодных языках: https://docs.flutter.dev/app-architecture/design-patterns/result
S>- все-таки топят за Result objects для бизнес-логики.
S>А ведь это всю цепочку поддерживать. Как-то много лишних букв добавляется.
S>...
В swift с некотороц версии встроенным result пользоваться удобно, в сочетании с возможностью объявления enum с параметрами. Try catch на каждом шаге — мало того, что громоздко, оно вроде ещё и по части производительности так себе. А если catch только в конце — тогда как понять, что и где произошло, неужели stack разбирать? Т. е. сбоцнул сетевой запрос — есть информация куда, допустим routes объявлены в константах, тогда сравнениваем и узнаём, что там запрашивалос. Но даже ясно что, может быть не очевидно, зачем, даже при условии, что понятно, с чего обработка началас. Ну и какая информация об ошибке в итоге будет — слегка больше, чем просто ошибка? Если, конечно, делать тяп-ляп и в 90% случаев неизвестная ошибка считать приемлемым, то, конечно, но иначе как делать, чтоб информация об ошибке была? Кроме бесконечных rethrow, достоинства которых мне не понятно.
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
_>То что на одном уровне нарушение контракта ацкое нарушение, то на уровне выше вполне себе обрабатываемая ситуация. Например, есть сетевые протоколы, в которых в обе стороны гоняются бесконечные потоки json-пакетов. На уровне парсера — это беда и исключение, ведь битый json это реально нарушение контракта. На уровне сервера — это просто клиент отвалился, закрываем коннект и забываем о нём. На уровне клиента — нужно reconnect и перепослать. А на уровне какой-нибудь утилиты читающей свой конфиг для старт — это снова ад и исключительная ситуация. _>Получается разработчикам json-библиотек надо делать 2 версии — одну для тех кому нарушение протокола проблема, а другую для тех для кого это просто ошибочка, не более. Так что ли?
Нет, в ракурсе библиотеки JSON (слой утилит) — очевидно что JsonException акцентуированно и оно должно быть проверяемым, его ожидаем.
В точке, где вы используете эту библиотеку — вы уже снимаете акцент и переводите в RuntimeException, JSON-exception уже делаете inner.
Re[2]: Result objects - все-таки победили Exceptions?
Здравствуйте, Qulac, Вы писали:
Q>Исключение показывает что мы вышли за область определения контракта объекта, т.е. что при таком состоянии объекта при таких входных данных его поведение не определено и чисто теоретически может быть любым. Это как область определения функции из алгебры. Выброс Exception тут очень удобный механизм. Конечно выброс exception можно использовать не только для защиты контракта объекта но и для других целей, например для наивной валидации, но это уже не лучший прием.
Исключения удобно сделать частью контракта, когда они проверяемые. Нет постоянного бойлерплейт-кода с обертками.
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>Здравствуйте, Qulac, Вы писали:
Q>>Исключение показывает что мы вышли за область определения контракта объекта, т.е. что при таком состоянии объекта при таких входных данных его поведение не определено и чисто теоретически может быть любым. Это как область определения функции из алгебры. Выброс Exception тут очень удобный механизм. Конечно выброс exception можно использовать не только для защиты контракта объекта но и для других целей, например для наивной валидации, но это уже не лучший прием.
S>Исключения удобно сделать частью контракта, когда они проверяемые. Нет постоянного бойлерплейт-кода с обертками.
Я считаю, что это не обязательно.
Программа – это мысли спрессованные в код
Re[2]: Result objects - все-таки победили Exceptions?
Здравствуйте, vsb, Вы писали:
vsb>Считаю, что непроверяемые исключения в общем случае это идеальный способ обработки ошибок и все эти result objects не нужны (могут быть нужны в специфических случаях, но не в общем случае). Проверяемые исключения тоже не нужны, от них проблем больше, чем пользы. По крайней мере в том виде, в котором они в Java.
А какие проблемы с ними?
Re[4]: Result objects - все-таки победили Exceptions?
Q>Тут мы подходим к тому, что у ошибок может быть разная сематика. Тот случай который я описал, если возникло исключение — это означает, что в программе баг и правильным способом будет закрыть программу с записью в лог об исключении. В случае если у объекта часть состояния находится за пределами программы(сетевые подключения, файлы и т.д) т.е. то за что программа не может нести ответственность, тут обычно требуется более сложная логика обработки ошибок. Тут мы можем определить разные классы exception и производить обработку для каждого вида ошибок
Я в твоём сообщении не могу отделить ошибки от исключений. А хочется определённости — в итоге-то что? Парсеру json, если пришёл битый, что делать — кидать "просто ошибку" или исключение, которое приведёт к закрытию программы? Ну и чтоб два раза не вставать — выход за границу массива это ошибка или исключение, которое должно всё сломать? А невозможность выделить память?
Самый прикол, что решение "исключения существуют, и это нормально, главное что мы их можем перехватывать и обрабатывать" — даёт универсальный и единообразный подход к самым разным проблемам. А вот подход — "у нас в языке исключений нет, ну может чуть-чуть, на донышке" — приводит к тупику. Решение rust как раз в духе, в моём понимании, дибилизма — делайте всего по 2.
Re[4]: Result objects - все-таки победили Exceptions?
S>Нет, в ракурсе библиотеки JSON (слой утилит) — очевидно что JsonException акцентуированно и оно должно быть проверяемым, его ожидаем. S>В точке, где вы используете эту библиотеку — вы уже снимаете акцент и переводите в RuntimeException, JSON-exception уже делаете inner.
Как решается это вопрос, когда исключения есть — я прекрасно знаю. Хотелось бы узнать какое решение этого вопроса предлагают языки без исключений? В Rust есть варианты компиляции без std и с no_panic крейтом. И прекрасно что есть. Но тогда пол экосистемы отваливается, вместе с почти всеми доступными библиотеками. Очень такое себе.
Ну и чтоб веселее было — даже если я напишу код с no_panic, но подгружу плагин, автор которого так не заморачивался — всё. Приехали. Этот плагин мне в одно лицо сложит всю прогу, несмотря на все мои усилия. Очень косячно для языка, который позиционируется как надёжный.
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, johny5, Вы писали:
J>Явно прокидываем ошибку, а не "я не знаю кинет оно или нет" что частенько приводит к сюрпризам в проде и перечитыванию доков. Просто нужно написать "?". При этом задумываешься. Ровно то же самое когда разворачиваешь optional<> (который тоже обретает лютую популярность в С++): всё что нужно сделать, написать "*", но задумываешься, ведь там объекта может и не быть.
optional<> в С++ — то ещё говно. Откуда ты высосал, что он обретает лютую популярность...
S>А какие проблемы с ними?
Ищи статьи или интервью Хельсберга. Они в MS очень долго решали, потому что сначала идея checked exceptions им тоже казалась очень правильной.
Самый очевидный пункт: допустим у нас есть сериализатор, который пишет в абстрактный Stream. Этот Stream может быть диском, файлом, памятью, и т.п. Какие исключения объявить сериализатору, если он сам не знает, какой Stream ему дадут на вход? Заставить все Stream кидать абстрактный StreamWriteException? А почему совершенно не имеющие отношения к сериализатору классы, вдруг должны его учитывать и от него зависить, причём зависеть неформально? И это только один из сценариев. А есть ещё версии (например NetworkStream может начать поддерживать новый протокол, и кидать новые типы исключений), и так далее.
Re[5]: Result objects - все-таки победили Exceptions?
S>Конечно StreamWriteException — это очевидно, причем во внутрь в innerException уже вкладывают то что породило данное исключение. В этом и есть смена акцентуации.
С этими "вложениями исключений" нужно быть крайне аккуратным. Потому что при бездумном использовании у нас опять получится call stack, чего стоит избегать. На мой взгляд, оборачивать исключение в другое исключение имеет смысл только в том случае, когда эта обертка добавляет контекст, недоступный в call stack (скажем, какие-то значения local variables, или что-то из global mutable state).
И дело там вовсе не в "акцентуации", а в том, чтобы собрать полный контекст (а значит, иметь возможность воспроизвести проблему путем создания аналогичного контекста). Задача заметно упрощается, если функции — чистые (pure functions), тогда исключение может пролететь сколько угодно фреймов стека, и по дороге собирая весь контекст автоматически.
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
vsb>>Считаю, что непроверяемые исключения в общем случае это идеальный способ обработки ошибок и все эти result objects не нужны (могут быть нужны в специфических случаях, но не в общем случае). Проверяемые исключения тоже не нужны, от них проблем больше, чем пользы. По крайней мере в том виде, в котором они в Java.
S>А какие проблемы с ними?
Да много проблем. Их никто не использует, думаю, это достаточный индикатор того, что что-то с ними не так. У тебя методы превращаются в длиннющие throws из десятков исклюючений. Ты добавляешь в одну реализацию работу с XML и интерфейс должен кидать это исключение, все клиенты сломаны, теперь это XML исключение должно бросаться изо всех клиентов этого интерфейса.
Или на каждый класс надо создавать отдельное исключение, а то и на каждый метод, заворачивающий вложенное исключение. И весь смысл этой типизации теряется кроме тонн лишней писанины.
Я не отрицаю некоторого смысла во всей этой идее проверяемых исключений, но конкретно в Java реализация фатально плохая. И я не уверен, что знаю, как сделать лучше.
Если немного пересмотреть всю эту концепцию, можно прийти к аналогии со статической и динамической типизацией.
Думаю, сегодня уже никто не будет отрицать, что и языки с динамической типизацией и языки со статической типизацией имеют свои плюсы и минусы. При этом тем же Haskell-ем, который можно считать эдакой кульминацией статической типизации, мало кто пользуется, не в последнюю очередь из-за сложности. Больше пользуются языками, которые в основном статически типизированы, но в отдельных местах не гнушаются динамической типизации. Нет ничего криминального в том, чтобы передать значение, как Object и потом проверить его через instanceof. Ну не проверит компилятор это место, ну и ладно, переживём.
И вот в этой перспективе проверяемые и непроверяемые исключения можно сравнить со статически и динамически типизированными языками программирования. Да, непроверяемые исключения это, безусловно, обход типизации языка. Да, проверяемые исключения в теории позволят найти какой-то баг, когда программист был обязан обработать какое-то исключение. Но они же привносят неудобства, которые на практике слишком велики. А с непроверяемыми исключениями всё очень просто и понятно.
Re[6]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
_>Так проблема в том, что лазить по дебрям Inner Exceptions неправильно. В какой-то день писатель сериализатора захочет разделить его на 2 компонента — собственно писатель в стрим, и форматтер. И бах — у нас уже иерархия из 2-х Inner Exceptions, потому что каждый на своём уровне что-то во что-то завернул. Или для скорости форматтер теперь многопоточный — и у нас InnerException стал AggregateException. К любому такому сюрпризу вызывающий код готов не будет. Проверит что в InnerException неведомая хрень, и либо сломается от неожиданности там где ломаться не надо, либо проигнорит.
Вызывающий код знает только что сериализация не удалась. Зачем ему знать больше? Если нужно больше для отладки — добро пожаловать в inner.
S>>Не должны зависеть — они могут выбрасывать наследника StreamExceptin, а сериализатор перехватывать все наследники и пихать в innerException своего StreamWriteException. _>InnerException это вообще штука информативная только для разработчика на работе. Чтобы разбирать тихонько лог, и думать что сделать чтобы так падало пореже. Если внешний код серьёзно начинает полагаться на разбор InnerException — беда неминуема.
Почему же — вполне нормальная практика — пройтись по всей иерархии и найти первопричину.
S>>Этот сценарий я разобрал и показал как правильно делать, давайте далее. _>Только один нюанс — если мы передаём сериализатору абстрактный Stream, который может кидать любые невиданные ранее исключения, а заворачивает их в StreamWriteException сам сериализатор, то все объявления в секции checked у Stream не имеют смысла. Потому что сериализатору в общем-то пофиг что там, он всё равно с этим сделать ничего не может, и вынужден заворачивать в свою обёртку всё, что бы там ни было. И сериализатор тут только простой наглядный пример. Так-то во всех местах где один код вызывает другой, и есть какая-то иерархия классов или интерфейсов — секция checked начинает вот так размазываться до бесполезной. Что и произошло с Java.
Stream — это контракт. Проверяемые исключения — так же часть контракта, они должны быть не любыми а только наследниками StreamException.
S>>Слышали ли вы что-нибудь про наследование? _>Слышал. Вот есть базовый Stream, у него checked фиксированный набор исключений. И вдруг появляется новый NetworkStream, которому надо добавить новый CertificateException. А добавить низзя: базовый стрим менять не положено — на его checked уже весь клиентский код завязался. У пользователей уже свои checked зависят от тех checked что были у старого Stream. Приехали. В Java это проблему решили "элегантно" — забили на checked. Могли бы иначе — решили бы иначе.
Так сделайте наследника StreamException — в чем вопрос
Re[6]: Result objects - все-таки победили Exceptions?
Здравствуйте, SkyDance, Вы писали:
SD>С этими "вложениями исключений" нужно быть крайне аккуратным. Потому что при бездумном использовании у нас опять получится call stack, чего стоит избегать. На мой взгляд, оборачивать исключение в другое исключение имеет смысл только в том случае, когда эта обертка добавляет контекст, недоступный в call stack (скажем, какие-то значения local variables, или что-то из global mutable state).
Со всем нужно работать обдуманно, испортить можно любую идею.
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, vsb, Вы писали:
vsb>Да много проблем. Их никто не использует, думаю, это достаточный индикатор того, что что-то с ними не так. У тебя методы превращаются в длиннющие throws из десятков исклюючений. Ты добавляешь в одну реализацию работу с XML и интерфейс должен кидать это исключение, все клиенты сломаны, теперь это XML исключение должно бросаться изо всех клиентов этого интерфейса.
Не все поняли как правильно пользоваться. Исключение — это часть конракта. Если контракт не изменялся — то новое исключение не добавляете, просто пишите в inner того исключения, которое является частью контракта. Выше приводили пример с сериализацией — SerializationException — а внутри уже inner. Сериализатор перехватывает только наследников StreamException.
vsb>Или на каждый класс надо создавать отдельное исключение, а то и на каждый метод, заворачивающий вложенное исключение. И весь смысл этой типизации теряется кроме тонн лишней писанины.
Там где не нужна акцентуация — просто оборачиваете в RuntimeException — не проверяемое.
vsb>Я не отрицаю некоторого смысла во всей этой идее проверяемых исключений, но конкретно в Java реализация фатально плохая. И я не уверен, что знаю, как сделать лучше.
Просто не поняли идею.
vsb>Думаю, сегодня уже никто не будет отрицать, что и языки с динамической типизацией и языки со статической типизацией имеют свои плюсы и минусы. При этом тем же Haskell-ем, который можно считать эдакой кульминацией статической типизации, мало кто пользуется, не в последнюю очередь из-за сложности. Больше пользуются языками, которые в основном статически типизированы, но в отдельных местах не гнушаются динамической типизации. Нет ничего криминального в том, чтобы передать значение, как Object и потом проверить его через instanceof. Ну не проверит компилятор это место, ну и ладно, переживём.
Возможно дело не в сложности — а в непривычности. И в том что он сырой еще, над ним мало работали. Возможно лет через 50 человечество до него дорастет.
vsb>И вот в этой перспективе проверяемые и непроверяемые исключения можно сравнить со статически и динамически типизированными языками программирования. Да, непроверяемые исключения это, безусловно, обход типизации языка. Да, проверяемые исключения в теории позволят найти какой-то баг, когда программист был обязан обработать какое-то исключение. Но они же привносят неудобства, которые на практике слишком велики. А с непроверяемыми исключениями всё очень просто и понятно.
Просто люди не проработали в голове концепцию и как бы боятся исключений. Привыкли терять контроль над кодом. У меня каждый метод КАЖДЫЙ — я 100% знаю не только параметры, но и все возможные исключения, которые он может выбросить. Т.е. 100% контроль кода. А люди привыкли к исключениям относиться как к магии. Как бы какая-то неприятность, которой быть не должно.
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
S>>А какие проблемы с ними? _>Ищи статьи или интервью Хельсберга. Они в MS очень долго решали, потому что сначала идея checked exceptions им тоже казалась очень правильной.
ну там мне кажется довольно просто. Они просто прогнулись под мнением большинства. Тогда слишком сильно приоткрыли окно Овертона и многие разработчики (в том числе и я) восприняли идею в штыки. Например на Rust и Go уже ругаются куда меньше. Хотя там всё куда извращённее.
_>Самый очевидный пункт: допустим у нас есть сериализатор, который пишет в абстрактный Stream. Этот Stream может быть диском, файлом, памятью, и т.п. Какие исключения объявить сериализатору, если он сам не знает, какой Stream ему дадут на вход? Заставить все Stream кидать абстрактный StreamWriteException? А почему совершенно не имеющие отношения к сериализатору классы, вдруг должны его учитывать и от него зависить, причём зависеть неформально?
а почему не должны? Мы должны зависеть от абстракций, но не от реализаций. StreamWriteException — абстракция. Не вижу здесь проблемы. Если клиент не работает с сериализатором напрямую, пусть принимает на вход ещё более общую ошибку: IOError н-р.
И вообще не ясно, как решают проблему непроверяемые исключения? Добавилась у вас новая ошибка MySpecificStreamError. Кто-то её перехватит? Или подождут релиза когда у клиента вылетит необработанное? Вот как-то не помню чтобы я сильно мучался с проверяемыми исключениями в java (было это правда давно), зато отчётливо помню как напарывался на необработанные исключения.
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
AD>>Коды ошибок в расте? Интересно... _>Если речь о том что Option или Variant сильно отличаются от return 0 в C, или HRESULT в COM/OLE (тут серая рожа того кто знает ) — то для меня разница исчезающе мала. Современными средствами можно накрутить на почти любой язык проверку что на каждый возврат из функции написан if. То что в Rust такая проверка часть языка — просто деталь реализации.
А какая разница, осилили они или неосилили исключения? Какая разница что оператор ? — это просто синтаксический сахар? Лично мне нравится подход с ? тем, что визуально просматривая код, ты всегда видишь где ошибка передаётся на уровень выше.
Подобные споры — это не более чем споры о вкусах. В разных проектах на разных языках я использую и исключения и result values. И меня устраивает и то и другое.
Здравствуйте, ·, Вы писали:
vsb>>Я не отрицаю некоторого смысла во всей этой идее проверяемых исключений, но конкретно в Java реализация фатально плохая. И я не уверен, что знаю, как сделать лучше. ·>Основная проблема не в исключениях самих по себе, а в том, что они слишком "другие" и плохо совмещаются с другими фичами ЯП.
·>Например, был у тебя for-цикл, решил переписать на stream с лямбдами и checked исключения, внезапно, через лямбду уже не передать.
·>Распараллелить кусок кода, и вот теперь у тебя проблема как пробрасывать исключения между тредами и что с ними делать потом.
Проверяемые исключения были плохой идеей до лямбд.
В принципе я не вижу ничего, что мешало бы сделать лямбды с нормальными проверяемыми исключениями. Немного язык доработать пришлось бы, но ничего сверхестественного. И сейчас можно писать interface Runnable<E> { void run() throws E; }, но это работает только с одним исключением. Нужно было сделать непредставимый тип вроде SQLException | IOException, который бы присваивался типу E. Ну и спроектировать стримы с учётом этой фичи. И было бы нормально. Но это так, к слову.
Re[5]: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>Просто люди не проработали в голове концепцию и как бы боятся исключений. Привыкли терять контроль над кодом. У меня каждый метод КАЖДЫЙ — я 100% знаю не только параметры, но и все возможные исключения, которые он может выбросить. Т.е. 100% контроль кода. А люди привыкли к исключениям относиться как к магии. Как бы какая-то неприятность, которой быть не должно.
Во-первых, если говорить про Java, каждый метод может бросать StackOverflowError при вызове.
Во-вторых 99% методов могут бросать OutOfMemoryError при использовании new или вызове других методов, использующих new.
Это просто штатно, альтернатива разве что ронять JVM вместо выброса таких исключений, но это уже совсем глупо.
Точно так же 99% методов могут бросать разные другие исключения. К примеру вызываем метод, его байткод не загружен в JVM. Грузим байткод, диск сломался и бросил IOException при попытке чтения JAR файла. Кидается IOException обёрнутый в какой-то Error. Это всё теоретически возможно.
Мы логгируем что-то в stdout и может броситься ошибка.
Т.е. концепция полного отказа от непроверяемых исключений не выдерживает никакой критики. Всегда будет множество ошибок, которые в теории могут возникать, но которые 100% неразумно включать в контракт функции.
SP>а почему не должны? Мы должны зависеть от абстракций, но не от реализаций. StreamWriteException — абстракция. Не вижу здесь проблемы. Если клиент не работает с сериализатором напрямую, пусть принимает на вход ещё более общую ошибку: IOError н-р.
А потому что NetworkStream может заиспользовать сертификаты, а менеджер сертификатов вообще относится к шифрованию, и совершенно не обязан CertificateException наследовать от IOError или ещё кого-то, особенно из-за сериализатора. Можно конечно заворачивать ошибку в ошибку, но зачем, какую проблему решаем?
SP>И вообще не ясно, как решают проблему непроверяемые исключения? Добавилась у вас новая ошибка MySpecificStreamError. Кто-то её перехватит? Или подождут релиза когда у клиента вылетит необработанное? Вот как-то не помню чтобы я сильно мучался с проверяемыми исключениями в java (было это правда давно), зато отчётливо помню как напарывался на необработанные исключения.
Да, это косяк многих языков с исключениям. Но он, как мне кажется, совсем не решается на той стороне где его описывают. Весь checked (в моих фантазиях) надо переводить туда, где пишется сам try. Т.е. добавить какой-то новый, особенный, reliable_try, от которого а компилятор/линкер/VM/CLR потребуют (и сами через вызовы проследят), что все исключения которые могут вылететь — имеют catch.
Re[5]: Result objects - все-таки победили Exceptions?
·>Распараллелить кусок кода, и вот теперь у тебя проблема как пробрасывать исключения между тредами и что с ними делать потом.
Если "параллелить" по принципу "нашлепка сверху" (см. async/await), и исключения делать так же, то, действительно, так и есть. Но если параллельность (concurrency) с самого начала сделана как следует (с message passing в синтаксисе языка), и исключения тоже — в поведении есть логика. Выброс исключения в одном потоке, в зависимости от намерений программиста, может вызывать требуемые эффекты, причем очень элегантно. Базовые блоки (типа supervision) идут как часть stdlib, так что исключение может вести к повтору (retry) с заведомо рабочего состояния, или к прекращению выполнения операции (с возвратом ошибки), или вовсе конвертироваться в сообщение, которое будет передано процессу-родителю (trap exit).
Без исключений всю эту логику придется попросту навелосипедить.
Re[5]: Result objects - все-таки победили Exceptions?
AD> Лично мне нравится подход с ? тем, что визуально просматривая код, ты всегда видишь где ошибка передаётся на уровень выше.
Исключения по сути делают ? по умолчанию. Потому что это более частый случай, очень редко когда ошибку можно обработать на уровне вызывающего кода. Куда чаще ее нужно пробросить выше.
Можно было бы придумать оператор ! , но не так как в perl'e || или в Расте с паникой, а чтоб он означал "ошибку обязан обработать здесь". Но семантически это не имеет смысла, если код обработки и так уже есть, оператор ! не нужен. В конечном итоге, все равно получается как в Elixir, там этот "nothrow" настолько прижилися, что во многих интерфейсах почти всегда есть два варианта, с ! (throw) и без такового. Не новая концепция, но определенно рабочая.
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, Константин Б., Вы писали:
_>>Моду на коды ошибок вместо исключений ввели разработчики Rust
КБ>Так облажаться в первом же предложении. Result objects — это не коды ошибок. КБ>Когда вы уже поймете что ваш сишный опыт абсолютно уже не релевантен.
Как я понял, это код ошибки + полезный результат, упакованное вместе. Те же яйца, вид сбоку
Здравствуйте, hi_octane, Вы писали:
I>>А если catch только в конце — тогда как понять, что и где произошло, неужели stack разбирать? _>В языках с исключениями никто не заставляет делать catch на каждом шаге. Оптимистично делают кучу связанных друг с другом по смыслу шагов, а если какой-то шаг вылетает — у исключения, в норме, есть вполне информативный тип, по которому проверяют, и решают как обработать. Один метод, который забирает из сети, парсит и сохраняет в файл — может не иметь ни одной строчки try, но бросать 3 типа исключений. И снаружи будет всего один код обработки всего сразу и в одном месте. А этот код уже смотрит: SocketException — проблемы с сетью, FileNotFound — проблемы с файлом, и т.д.
Проблемы с сетью какие — вообще до back-end не достучаться оказалось, или какие-то данные не загрузились? А в первом случае — на устройстве пользователя вообще связи нет, или подключение сейчас к кривому wi-fi, который половину адресов не находит, а может что-то только что под блокировку попало? А если подробности зависят от того, с чего запрос начинался, в exception это как искать.
_>А иногда можно и ничего не делать, повторить операцию, и бывает прокатывает.
И как этот повтор будет выглядеть для пользователя — индикатор ожидания... затрудняюсь назвать сколько конкретно времени, но напрашивается выразить ненормативной лексикой... типа жди и надейся, что результат будет через конечное время. Дело вполне обычное нынче, но неужели это в порядке вещей?
_>Этот подход, вообще-то, уже отлично проявил себя в базах данных. Начали большую транзакцию, не вышло — ну и откатили. Но выкати БД, с заявой "в нашей идеологии если транзакция не прошла — то ложится вся база", и, мягко говоря, люди не поймут
Не понял идеи. Если не вышло, не возникает ли вопрос, какого *эпитет* не вышло. Или типа предлагать попробовать позже, авось когда-нибудь сработает, может, через час, может через год, может через пару-тройку тысячелетий.
_>Кстати, мало кто знает, почему в Rust трейт std::Error (кто не в теме — аналог интрефеса для ошибки) вполне официально советует сообщение об ошибке давать в нижнем регистре, с минимумом пунктуации. А всё потому, что периодически информацию об ошибке только из сообщения и получают.
В смысле из сообщения в exception разбирать, что произошло? Если так, то это даже похлеще, чем разбирать call stack.
I>>Try catch на каждом шаге — мало того, что громоздко, оно вроде ещё и по части производительности так себе. _>Именно что вроде. Как только начинают проверять реальный код — цифры могут быть очень разными. И программы с исключениями, и программы с кодами ошибок пишутся оптимистично — т.е. из расчёта что ошибок будет минимум (а исключений, соответственно, будет мало).
Про производительность я, конечно, не уверен, но что если inet у пользователя сбоит именно сейчас, или что-то только что оказалось под блокировкой? Но если в конце только ставить catch то там разве что и вправду какие-то экстраординарные методы нужны, чтоб понять, что всё-таки было. А если информация дополняется в процессе, то понятно, что делалось и где сбойнуло.
Если где-то framework делался во времена, когда ещё не были понятны все недостатки try-catch, то, естественно, поменять после уже сложно. Но если всё-таки однажды находятся способы сделать лучше, по мне это хорошо. Если оно и впрямь лучше, конечно, как в swift есть enum с параметрами, а где такого нет, то, конечно, дополнять информацию непросто будет.
Re[6]: Result objects - все-таки победили Exceptions?
Здравствуйте, SkyDance, Вы писали:
SD>·>Распараллелить кусок кода, и вот теперь у тебя проблема как пробрасывать исключения между тредами и что с ними делать потом. SD>Если "параллелить" по принципу "нашлепка сверху" (см. async/await), и исключения делать так же, то, действительно, так и есть. Но если параллельность (concurrency) с самого начала сделана как следует (с message passing в синтаксисе языка),
Это от парадигмы зависит. Вызов метода в ООП — это и есть message passing. Т.е. по сути — коллбэки. Так себе.
SD>и исключения тоже — в поведении есть логика. Выброс исключения в одном потоке, в зависимости от намерений программиста, может вызывать требуемые эффекты, причем очень элегантно. Базовые блоки (типа supervision) идут как часть stdlib, так что исключение может вести к повтору (retry)
В терминах ООП — это уже какой-нибудь декоратор.
SD> с заведомо рабочего состояния, или к прекращению выполнения операции (с возвратом ошибки), или вовсе конвертироваться в сообщение, которое будет передано процессу-родителю (trap exit).
Проблема исключений, что они нелокальны. Действия в одном куске кода внезапно перескакивают в совершенно другой кусок кода, без явной связи между скопами.
SD>Без исключений всю эту логику придется попросту навелосипедить.
error handling — одна из самых сложных проблем программирования.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[5]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
Q>>Тут мы подходим к тому, что у ошибок может быть разная сематика. Тот случай который я описал, если возникло исключение — это означает, что в программе баг и правильным способом будет закрыть программу с записью в лог об исключении. В случае если у объекта часть состояния находится за пределами программы(сетевые подключения, файлы и т.д) т.е. то за что программа не может нести ответственность, тут обычно требуется более сложная логика обработки ошибок. Тут мы можем определить разные классы exception и производить обработку для каждого вида ошибок
_>Я в твоём сообщении не могу отделить ошибки от исключений. А хочется определённости — в итоге-то что? Парсеру json, если пришёл битый, что делать — кидать "просто ошибку" или исключение, которое приведёт к закрытию программы? Ну и чтоб два раза не вставать — выход за границу массива это ошибка или исключение, которое должно всё сломать? А невозможность выделить память?
_>Самый прикол, что решение "исключения существуют, и это нормально, главное что мы их можем перехватывать и обрабатывать" — даёт универсальный и единообразный подход к самым разным проблемам. А вот подход — "у нас в языке исключений нет, ну может чуть-чуть, на донышке" — приводит к тупику. Решение rust как раз в духе, в моём понимании, дибилизма — делайте всего по 2.
Тут смысл в чем: в первом случае у нас объект находится полностью в нашей программе и выброс исключения означает ошибку в коде, во втором случая часть объекта как бы находится за пределами программы, например какой ни будь DataAdapter, в этом случае исключение может говорить об проблемах в БД, а не в программе. У этих ошибок разный смысл. Тоже самое с json парсером.
Программа – это мысли спрессованные в код
Re[6]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
_>А потому что NetworkStream может заиспользовать сертификаты, а менеджер сертификатов вообще относится к шифрованию, и совершенно не обязан CertificateException наследовать от IOError или ещё кого-то, особенно из-за сериализатора. Можно конечно заворачивать ошибку в ошибку, но зачем, какую проблему решаем?
Все что не является частью контракта — оборачиваете в Runtime Exception. Они не проверяемые. Все просто.
Проверяемые — это там где вам хочется впихнуть Result object.
Re[6]: Result objects - все-таки победили Exceptions?
S>>Слышали ли вы что-нибудь про наследование? _>Слышал. Вот есть базовый Stream, у него checked фиксированный набор исключений. И вдруг появляется новый NetworkStream, которому надо добавить новый CertificateException. А добавить низзя: базовый стрим менять не положено — на его checked уже весь клиентский код завязался. У пользователей уже свои checked зависят от тех checked что были у старого Stream. Приехали. В Java это проблему решили "элегантно" — забили на checked. Могли бы иначе — решили бы иначе.
Не знаком с концепцией checked exp, но почему CertificateException нельзя добавить на уровень NetworkStream, а не лезть в базовый класс? Непонятно, правд, как быть с полиморфизмом, если тип Stream, а передаётся NetworkStream...
Кодом людям нужно помогать!
Re[8]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
S>>Так сделайте наследника StreamException — в чем вопрос _>Ага, и тут у разработчиков менеджера сертификатов дилемма — делать CertificateException наследником StreamException, или HTTPException, или послать всех нафиг
Здравствуйте, SkyDance, Вы писали:
SD>·>Это от парадигмы зависит. Вызов метода в ООП — это и есть message passing. SD>Разве что в теории. На практике, в известных мне реализациях ООП вызов осуществляется синхронно, и синхронно же ждется, пока вызов что-то вернет. Это уж совсем не message passing.
sync/async — ортогонально message passing.
SD>·>В терминах ООП — это уже какой-нибудь декоратор. SD>Такой декоратор должен быть потокобезопасным и весьма изощренным. То есть, ожидаемо, он должен реализовывать половину того, что может быть реализовано общим образом в синтаксисе и рантайме языка.
А зачем обязательно в синтаксисе? Достаточно какой-нибудь concurrent queue на библиотечном уровне, пусть реализованный изощрённо, без разницы.
SD>·>Проблема исключений, что они нелокальны. Действия в одном куске кода внезапно перескакивают в совершенно другой кусок кода, без явной связи между скопами. SD>Это не проблема, а назначение! Правила "перескакивания" (та самая явная связь) между областями ответственности как раз и задаются с помощью синтаксиса языка. А где язык этого не может, городят декораторы.
Угу, назначение. Как и у оператора goto, например. Проблемой это быть не перестаёт.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[7]: Result objects - все-таки победили Exceptions?
Здравствуйте, SkyDance, Вы писали:
SD>Вызываемая функция не знает, хочет ли вызывающий код проверять, или просто пробросить наверх.
Так это уже вызывающий код решает — обернуть в RuntimeException — снять акцентуацию. Сделать inner (исключение — часть контракта, вложенная). Или же оставить проверяемым.
Re[9]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали: _>Этот подход, вообще-то, уже отлично проявил себя в базах данных. Начали большую транзакцию, не вышло — ну и откатили. Но выкати БД, с заявой "в нашей идеологии если транзакция не прошла — то ложится вся база", и, мягко говоря, люди не поймут
Этот подход, насколько я помню, очень плохо проявил себя в базах данных. Проверяемые исключения оставляют разработчика наедине с абстрактным SQLException, из которого совершенно невозможно вытащить ничего полезного.
В теории, можно было бы, скажем, делегировать львиную долю проверок согласованности реляционной базе, и ловить конкретные ситуации типа "нарушения внешнего ключа" или там "нарушение check constraint". Но это потребовало бы от разpаботчиков JDBC-драйвера усилий по поддержанию развитой системы типов исключений, поэтому в реале никто так не делает (см. например https://github.com/xerial/sqlite-jdbc/issues/367).
Так что разработчики enterprize-приложений вынуждены выпиливать лобзиком по вазелину, вручную проверяя все failure-сценарии, важные с точки зрения бизнеса.
Что неэффективно как с т.з. объёма кода, так и с т.з. нагрузки на СУБД.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[5]: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>Здравствуйте, hi_octane, Вы писали:
_>>Самый очевидный пункт: допустим у нас есть сериализатор, который пишет в абстрактный Stream. Этот Stream может быть диском, файлом, памятью, и т.п. Какие исключения объявить сериализатору, если он сам не знает, какой Stream ему дадут на вход? Заставить все Stream кидать абстрактный StreamWriteException?
S>Конечно StreamWriteException — это очевидно, причем во внутрь в innerException уже вкладывают то что породило данное исключение. В этом и есть смена акцентуации.
Отлично. А теперь напишите код, который использует этот сериализатор. Например, отлавливает FileAccessException и выводит пользователю диалог типа "попробовать ещё / указать другое имя файла".
Прикол в том, что пока сериализатор бросает "настоящее" исключение, у нас есть хоть и убогий, но паттерн-матчинг:
try
{
serializer.Write(config, fileStream);
}
catch(UnserializableValueException uve)
{
Alert("Sorry, the current config cannot be saved. Try changing options and save again");
}
catch(FileAccessException fex)
{
...
}
// все другие исключения успешно улетают наверх, чтобы дать вызывающему шанс их обработать. Компилятор следит за полнотой обработки
А теперь мы остаёмся наедине непонятно с чем:
try
{
serializer.Write(config, fileStream);
}
catch(StreamWriteException swe)
{
if(swe instanceof UnserializableValueException)
Alert("Sorry, the current config cannot be saved. Try changing options and save again");
else// забыли про FileAccessException :(throw;
}
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[6]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Но вот моя практика работы с языками с исключениями — она как раз такая, что провоцирует людей писать в стиле "исключений не существует; бремя доказательства обратного лежит на QA". Это означает, что стоимость работоспособного кода очень сильно возрастает — первоначально написанный "оптимистичный" код уезжает в коммит, чтобы спустя дни и недели попасть в third-line support со словами "тут иногда что-то где-то крэшится, найдите и почините". Это гораздо, гораздо дороже, чем сразу заставить программиста написать код обработки.
Абсолютно такое же ощущение/практика. Единственно, что это не только с исключениями так работает, но и с кодами возврата, и с просто некорректными результатами ака "обычно не бывает" — которые просто игнорируются, пока это возможно, если "не давать по рукам". У исключений есть даже преимущество в этом случае — они хотя бы не игнорируются по умолчанию, а вылетают выше уровнем и их легче задетектать, чем проигноренный результат от которого никаких следов может и не остаться к моменту неадекватного поведения.
Здравствуйте, Andir, Вы писали: A>Абсолютно такое же ощущение/практика. Единственно, что это не только с исключениями так работает, но и с кодами возврата, и с просто некорректными результатами ака "обычно не бывает" — которые просто игнорируются, пока это возможно, если "не давать по рукам". У исключений есть даже преимущество в этом случае — они хотя бы не игнорируются по умолчанию, а вылетают выше уровнем и их легче задетектать, чем проигноренный результат от которого никаких следов может и не остаться к моменту неадекватного поведения.
То, о чём вы говорите — тяжкое наследие "ассемблерноподобных" языков с рудиментарными системами типов.
Если вы пишете на ассемблере, то совершенно логичными являются всякие неявные соглашения вроде "мы возвращаем не int index, а некий алгебраический тип, устроенный так: если это положительное нечётное число — то index-1 это указатель на word; если положительное чётное — то x/2 даст нам само значение; если число отрицательное — то это код ошибки".
К моменту обработки вся эта тонкая семантика потеряна, т.к. в момент записи в регистр эта штука становится просто битами. Корректная их интерпретация — забота программиста; у компилятора просто нет информации, достаточной для отличения корректного кода от некорректного.
А современные языки оборудованы, в том числе, и exhaustiveness-checking. То есть если у меня тип значения — это "number | string", то мне недостаточно просто обложить арифметическую операцию с ним проверкой вида if(x is number) return x + 1;. Компилятор может и должен бить меня по рукам, "э-э-э, а ты не написал, что делать если там string!".
Строгая типизация сильно помогает в таких случаях. Но сильно упарываться в эту сторону становится контрпродуктивным, т.к. сложные системы типов становятся некомфортными в использовании как для разработчика, так и для компилятора.
Имеет смысл дополнять автовывод типов явными пред/пост условиями. Их преимущество по сравнению с императивными проверками — в том, что компилятор может попытаться статически доказать их корректность; в случае заведомой некорректности он даст по рукам; в случае заведомой корректности он едет дальше; в случае невозможности доказательства — оставляет проверки в рантайме.
Скажем, я бы не стал выражать концепцию "коллекция, отсортированная по возрастанию" в терминах системы типов. Громоздко и неэффективно.
А вот записать это требование в предусловиях метода бинарного поиска — норм. Современные системы верификации прекрасно
а) проверяют, что метод sort реально возвращает отсортированный массив, а не всякий мусор
б) в случае, если на вход методу binary_search подаётся неотсортированный массив, бьют программиста по рукам
в) в случае, если на вход методу binary_search подаётся массив, который был только что отсортирован, не тратят такты на проверку факта отсортированности.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>И вот тут оказывается, что если у нас на дне дерева вызовов возникает специфическая ситуация, которую мы хотим обработать поближе к корню, то у нас связаны руки.
S>Середина этого дерева знать не знает про эту специфическую ситуацию; её спроектировали в те времена, когда никому и в голову не приходило, что там внутри может что-то сломаться таким способом. S>В мире checked exceptions у нас есть три плохих способа это решить: S>1. Пройтись вверх по стеку вызовов и везде подправить сигнатуры, добавив MySpecificException к списку S>Это зачастую невозможно (потому что в стеке — четыре библиотеки от семи разных поставщиков, из которых девятеро уже давно ушли с рынка и их код никто не маинтейнит), да и вредно (потому что часть этих библиотек используется ещё в сорока местах нашего проекта, которым совершенно неинтересна обязанность обрабатывать ещё и MySpecificException) S>2. Отнаследоваться от какого-то исключения, которое в сигнатурах уже есть S>Это если в сигнатурах есть хоть что-то, и это что-то — не final. S>3. Завернуть MySpecificException в UniversalException S>Это если в сигнатурах есть UniversalException, в котором предусмотрено место для InnerException S>4. Выбросить потомка RuntimeException
S>В итоге практически реализуемым становится четвёртый способ, который сводит всю идею проверяемых исключений на нет.
Вполне согласен, но вот то, что вариант 4 сводит на нет всю идею проверяемых исключений — можно поспорить.
Мы же пишем код очень низкий по стеку вызовов. А это чаще всего означает, что мы вкладываем некую новую функциональность, которая изначально не была заложена во всех этих четырех библиотеках от семи разных источников. Ну например, работа с данными, которые поступают из файлов, которые лежат в какой-то встраиваемой ФС, а мы решили встроить новую ФС, а в ней нам нужно завести какое-то исключение, которого ни в одной из существующих ФС никогда не было. Такая уж странная ФС.
Если его и впрямь нельзя уложить в одно из исключений ранее существовавших ФС, то это значит, что мы работаем с чем-то принципиально новым. И тогда понятно, что прежний код (все четыре библиотеки от семи источников), в общем-то, не рассчитан на это нововведение. И обработать эту ошибку он не может (вариант с его переписыванием в виде (1) не обсуждаем). А вообще-то должен бы именно он это делать. А мы фактически хотим пробросить эту ошибку, минуя все слои, которые могли бы ее обработать, на один из верхних уровней, где обработать ее по существу едва ли удастся, удастся лишь признать неудачу. В самом деле, ну поймали мы ее там, и что дальше ? Разве что повторить операцию с какими-то изменениями... А это и значит, что возникло исключение, которого код не ждал. То есть фактически unchecked exception. И тут действительно можно выбросить RuntimeException. Потому что ситуация по сути мало чем отличается от деления на 0, которое вдруг произошло, когда мы добавили на нижнем уровне новый код. Не рассчитаны они на такое деление и не собираются его обрабатывать. А ArifmeticException поймать можно будет только на верхнем уровне. Поймать и сказать : "упс!"
Но это никак не отменяет тот факт, что FileNotFoundException будет нормально выбрасываться этой новой ФС и нормально обрабатываться второй из этих 4 библиотек, и будут там предприняты нужные действия, и все исправлено , а корень так и не узнает, что была такая проблема.
S>Именно поэтому в дотнете нету checked exceptions.
Подозреваю, что не поэтому, а потому что унаследовались от C++, а в нем тогда (не знаю как сейчас) throws в сигнатуре метода ровным счетом ничего не делало. Так, для красоты...
А с uncheked exception другая проблема. Я с ней именно на C# когда-то столкнулся. Все сделано по документации, все работает, и вдруг вылетает XyzException. В документации ни слова о том, что оно может быть выброшено. Да это и неудивительно, так как оно выбрасывается не вызываемым методом самим, а методом третьей из 4 библиотек семи разработчиков, о которых вызываемый метод и не знает ничего. Ну ладно, чертыхнулся, написал для него catch (хорошо, хоть было понятно, что за исключение и что делать) и задумался — а какие еще исключения этот метод может выбросить не сам, а в одной из этих 4 библиотек ? Дай ответ... Не дает ответа.
S>А вот если идти по пути "result object", то мы остаёмся в поле традиционной системы типов. S>Вот есть у нас библиотечная функция map, которая принимает коллекцию и трансформер f вида Func<T, R>. Нам совершенно всё равно, может ли возникнуть ошибка при вычислении f над конкретным экземпляром T или нет. S>Потому что всё это "спрятано" внутри типа R. Если f — это функция 1/x, то она возвращает тип "number | undefined" (или там "number | DivisionByZero"). Тип результата map соответственно будет Iterable<number | undefined> и обязанность принять решение, что с этим делать, остаётся у того, кто вызывает функцию map, а не у автора этой функции. Ровно так, как этого хотели авторы идеи исключений. S>А если f — это функция x ^ 0xF, то у неё тип результата — просто number, и вызов map с ней в качестве аргумента получит Iterable<number>. S>Написать функцию IEnumerable<R> map(IEnumerable<T> source, Func<T, R> f) c проверяемыми исключениями не представляется возможным. В ней функция f не имеет права ничего выбрасывать, потому что map пообещала ничего не выбрасывать.
С этим согласен.
With best regards
Pavel Dvorkin
Re[6]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
_>>Ну и чтоб два раза не вставать — выход за границу массива это ошибка или исключение, которое должно всё сломать? А невозможность выделить память? S>Всё то же самое. Корректный тип у функции типа malloc<T>(n: int) — это не T[], а T[] | OutOfMemory. Поэтому перед тем, как пользоваться полученным указателем, программист вынужден его проверять. А не просто надеяться на то, что память всегда выделиться ("у меня никогда не возвращался нулевой указатель"), и получить SegFault в произвольно далёкой от malloc точке.
Что делать в языках с GC без явного разграничения стэк/куча? Возьмём, к примеру, Hashell или Ocaml. Куда добавить OutOfMemory?
Re[8]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
A>>Абсолютно такое же ощущение/практика. Единственно, что это не только с исключениями так работает, но и с кодами возврата, и с просто некорректными результатами ака "обычно не бывает" — которые просто игнорируются, пока это возможно, если "не давать по рукам". У исключений есть даже преимущество в этом случае — они хотя бы не игнорируются по умолчанию, а вылетают выше уровнем и их легче задетектать, чем проигноренный результат от которого никаких следов может и не остаться к моменту неадекватного поведения. S>То, о чём вы говорите — тяжкое наследие "ассемблерноподобных" языков с рудиментарными системами типов.
Не согласен. Артефакт слабой системы типов тут имеет вклад безусловно, но не думаю, что он является решающим. Желание писать только happy-path, а остальное проигнорировать (ака оптимистичное программирование) — это скорее всего атрибут психики человека, а требовать от него проверить все возможные условия — только способ заставить искать обходные пути, чтобы это не проверять, а проигнорировать при любой возможности. Собственно мы это и наблюдаем в эволюции различных языков программирования — ака static vs dynamic, например.
S>А современные языки оборудованы, в том числе, и exhaustiveness-checking. То есть если у меня тип значения — это "number | string", то мне недостаточно просто обложить арифметическую операцию с ним проверкой вида if(x is number) return x + 1;. Компилятор может и должен бить меня по рукам, "э-э-э, а ты не написал, что делать если там string!". S>Строгая типизация сильно помогает в таких случаях. Но сильно упарываться в эту сторону становится контрпродуктивным, т.к. сложные системы типов становятся некомфортными в использовании как для разработчика, так и для компилятора.
Это хорошо выглядит в теории и при методе "аккуратного" программирования. А на практике всё также прекрасно игнорируется и код начинает весь выглядеть как "мамой клянусь тут всегда number", а если вдруг не number — то тогда произвольное валидное значение — например 0.
И даже современные языки в большинстве своём прекрасно живут с игнорированием, например, проблемы переполнения int32 в системе типов. Хотя это тоже артефакт "ассемблероподобных" языков и устройства процессоров собственно. Никто не хочет писать код пессимистично в этом случае и обрабатывать все возможные кейсы (ака exhaustiveness-checking).
Здравствуйте, Sinclair, Вы писали:
PD>>Мы же пишем код очень низкий по стеку вызовов. S>Мы — это кто?
Ты. В своем примере с деревом.
>В реальном ентерпрайз-коде типичная глубина стек-трейса — пара сотен фреймов.
Это да
PD>>А это чаще всего означает, что мы вкладываем некую новую функциональность, которая изначально не была заложена во всех этих четырех библиотеках от семи разных источников. Ну например, работа с данными, которые поступают из файлов, которые лежат в какой-то встраиваемой ФС, а мы решили встроить новую ФС, а в ней нам нужно завести какое-то исключение, которого ни в одной из существующих ФС никогда не было. Такая уж странная ФС. S>Ничего странного. Например, предкам не приходило в голову, что данные могут лежать не на диске, а в клауде.
Возможно. Подходящий пример, да.
PD>>Если его и впрямь нельзя уложить в одно из исключений ранее существовавших ФС, то это значит, что мы работаем с чем-то принципиально новым. И тогда понятно, что прежний код (все четыре библиотеки от семи источников), в общем-то, не рассчитан на это нововведение. И обработать эту ошибку он не может (вариант с его переписыванием в виде (1) не обсуждаем). А вообще-то должен бы именно он это делать. А мы фактически хотим пробросить эту ошибку, минуя все слои, которые могли бы ее обработать, на один из верхних уровней, где обработать ее по существу едва ли удастся, удастся лишь признать неудачу. S>С чего бы это? На самом верху — наш код, он знает, чем retryable HTTP error отличаются от non-retryable. S>в самом низу — тоже наш код, он как раз порождает разные виды исключений, в зависимости от кода HTTP-респонса. S>А вот в середине — треш, угар и содомия возрастом 30+.
Внизу у нас (в рамках примера с cloud) код, который работает с этой cloud file system, и знать про HTTP ничего не должен. Он чего-то из этой ФС берет и у него иногда не получается. А потом отдает выше и там треш и угар делают из этого HTTP какой-то, или же HTTP error хоть retryable, хоть нет
PD>>В самом деле, ну поймали мы ее там, и что дальше ? Разве что повторить операцию с какими-то изменениями... А это и значит, что возникло исключение, которого код не ждал. То есть фактически unchecked exception. И тут действительно можно выбросить RuntimeException.
S>Ну, так проблема — в том, что компилятор теперь нам ничего не говорит о том, где это исключение нужно ловить. Нам показалось, что мы его перехватили, а на практике оно вылетело куда-то вовсе не туда, где мы поставили catch. S>Это и есть подрыв всей идеологии checked exceptions.
Безусловно не туда, потому что туда не получится. Туда — это в треш и угар, там бы его и должны были бы обработать и сделать HTTP иным способом, может быть. Или HTTP error сделать. И это не зависит от того, checked или unchecked — мы этот треш и угар по условиям править не можем, а именно он HTTP error делает и верхнему уровню возвращает, а тот уж и решает, что потом с ним делать. А нижний уровень с файловой системой работает и знать про HTTP ничего не может.
PD>>Подозреваю, что не поэтому, а потому что унаследовались от C++, а в нем тогда (не знаю как сейчас) throws в сигнатуре метода ровным счетом ничего не делало. Так, для красоты... S>Нет. Никто там от С++ не наследовался. И подозревать причины нет — все ходы записаны, включая ход мысли отцов-основателей.
Допускаю. Равно как допускаю, что они неосознанно все же были под влиянием MSVC++. Не могли они его не знать.
PD>>А с uncheked exception другая проблема. Я с ней именно на C# когда-то столкнулся. Все сделано по документации, все работает, и вдруг вылетает XyzException. В документации ни слова о том, что оно может быть выброшено. Да это и неудивительно, так как оно выбрасывается не вызываемым методом самим, а методом третьей из 4 библиотек семи разработчиков, о которых вызываемый метод и не знает ничего. Ну ладно, чертыхнулся, написал для него catch (хорошо, хоть было понятно, что за исключение и что делать) и задумался — а какие еще исключения этот метод может выбросить не сам, а в одной из этих 4 библиотек ? Дай ответ... Не дает ответа. S>В Java у вас будет ровно то же самое. Как раз потому, что в сигнатуре ничего нет, и вдруг вылетает какой-то XyzException, отнаследованный от RuntimeException.
Тут будет, да. Но это особая ситуация. Произошло что-то непредвиденное. На все такие непредвиденные ситуации не напишешь throws. В конце концов NullPointerException может почти везде возникнуть , не будешь же его писать в каждом методе.
А вот "штатные" ошибки под контролем. Например, отсутствие файла, ошибка IO и все другие. Я не смогу написать код, который не учтет возможности их возникновения, мне просто не дадут это сделать. Мне придется решить, что я буду делать, если такая проблема возникнет. И это правильно. Если я пишу метод, читающий строки из файла, я должен написать код, который их читает и код, который что-то делает, если файла нет. Либо сам делает, либо декларирует — я не могу это обработать, оставляю обработку вызывающему коду. Четко и ясно. Входит в контракт метода.
Кстати. Кроме твоих 4 вариантов, есть и 5-й. Выбросить-таки RuntimeException, но завернуть в него этот самый XyZException. На нижнем уровне его генерируют, на верхнем должны теперь понимать, коль скоро на верхнем мы его теперь хотим обрабатывать. А треш и угар все это пройдет как нож через масло.
Здравствуйте, SkyDance, Вы писали:
SD>Э нет. Message passing по определению async. Как только у тебя появился wait, да к тому же строго ограниченный определенным предыдущим message, то все, это уже никакой не message passing. SD>·>А зачем обязательно в синтаксисе? SD>Потому что иначе получается фантастическое уродство, типа как в Go. Смотри, как элегантно можно в Erlang:
Это просто одна из моделей конкурентности — акторы. Можно Скалу посмотреть, там такое есть на уровне библиотек, не нужно вкорячивать в синтаксис ЯП. Достаточно лямбд, да паттерн-матчинга. Ну и ud-операторы для похожего синтаксиса.
SD> {success, Result} -> Result;
Т.е. никаких исключений, а result objects. ЧТД.
SD> %% а тут можно "поймать" исключение из связанного актора, например, так - {'EXIT', Reason} -> <обработка>
И теперь проблема разобраться — откуда этот 'EXIT' пришел/может прийти и как оно всё навернётся если вместо этого 'QUIT' какой-нибудь придёт.
SD>Очень выразительно. Мне недавно пришлось примерно такое на C# писать, так там 4 экрана кода и всякая магия c DI.
Что-то странно.
SD>·>Угу, назначение. Как и у оператора goto, например. Проблемой это быть не перестаёт. SD>У goto проблема в отсутствии (enforced) правил поведения. У исключения такие правила есть.
Это улучшает ситуацию, но не совсем решает...
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[7]: Result objects - все-таки победили Exceptions?
Здравствуйте, korvin_, Вы писали:
_>Что делать в языках с GC без явного разграничения стэк/куча? Возьмём, к примеру, Hashell или Ocaml. Куда добавить OutOfMemory?
Простите, я не понимаю ваш вопрос. Мне, если честно, строго всё равно, откуда malloc берёт память — со стека или из кучи.
Может, она вообще для одних n берёт из одного места, а для других — из другого.
Но как только malloc может закончиться неудачей, я добавляю это в сигнатуру malloc.
Опять же — если я считаю, что пользователю достаточно признака успешности, то у меня получается (T[] | undefined), он же Optional<T[]>.
А если мне интересны сценарии вроде "если не хватило памяти для X, давайте захватим X/2", то может потребоваться более интересная сигнатура, вроде (T[] | OutOfMemory), где OutOfMemory — это не унитарный тип, а некая структура с подробностями вроде хинтов на предмет того, какие n были бы приемлемыми.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[7]: Result objects - все-таки победили Exceptions?
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Внизу у нас (в рамках примера с cloud) код, который работает с этой cloud file system, и знать про HTTP ничего не должен.
Конечно же должен. Потому что клаудная "FS" — это некий Web API. Который, естественно, отвечает нам HTTP-шными кодами. Например — 403 с подробностями "token expired", которые совершенно непонятны среднему слою, зато понятны верхнему — который знает, что нужно токен обновить и попробовать снова.
PD>Безусловно не туда, потому что туда не получится. Туда — это в треш и угар, там бы его и должны были бы обработать и сделать HTTP иным способом, может быть.
Нет конечно. Там, в этом треше и угаре, просто нет и не может быть информации о том, что делать с HTTP исключениями. Эта информация есть только у того, кто вызывает треш и угар.
S>>Нет. Никто там от С++ не наследовался. И подозревать причины нет — все ходы записаны, включая ход мысли отцов-основателей. PD>Допускаю. Равно как допускаю, что они неосознанно все же были под влиянием MSVC++. Не могли они его не знать.
Это очень странный ход рассуждений. Во-первых, с тем же успехом можно сказать, что они были под влиянием Pascal — ведь там тоже нет checked exceptions. То есть наличие фичи откуда-то унаследовать можно; а вот унаследовать отсутствие фичи....
Во-вторых, в С++ исключения всё же входят в сигнатуру, хоть и не проверяются. В дотнете — нет, не входят. То есть "наследование" какое-то очень однобокое.
PD>Тут будет, да. Но это особая ситуация. Произошло что-то непредвиденное.
Нет тут ничего особенного. Исключения и придуманы для "непредвиденных" ситуаций. Предвиденные неприятности принято обрабатывать явно.
PD>На все такие непредвиденные ситуации не напишешь throws. В конце концов NullPointerException может почти везде возникнуть , не будешь же его писать в каждом методе.
Вовсе не везде. В целом, конечно же, крайне полезно знать, что в некоторых местах NullPointerException возникнуть не может.
PD>А вот "штатные" ошибки под контролем. Например, отсутствие файла, ошибка IO и все другие. Я не смогу написать код, который не учтет возможности их возникновения, мне просто не дадут это сделать. Мне придется решить, что я буду делать, если такая проблема возникнет. И это правильно. Если я пишу метод, читающий строки из файла, я должен написать код, который их читает и код, который что-то делает, если файла нет. Либо сам делает, либо декларирует — я не могу это обработать, оставляю обработку вызывающему коду. Четко и ясно. Входит в контракт метода.
Это всё работает только в плоских иерархиях вызовов. И совершенно никак не работает в коде на колбеках. Который, собственно, и является наиболее частоиспользуемым примером, когда мы контролируем "дно" и "крышу", а середину хотим куда-то делегировать.
PD>Кстати. Кроме твоих 4 вариантов, есть и 5-й. Выбросить-таки RuntimeException, но завернуть в него этот самый XyZException. На нижнем уровне его генерируют, на верхнем должны теперь понимать, коль скоро на верхнем мы его теперь хотим обрабатывать. А треш и угар все это пройдет как нож через масло.
Ну, я так и говорил, что способов три
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[7]: Result objects - все-таки победили Exceptions?
Здравствуйте, ·, Вы писали: ·>Тогда это становится по сути аналогом checked exceptions. В каждом методе придётся пробрасывать всю это портянку T | OutOfMemory | StackOverflow | ByteCodeVerificationError | DivisionByZero | NullPointer | ....
Во-первых, "пробрасывать" в таком случае гораздо легче, чем в случае checked exceptions. Потому что мне не нужно выписывать всю сигнатуру.
Достаточно обработать happy path а остальное вернуть как есть:
·>Если у меня есть большая портянка кода, которая считает скидку — ну если что-то где-то там навернулось, я это могу поймать на верхнем уровне и не применить скидку, чем просто терять весь заказ или вообще завалить весь процесс с panic.
Совершенно верно.
S>Тип результата map соответственно будет Iterable<number | undefined> и обязанность принять решение, что с этим делать, остаётся у того, кто вызывает функцию map, а не у автора этой функции. Ровно так, как этого хотели авторы идеи исключений.
Так именно (выделенное слово) в этом и состоит проблема. Хочет вызывающий код или нет, но он обязан "принять решение". Анализ больших кодовых баз показывает, что в большинстве случаев "решение" — это пробросить ошибку выше (завернув в свой слой описания ошибки, получив т.н. "bubble wrap"). Такую практику (только без экстра заворачивания) и "упросили" до значка ?, что ни разу не улучшает читаемость (и "писаемость" тоже).
Re[6]: Result objects - все-таки победили Exceptions?
S>Что с этим делать — решает пользователь этого парсера. Он может прямо тут же заматчить ошибку синтаксиса и, скажем, подставить дефолтный конфиг; или сделать retry, или просто прокинуть результат выше по стеку, делегировав принятие решения туда.
Последний вариант (прокинуть дальше) наиболее частый и вероятный в реальном коде. Поэтому он и должен работать по умолчанию (безо всяких ?)
S>Компилятор будет бить по рукам за попытку прочитать значение параметра из экземпляра типа "myConfig | BadSyntax".
Отвратительно. Теперь вместо processData(getData()) придется писать простыню из десятка строчек кода с имитацией исключений путем проброса BadSyntax ответа выше.
S>Не знаю насчёт Rust — никогда на нём не писал. Но вот моя практика работы с языками с исключениями — она как раз такая, что провоцирует людей писать в стиле "исключений не существует; бремя доказательства обратного лежит на QA".
Так это как раз идеальный вариант! Код получается чистым, читабельным, легко модифицируемым. Исключения начинают существовать только на том уровне, где они МОГУТ быть обработаны. Вместо того, чтобы на каждом уровне из 100 писать "на этом уровне обработать не могу, бросай дальше".
S>Любой словарик из метода get(key: K) возвращает не V, а Maybe<V>, и нужно обязательно проверить, нашлось ли значение по ключу. А для типового случая есть перегрузка get(key: K, default: V), которая позволяет обойтись без паттерн-матчинга результата. Ну, или язык умеет в x: V = mydict.get(k) ?? defX; вместо x: V = mydict.get(k, defX);, и тогда даже и перегрузка не нужна.
·>Это просто одна из моделей конкурентности — акторы. Можно Скалу посмотреть, там такое есть на уровне библиотек, не нужно вкорячивать в синтаксис ЯП.
Scala из-за этого сложнее читать. Да и в целом забавная судьба у Akka, и у некоторых аналогов.
·>
SD>> {success, Result} -> Result;
·>
·>Т.е. никаких исключений, а result objects. ЧТД.
Это не result object, а сообщение, и tagged record я просто как пример привел, для демонстрации работы pattern matching. Чтобы было видно, что там можно заматчить то, что тебе нужно на этом уровне обрабатывать.
·>И теперь проблема разобраться — откуда этот 'EXIT' пришел/может прийти и как оно всё навернётся если вместо этого 'QUIT' какой-нибудь придёт.
'EXIT' приходит с пояснениями (Reason). Если придет 'QUIT', он так и останется лежать в очереди, пока кто-то его оттуда не достанет и не попробует обработать.
SD>>Очень выразительно. Мне недавно пришлось примерно такое на C# писать, так там 4 экрана кода и всякая магия c DI. ·>Что-то странно.
Никакой странности, C# все-таки уж очень Java-inspired. Плюс все эти фреймворки, да best practices, да идиотский code formatter, требующий писать так:
if (success)
{
run_useful_code();
}
else
{
run_another_code();
}
·>Это улучшает ситуацию, но не совсем решает...
Думаю, в моем предыдущем ответе Sinclair стало понятно, что у нас разные взгляды на то, как должен выглядеть код.
Я хочу, чтобы код действительно читался как "исключений не бывает" — только happy path. А обработка ошибок явно происходит только на тех уровнях, где это действительно можно сделать, а не "обработать" в стиле "запишем в лог и пробросим дальше".
Re[7]: Result objects - все-таки победили Exceptions?
_>Поэтому, закончилось всё макросами, которые уже на автомате пишут, чтобы компилятор по рукам не бил а позволял сразу использовать результат не вникая в ошибку. Пишем тупо в конце каждой функции '?', и всё. Сведение этого бойлерплейта к пустой строке ("") — как раз и есть превращение Result Object в самый обычный Exception.
Во!
Я ровно это и написал уже несколько раз другим участникам. Поскольку это наиболее частый случай (пробросить дальше), как раз и нужно чтоб он не требовал никакого кода и прочих значков типа ?. И значок ! тоже не нужен, т.к. он означает "компилятор, на этом уровне я хочу обработать ошибку". Но в таком варианте нужно писать не !, а код обработки
писать код обработки.
_>С кодами ошибок (или с result object) — проблема та же самая. Вот типовой оптимистичный код (прямо из руководства по Rust):
_>Для кодеров, которые не пишут обработчики, а пробрасывают всё наверх, разница с исключениями только в том, что в случае исключений match, try! или '?' писать не нужно. Это почти такое же зло как ON ERROR RESUME NEXT в VB. Только теперь это ON ERROR RETURN FALSE. У нас как-бы заявлена, как-бы явная обработка ошибок, только она ...неявная
Йес!
Re[12]: Result objects - все-таки победили Exceptions?
Здравствуйте, SkyDance, Вы писали:
SD>·>Это просто одна из моделей конкурентности — акторы. Можно Скалу посмотреть, там такое есть на уровне библиотек, не нужно вкорячивать в синтаксис ЯП. SD>Scala из-за этого сложнее читать. Да и в целом забавная судьба у Akka, и у некоторых аналогов.
Ну это выходит за рамки обсуждения сабжа.
SD>·>[/code] SD>·>Т.е. никаких исключений, а result objects. ЧТД. SD>Это не result object, а сообщение,
А какая разница-то?
SD> и tagged record я просто как пример привел, для демонстрации работы pattern matching. Чтобы было видно, что там можно заматчить то, что тебе нужно на этом уровне обрабатывать.
С т.з. дизайна ЯП — неважно что паттерн-матчить. Если erlang умеет матчить только сообщения, но не умеет ud-типы — ну такое себе...
SD>·>И теперь проблема разобраться — откуда этот 'EXIT' пришел/может прийти и как оно всё навернётся если вместо этого 'QUIT' какой-нибудь придёт. SD>'EXIT' приходит с пояснениями (Reason). Если придет 'QUIT', он так и останется лежать в очереди, пока кто-то его оттуда не достанет и не попробует обработать.
Угу.. Т.е. по сути On Error Resume Next™.
SD>>>Очень выразительно. Мне недавно пришлось примерно такое на C# писать, так там 4 экрана кода и всякая магия c DI. SD>·>Что-то странно. SD>Никакой странности, C# все-таки уж очень Java-inspired. Плюс все эти фреймворки, да best practices, да идиотский code formatter, требующий писать так:
Ты куда-то не туда пошёл, уже к форматированию кода придираешься.
SD>·>Это улучшает ситуацию, но не совсем решает... SD>Думаю, в моем предыдущем ответе Sinclair стало понятно, что у нас разные взгляды на то, как должен выглядеть код. SD>Я хочу, чтобы код действительно читался как "исключений не бывает" — только happy path. А обработка ошибок явно происходит только на тех уровнях, где это действительно можно сделать, а не "обработать" в стиле "запишем в лог и пробросим дальше".
Так java-like unchecked исключения — оно и есть.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[9]: Result objects - все-таки победили Exceptions?
Здравствуйте, SkyDance, Вы писали:
S>>Точно ли вы хотите писать вместо этого SD>В реальности этот код будет выглядеть так:
Почему? Разные catch нужны если только надо делать разные действия. Обычно хватает catch (Exception), достаточно зарепортить исключение, пропустить применение скидки и может куда-то инфу послать, что нешмогла.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[13]: Result objects - все-таки победили Exceptions?
SD>>Это не result object, а сообщение, ·>А какая разница-то?
Разница в том, что у вызывающего кода нет обязанности обработать его ровно там, где хочется обрабатывать только success/happy path. Код выглядит чистым и понятным, — код обработки ошибок вынесен туда, где достаточно контекста для оной обработки.
·>Угу.. Т.е. по сути On Error Resume Next™.
Нет! В том-то и дело, что ровно наоборот: когда получен результат, продолжить выполнение. То есть в коде есть только happy path, а ошибки обрабатываются там, где им и положено. Конкретно в случае с Erlang, его стандартная библиотека содержит т.н. "супервизоры", которые по дефолту представляют дерево retry'ев. Если ошибки никак не обрабатывать, а только указать failure domains, то сначала будут retry в самом внутреннем домене, потом в домене побольше, и так далее.
·>Ты куда-то не туда пошёл, уже к форматированию кода придираешься.
Это все части одной истории, имя которой — читаемость кода. Если код замусорен всеми этими объявлениями checked exceptions, бессмысленной "обработкой ошибок" в стиле "запиши в лог и пробрось выше", да еще и отформатирован вот так — одна строчка превращается в 20. И прочитать пять вложенных вызовов занимает час вместо 5 минут.
·>Так java-like unchecked исключения — оно и есть.
Так я за них и ратую, и оправдываю C#-стиль без checked вообще.
Re[10]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, korvin_, Вы писали:
_>>Что делать в языках с GC без явного разграничения стэк/куча? Возьмём, к примеру, Hashell или Ocaml. Куда добавить OutOfMemory? S>Простите, я не понимаю ваш вопрос. Мне, если честно, строго всё равно, откуда malloc берёт память — со стека или из кучи. S>Может, она вообще для одних n берёт из одного места, а для других — из другого. S>Но как только malloc может закончиться неудачей, я добавляю это в сигнатуру malloc. S>Опять же — если я считаю, что пользователю достаточно признака успешности, то у меня получается (T[] | undefined), он же Optional<T[]>. S>А если мне интересны сценарии вроде "если не хватило памяти для X, давайте захватим X/2", то может потребоваться более интересная сигнатура, вроде (T[] | OutOfMemory), где OutOfMemory — это не унитарный тип, а некая структура с подробностями вроде хинтов на предмет того, какие n были бы приемлемыми.
Хорошо, давайте на примере.
Сначала Java:
class Foo {
...
// constructor
Foo(...) {
...
}
...
}
...
var x = new Foo(...);
Где здесь должно быть указано, что new Foo может вернуть не объект класса Foo, а OutOfMemory? В сигнатуре конструктора Foo? В "сигнатуре" оператора new? Тот же вопрос про StackOverflow: где он должен быть указан? В сигнатуре каждой функции (метода)? Операция () вызов метода автоматически должна его добавлять к каждому методу?
Теперь Haskell. Имеем такой код, например:
data Expr
= Val Int
| Add Expr Expr
| Mul Expr Expr
| If Expr Expr Expr
eval :: Expr -> Expr
eval e = ...
expr0 = If (Mul (Val 1) (Val 0))
(Add (Mul (Val 4) (Val 10))
(Val 2))
(Mul (Add (Val 7) (Val 14))
(Val 2))
print (eval expr0)) -- Val 42
Где тут должено быть указано OutOfMemory?
Re[5]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали:
TB>И вот тут оказывается, что у нас на дне дерева вызовов возвращается Result с каким-то Error для которого нет места в сигнатурах всех прочих функций.
Почему это? S>>Середина этого дерева знать не знает про эту специфическую ситуацию; её спроектировали в те времена, когда никому и в голову не приходило, что там внутри может что-то сломаться таким способом. TB>В мире Result у нас есть три плохих способа это решить:
S>>1. Пройтись вверх по стеку вызовов и везде подправить сигнатуры, добавив MySpecificException к списку TB>Пройтись вверх по стеку вызовов и везде подправить сигнатуры, добавив MySpecificError к списку возможных Error (это конечно если Error вообще предусматривает различные ошибки)
Автовывод решает эту проблему. Сигнатуры в явном виде пишутся там, где нам принципиален тип. И да, там нужно обрабатывать ошибку.
S>>2. Отнаследоваться от какого-то исключения, которое в сигнатурах уже есть TB>Заимплементить SomeTrait если в сигнатурах есть Box<dyn SomeTrait>
Зачем? S>>3. Завернуть MySpecificException в UniversalException TB>Завернуть MySpecificError в UniversalError или скастить в UniversalError.
Зачем? S>>4. Выбросить потомка RuntimeException TB>Выбросить панику
Паника — это да, это наше всё. S>>Написать функцию IEnumerable<R> map(IEnumerable<T> source, Func<T, R> f) c проверяемыми исключениями не представляется возможным. В ней функция f не имеет права ничего выбрасывать, потому что map пообещала ничего не выбрасывать.
TB>А нельзя тип исключения принять в качестве параметра шаблона?
А толку? Если у меня код бросает два разных типа исключения, то какой из них будет подставлен в качестве параметра шаблона? Most specific supertype? Ну, тогда просто в 99% будет подставляться Exception, и ещё в 1% — Throwable. Что, собственно, эквивалентно отключению checked exceptions вообще.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[7]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали:
TB>Ну в одном случае перечислили FileAccessException, в другом нет. А что мешает во втором его перечислить / не забыть перечислить?
Отсутствие поддержки компилятором. То есть мы получили недостатки checked exceptions без преимуществ checked exceptions.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[9]: Result objects - все-таки победили Exceptions?
Здравствуйте, korvin_, Вы писали:
_>Где здесь должно быть указано, что new Foo может вернуть не объект класса Foo, а OutOfMemory? В сигнатуре конструктора Foo? В "сигнатуре" оператора new?
Примерно там же, где сейчас указано, что new Foo может выбросить OutOfMemoryError. То есть — в "сигнатуре" оператора new.
_>Тот же вопрос про StackOverflow: где он должен быть указан? В сигнатуре каждой функции (метода)? Операция () вызов метода автоматически должна его добавлять к каждому методу?
Если идти по строгому пути — то да, в сигнатуре каждой функции.
_>Теперь Haskell. Имеем такой код, например:
_>
_>data Expr
_> = Val Int
_> | Add Expr Expr
_> | Mul Expr Expr
_> | If Expr Expr Expr
_>eval :: Expr -> Expr
_>eval e = ...
_>expr0 = If (Mul (Val 1) (Val 0))
_> (Add (Mul (Val 4) (Val 10))
_> (Val 2))
_> (Mul (Add (Val 7) (Val 14))
_> (Val 2))
_>print (eval expr0)) -- Val 42
_>
_>Где тут должено быть указано OutOfMemory?
Хороший вопрос. Надо полагать — везде.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[8]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, Pavel Dvorkin, Вы писали:
PD>>Внизу у нас (в рамках примера с cloud) код, который работает с этой cloud file system, и знать про HTTP ничего не должен. S>Конечно же должен. Потому что клаудная "FS" — это некий Web API. Который, естественно, отвечает нам HTTP-шными кодами. Например — 403 с подробностями "token expired", которые совершенно непонятны среднему слою, зато понятны верхнему — который знает, что нужно токен обновить и попробовать снова.
Так, давай по порядку.
До того, как мы добавили Cloud FS, все выглядело так
1. Верхний уровень. Получает от среднего (треш и угар) HTTPException, решает что в этом случае делать.
2. Средний уровень. Работает с HTTP, черт знает как, но работает. Иногда выбрасывает HttpException. Обращается к нижнему уровню для получения каких-то файлов.
3. Нижний уровень. Работает с ФС. Пока что у нас там или NTFS, или какая-то линуксовская ФС.
Откуда тут идут HTTPException ? Очевидно, только от среднего слоя. Не может нижний слой их генерировать. Никак не может.
Все так ?
Теперь вместо NTFS у нас некая CloudFS.
Ну если ты ФС, то будь добра вести себя как ФС. Собственно, даже не будь добра, а никуда не денешься. Потому что средний слой обращается к тебе как к ФС и не ждет от тебя никакого HTTP, равно как и FTP и т.д. Он работает по АПИ ФС, и только ошибки этого АПИ он может обрабатывать. Так что будь добра все свои HTTP/FTP проблемы решать сама, а в средний слой возвращать что-то связанное с ошибками файлов и потоков и тому подобным. IOException, например
Но не получилось. Иногда этот нижний слой выбрасывает XyzException, связанный с HTTP. Средний слой его не ждет и вообще-то обрабатывать не может. Поэтому они идет выше, в верхний слой, а это значит, что средний слой вообще-то потерпел неудачу — он вызвал нижний, а там необрабатываемая проблема. Значит, средний слой свое дело вообще не выполнил. Так что все верно, она попадает в верхний как неожиданное исключение. Просил средний слой какой-то файл ему дать, а в ответ — XyzException.
Короче, верхний слой должен ловить HttpException от среднего, именно там и идет работа с HTTP. А не от нижнего.
S>>>Нет. Никто там от С++ не наследовался. И подозревать причины нет — все ходы записаны, включая ход мысли отцов-основателей. PD>>Допускаю. Равно как допускаю, что они неосознанно все же были под влиянием MSVC++. Не могли они его не знать. S>Это очень странный ход рассуждений. Во-первых, с тем же успехом можно сказать, что они были под влиянием Pascal — ведь там тоже нет checked exceptions.
Ну в Pascal как он был вначале их нет вообще. Вроде как и в TurboPascal их не было, не помню точно. Вот в Delphi точно были.
>То есть наличие фичи откуда-то унаследовать можно; а вот унаследовать отсутствие фичи....
Вполне можно. Там ее не было, и тут не будет. Унаследовали отсутствие А то, что она есть в параллельном проекте (Java) — нам не указ.
S>Во-вторых, в С++ исключения всё же входят в сигнатуру, хоть и не проверяются. В дотнете — нет, не входят. То есть "наследование" какое-то очень однобокое.
PD>>Тут будет, да. Но это особая ситуация. Произошло что-то непредвиденное. S>Нет тут ничего особенного. Исключения и придуманы для "непредвиденных" ситуаций. Предвиденные неприятности принято обрабатывать явно.
checked exceptions и есть предвиденные неприятности, и их обрабатывают явно.
S>Вовсе не везде. В целом, конечно же, крайне полезно знать, что в некоторых местах NullPointerException возникнуть не может.
Если NPE может произойти, то оно произойдет. Если NPE произойти не может, оно все равно произойдет.
(C) моя перефразировка закона Мерфи.
S>Это всё работает только в плоских иерархиях вызовов. И совершенно никак не работает в коде на колбеках. Который, собственно, и является наиболее частоиспользуемым примером, когда мы контролируем "дно" и "крышу", а середину хотим куда-то делегировать.
Основная суть проблемы вот в чем. Ты доказываешь, что применение checked не панацея и приводишь примеры, когда они не очень успешно могут быть применимы. Я не спорю, не всегда все хорошо получится. Но зачем же, если нельзя обеспечить 100% успеха, заявлять, что это вообще никуда не годится ? Если во многих случаях это обеспечивает, что мы имеем меньше шансов забыть что-то важное, то почему этим не воспользоваться ? Пусть даже в других случаях это и не работает.
написал и запустил . Работает как часы, пока файл есть. А если его вдруг не окажется, будет хоть и не исключение (это же С), но плохо будет. А я забыл проверку поставить.
Не компилируется. На FileInputStream диагностика unhandled exception FileNotFoundException. На close — IOException.
Иди-ка, автор, и подумай, что ты делаешь. А если файла нет ? А если закрытие не прошло почему-то ? Изволь подумать, что делать и код написать.
Ну и что тут плохого ? Бог с ними, с верхними и средними уровнями и колбеками, но тут-то что плохого ?
With best regards
Pavel Dvorkin
Re[9]: Result objects - все-таки победили Exceptions?
Здравствуйте, Pavel Dvorkin, Вы писали:
S>>Это очень странный ход рассуждений. Во-первых, с тем же успехом можно сказать, что они были под влиянием Pascal — ведь там тоже нет checked exceptions.
PD>...Вот в Delphi точно были.
Не были.
Re[10]: Result objects - все-таки победили Exceptions?
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>А я не писал, что проверяемые. Я просто сказал, что в Delphi были. А в TurboPascal нет вроде.
Я же специально включил цитату, на которую вы отвечали:
S>>Это очень странный ход рассуждений. Во-первых, с тем же успехом можно сказать, что они были под влиянием Pascal — ведь там тоже нет checked exceptions.
PD>...Вот в Delphi точно были.
Re[9]: Result objects - все-таки победили Exceptions?
Здравствуйте, Pavel Dvorkin, Вы писали: PD>Так, давай по порядку.
PD>До того, как мы добавили Cloud FS, все выглядело так
PD>1. Верхний уровень. Получает от среднего (треш и угар) HTTPException, решает что в этом случае делать. PD>2. Средний уровень. Работает с HTTP, черт знает как, но работает. Иногда выбрасывает HttpException. Обращается к нижнему уровню для получения каких-то файлов. PD>3. Нижний уровень. Работает с ФС. Пока что у нас там или NTFS, или какая-то линуксовская ФС.
Нет. Никакого HTTPException в природе не было, т.к. средний уровень рассчитывает на совершенно определённую сигнатуру FS. В этой сигнатуре нет и не может быть никаких HTTPException.
PD>Теперь вместо NTFS у нас некая CloudFS.
PD>Ну если ты ФС, то будь добра вести себя как ФС. Собственно, даже не будь добра, а никуда не денешься. Потому что средний слой обращается к тебе как к ФС и не ждет от тебя никакого HTTP, равно как и FTP и т.д.
Ну так в этом и проблема, что он ничего не ждёт. А клауд — это клауд. И притворяться, что он такой же, как локальная FS — деградировать качество сервиса.
PD>Но не получилось. Иногда этот нижний слой выбрасывает XyzException, связанный с HTTP. Средний слой его не ждет и вообще-то обрабатывать не может. Поэтому они идет выше, в верхний слой, а это значит, что средний слой вообще-то потерпел неудачу — он вызвал нижний, а там необрабатываемая проблема. Значит, средний слой свое дело вообще не выполнил. Так что все верно, она попадает в верхний как неожиданное исключение. Просил средний слой какой-то файл ему дать, а в ответ — XyzException.
PD>Короче, верхний слой должен ловить HttpException от среднего, именно там и идет работа с HTTP. А не от нижнего.
В среднем слое нет никакой "работы с HTTP".
PD>Ну в Pascal как он был вначале их нет вообще. Вроде как и в TurboPascal их не было, не помню точно. Вот в Delphi точно были.
Отож. Тем более, что один из основных авторов языка C# и есть создатель Object Pascal (ЯП, применяемого в RAD IDE "Delph").
PD>Вполне можно. Там ее не было, и тут не будет. Унаследовали отсутствие А то, что она есть в параллельном проекте (Java) — нам не указ.
PD>checked exceptions и есть предвиденные неприятности, и их обрабатывают явно.
PD>Если NPE может произойти, то оно произойдет. Если NPE произойти не может, оно все равно произойдет.
PD>Основная суть проблемы вот в чем. Ты доказываешь, что применение checked не панацея и приводишь примеры, когда они не очень успешно могут быть применимы. Я не спорю, не всегда все хорошо получится. Но зачем же, если нельзя обеспечить 100% успеха, заявлять, что это вообще никуда не годится?
Затем, что самый нормальный способ использовать checked exceptions — это не использовать checked exceptions. Иначе оказывается, что они создают больше проблем, чем решают.
PD>Вот тебе простой пример.
PD>void read(char * filename){ PD>FILE* f = fopen(filename, "rb"); PD>// чтение PD>fclose(f); PD>} PD>написал и запустил . Работает как часы, пока файл есть. А если его вдруг не окажется, будет хоть и не исключение (это же С), но плохо будет. А я забыл проверку поставить.
Ну, я и говорю — пока у нас иерархия глубиной 1, всё в порядке. Как только появляются более глубокие иерархии — например, в qsort(), то мгновенно начинаются проблемы на ровном месте.
PD>void read(String filename) { PD>FileInputStream fis = new FileInputStream(new File(filename); PD>// чтение PD>close(fis); PD>}
PD>Не компилируется. На FileInputStream диагностика unhandled exception FileNotFoundException. На close — IOException. PD>Иди-ка, автор, и подумай, что ты делаешь. А если файла нет ? А если закрытие не прошло почему-то ? Изволь подумать, что делать и код написать.
PD>Ну и что тут плохого ? Бог с ними, с верхними и средними уровнями и колбеками, но тут-то что плохого ?
То, что когда я пытаюсь написать код с sort, компаратор не имеет права ничего выбрасывать. Это означает, что для отношений частичного порядка этот метод непригоден. В отсутствие checked exceptions я могу выбросить из компаратора исключение, чтобы показать несравнимость объектов — и пусть caller метода Arrays.sort разбирается, что с этим делать.
Или там когда я пытаюсь найти в Stream<String> строку, которая эквивалентна числу 42 (в произвольном представлении), я не могу использовать очевидный Integer::parseInt. И заменить его ничем не могу, потому что нет никакой возможности внутри mapToInt вернуть "я не могу отобразить входящее значение ни в какой int".
Ну, кроме выброса RuntimeException / Error.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[11]: Result objects - все-таки победили Exceptions?
Здравствуйте, korvin_, Вы писали: _>Сейчас это нигде не указано, кроме, разве что документации. Но вы не про документацию писали, а систему типов.
С моей точки зрения это примерно одно и то же. Вопрос именно в том, какой тип имеет результат операции new.
_>Удачи убедить хоть кого-нибудь писать на таком языке.
Возможно, вы правы.
_>И как это должно выглядеть?
Давайте повернём вопрос в другую сторону: а как это сделано в Haskell?
Я бегло почитал — ребята пишут, что исключения не требуют специальной языковой поддержки, т.к. для них достаточно монад. Ну, ок.
А что произойдёт, если хаскелл не смог сконструировать Add из Expr и Expr из-за OutOfMemory или StackOverflow?
_>-- ну и print, конечно, должен иметь сигнатуру вида _>-- print :: Show a => (a | OOM) -> (IO () | OOM) _>[/haskell]
_>То есть фактически OOM придётся писать буквально везде. Добавим сюда также StackOverflow, DivisionByZero и чёрт знает что ещё.
Я не очень понимаю, почему нам придётся писать OOM буквально везде. В первом варианте вы не писали никаких сигнатур у Add Expr Expr, а добавление OOM почему-то его потребовало.
Почему нельзя было просто добавить OOM в определение Expr как одной из альтернатив?
_>Удачи убедить хоть кого-нибудь писать на таком языке, да и самому не свихнуться от такого.
На первый взгляд, Хаскелл как раз такой язык. В том смысле, что я не могу просто взять и сделать throw в произвольном месте, не меняя сигнатуру функции (как это сделано в C++ и C#)
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[14]: Result objects - все-таки победили Exceptions?
S>>>Это очень странный ход рассуждений. Во-первых, с тем же успехом можно сказать, что они были под влиянием Pascal — ведь там тоже нет checked exceptions.
PD>>...Вот в Delphi точно были.
Ну значит, я не точно выразился. Прочитал, что исключения не могли быть заимствованы из Паскаля, и написал, что их там нет. А в Delphi есть. Какие — надо было уточнить
With best regards
Pavel Dvorkin
Re[12]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
_>>И как это должно выглядеть? S>Давайте повернём вопрос в другую сторону: а как это сделано в Haskell?
Что именно? OOM там не ловится никак. То же самое с SO — это внутренняя вещь рантайма.
В остальном — по-разному. Например, функция div имеет тип (Integral a => a -> a -> a) без указания какого либо исключения деления на 0, но исключение может возникнуть.
main :: IO ()
main = print $ 5 `div` 0
— никакого упоминания исключений.
Можно и перехватить:
import Control.Exception
test :: IO ()
test = print $ 5 `div` 0
main :: IO ()
main = catch test handler
where
handler :: SomeException -> IO ()
handler ex = putStrLn $ "Caught exception: " ++ show ex
S>Я бегло почитал — ребята пишут, что исключения не требуют специальной языковой поддержки, т.к. для них достаточно монад. Ну, ок.
Ну не совсем.
S>А что произойдёт, если хаскелл не смог сконструировать Add из Expr и Expr из-за OutOfMemory или StackOverflow?
Упадёт.
_>>-- ну и print, конечно, должен иметь сигнатуру вида _>>-- print :: Show a => (a | OOM) -> (IO () | OOM) _>>[/haskell]
_>>То есть фактически OOM придётся писать буквально везде. Добавим сюда также StackOverflow, DivisionByZero и чёрт знает что ещё. S>Я не очень понимаю, почему нам придётся писать OOM буквально везде. В первом варианте вы не писали никаких сигнатур у Add Expr Expr, а добавление OOM почему-то его потребовало. S>Почему нельзя было просто добавить OOM в определение Expr как одной из альтернатив?
Потому что OOM — это не часть Expr. Сигнатуры в примере с OOM вымышленные.
Что нам даст добавление OOM в качестве альтернативы?
Эти альтернативы -- просто конструкторы значений.
Всё равно, что в Java написать
var oom = new SomeInterface() {
...
};
Как это поможет?
expr0 = If (Mul ...) ... -- где здесь OOM?
_>>Удачи убедить хоть кого-нибудь писать на таком языке, да и самому не свихнуться от такого. S>На первый взгляд, Хаскелл как раз такой язык. В том смысле, что я не могу просто взять и сделать throw в произвольном месте, не меняя сигнатуру функции (как это сделано в C++ и C#)
IO так и делает: бросает исключения без указания в сигнатуре.
Вот теперь понял. Мы разные модели имели в виду. Я-то полагал, что средний слой работает сам с HTTP, но обращается к нижнему за какими-то данными из ФС. А ты имеешь в виду, что он с HTTP вовсе не работает.
Хорошо, тогда уточню модель.
Средний слой занимается машинной графикой. Там действительно может быть треш и угар, тем более что там еще и нативного кода полно.
Нижний слой дает ему картинки для этой графики, из ФС, а также другие файлы.
Пока нижний слой на базе NTFS, никакого HTTP вообще нет.
Заменяем NTFS на CloudFS и появляется HTTP в этой CloudFS и он может выбросить HTTPException.
Надеюсь, правильно понял теперь ?
В этом случае, действительно, при работе с CloudFS возможны HTTP-проблемы. Ну и что ? CloudFS выбрасывает HTTPException, и средний слой к нему не готов.
По-хорошему, CloudFS должна обрабатывать эти исключения сама. Она все же ФС, а значит, средний слой работает с ней как с ФС, а ФС не выбрасывает HTTPException. Какое, простите дело, слою машинной графики до того, как хранятся файлы ? Но допустим, все же выбросит. Поскольку средний слой не может его обработать в принципе, оно долетает до верхнего в виде RuntimeException, в которое оно завернуто. Верхний слой им займется.
Но это пока средний слой мы не можем править. А если все же можем, то это HttpException будет проходить по стеку методов среднего слоя и где-то обрабатываться.
Иными словами, сейчас средний слой должен обрабатывать FileNotFoundException как одно из исключений ФС. Обязан, иначе быть не может. Если же ФС изменяется и может выбросить еще какое-то CloudConnectionAbortedException, то ему нужно и это принять во внимание и прореагировать. Потому что это одна из ошибок ФС теперь.
Ну а пока он это не может делать, CloudConnectionAbortedException будет обрабатывать верхний слой. Он может лишь повторить операцию, если можно, или признать неудачу. Средний слой мог бы сделать что-то более разумное так же, как и для FileNotFoundException — если его средний слой поймает, это еще не причина кричать "караул", может быть, можно выполнить какое-то корректирующее действие.
S>Отож. Тем более, что один из основных авторов языка C# и есть создатель Object Pascal (ЯП, применяемого в RAD IDE "Delph").
Это да.
PD>>Вполне можно. Там ее не было, и тут не будет. Унаследовали отсутствие А то, что она есть в параллельном проекте (Java) — нам не указ. S>
А зря. Вот попал бы ты к индейцам какого-то племени , и оказался бы единственным бледнолицым и без перьев на голове в этой теплой компании. Отсутствие красного цвета кожи ты унаследовал от своих родителей, а отсутствие перьев — от европейской культуры. Отсутствие какого-то свойства тоже данные, если может быть и присутствие. Мы же не класс языка обсуждаем, а реальный мир.
PD>>Основная суть проблемы вот в чем. Ты доказываешь, что применение checked не панацея и приводишь примеры, когда они не очень успешно могут быть применимы. Я не спорю, не всегда все хорошо получится. Но зачем же, если нельзя обеспечить 100% успеха, заявлять, что это вообще никуда не годится? S>Затем, что самый нормальный способ использовать checked exceptions — это не использовать checked exceptions. Иначе оказывается, что они создают больше проблем, чем решают.
Ну как говорил один мой знакомый, "зафиксируем разногласия". В его устах это означало — к общему мнению прийти не удалось, но и обсуждать дальше не стоит.
PD>>Вот тебе простой пример.
PD>>void read(char * filename){ PD>>FILE* f = fopen(filename, "rb"); PD>>// чтение PD>>fclose(f); PD>>} PD>>написал и запустил . Работает как часы, пока файл есть. А если его вдруг не окажется, будет хоть и не исключение (это же С), но плохо будет. А я забыл проверку поставить. S>Ну, я и говорю — пока у нас иерархия глубиной 1, всё в порядке. Как только появляются более глубокие иерархии — например, в qsort(), то мгновенно начинаются проблемы на ровном месте.
PD>>void read(String filename) { PD>>FileInputStream fis = new FileInputStream(new File(filename); PD>>// чтение PD>>close(fis); PD>>}
PD>>Не компилируется. На FileInputStream диагностика unhandled exception FileNotFoundException. На close — IOException. PD>>Иди-ка, автор, и подумай, что ты делаешь. А если файла нет ? А если закрытие не прошло почему-то ? Изволь подумать, что делать и код написать.
PD>>Ну и что тут плохого ? Бог с ними, с верхними и средними уровнями и колбеками, но тут-то что плохого ? S>То, что когда я пытаюсь написать код с sort, компаратор не имеет права ничего выбрасывать. Это означает, что для отношений частичного порядка этот метод непригоден. В отсутствие checked exceptions я могу выбросить из компаратора исключение, чтобы показать несравнимость объектов — и пусть caller метода Arrays.sort разбирается, что с этим делать. S>Или там когда я пытаюсь найти в Stream<String> строку, которая эквивалентна числу 42 (в произвольном представлении), я не могу использовать очевидный Integer::parseInt. И заменить его ничем не могу, потому что нет никакой возможности внутри mapToInt вернуть "я не могу отобразить входящее значение ни в какой int".
Ну непригоден где-то, пусть. Я же не утверждаю, что он каждой дырке затычка. Непригоден — выброси unchecked, они же не отменены. В моем примере почему непригоден-то ?
S>Ну, кроме выброса RuntimeException / Error.
Именно. Но зачем отрицать везде, в том числе там, где вполне пригоден и уменьшает шансы сделать ошибку ?
With best regards
Pavel Dvorkin
Re[4]: Result objects - все-таки победили Exceptions?
Здравствуйте, hi_octane, Вы писали:
_>>>Моду на коды ошибок вместо исключений ввели разработчики Rust КБ>>Result objects — это не коды ошибок. _>Уже писал в этой же ветке
, что отличия настолько исчезающе малы, что их в лупу не разглядишь.
Потому что HRESULT? Сишный опыт — нерелевантен.
КБ>>Когда вы уже поймете что ваш сишный опыт абсолютно уже не релевантен >А если возвращаться к теме ошибок/исключений
А зачем от нее было уходить?
>, то первый раз столкнувшись с трейтом std::Error в Rust, у меня, среди матюков, проскочила мысль что это было предопределено — миллениалы должны были придумать достойного соперника для HRESULT
Ну да. std::Error плох потому что HRESULT. Сишный опыт — нерелевантен.
Re[3]: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>А что не современные этого не позволяли? В чем разница? Что принципиально нового появилось?
Сишечка не позволяла. На какой-нибудь яве легко можно было сделать.
S>Коды возврата использовали еще деды. Основная претензия была — из можно было забыть проверить.
Проблема специфичная для сишечки. Только их там не забывают. Их там просто не проверяют. Потому что с обработкой ошибок сишечка становится еще в 10 раз более убогая.
Оттуда и пошел миф про "забывают".
Например ты не найдешь не одной книги/статьи/туториала по го где "обработка ошибок опущена для простоты понимания".
Здравствуйте, SkyDance, Вы писали:
AD>>Хорошо ли это? Я думаю да. Знак вопроса не создаёт излишнего визуального шума и даёт при этом полезную информацию тому, кто читает код. Тут могут возразить те сторонники исключений, кому нафиг не сдалась инфа о том, что функция может бросить исключение. Но мне эта инфа нужна и знак ? в этом сильно помогает.
SD>Дело в том, что любая функция в любой момент может бросить исключение. Не OutOfMemory, так OutOfStack. И обработать это исключение можно только многими уровнями выше. Поэтому для меня это лишь бессмысленный шум, своеобразный карго-культ.
А раскажите пожалуйста как вы обрабатываете OutOfMemory и OutOfStack?
Re[13]: Result objects - все-таки победили Exceptions?
Здравствуйте, korvin_, Вы писали: _>Что именно? OOM там не ловится никак. То же самое с SO — это внутренняя вещь рантайма.
Ну, то есть panic. Скажем так: в такой среде сложно писать софт, который должен работать 24*7.
_>В остальном — по-разному. Например, функция div имеет тип (Integral a => a -> a -> a) без указания какого либо исключения деления на 0, но исключение может возникнуть.
Хм. То есть кидать можно всё, что угодно и где угодно?
Я посмотрел вот сюда. Написано
The great thing about Haskell is that it is not necessary to hard-wire the exception handling into the language.
То есть исключений нет, но если хочется прикрутить их поддержку, то можно их реализовать самостоятельно.
Читаем дальше:
Haskell solves the problem a diplomatic way: Functions return error codes, but the handling of error codes does not uglify the calling code.
Там написана неправда? _>Ну не совсем.
S>>А что произойдёт, если хаскелл не смог сконструировать Add из Expr и Expr из-за OutOfMemory или StackOverflow? _>Упадёт.
Как-то грустненько.
S>>Почему нельзя было просто добавить OOM в определение Expr как одной из альтернатив?
_>Потому что OOM — это не часть Expr. Сигнатуры в примере с OOM вымышленные. _>Что нам даст добавление OOM в качестве альтернативы? _>Эти альтернативы -- просто конструкторы значений.
А выглядит как алгебраический тип.
_>Всё равно, что в Java написать
_>
_>var oom = new SomeInterface() {
_> ...
_>};
_>
_>Как это поможет?
Поможет не писать везде Expr | OOM, а встроить OOM в Expr. _>
_>expr0 = If (Mul ...) ... -- где здесь OOM?
_>
В рамках Haskell, наверное, нигде. Язык не готов к работе в условиях дефицита памяти
_>IO так и делает: бросает исключения без указания в сигнатуре.
Ну, то есть в IO встроена возможность бросать всё что угодно? откуда в div появилась возможность "бросить" divisionByZero?
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[15]: Result objects - все-таки победили Exceptions?
Здравствуйте, korvin_, Вы писали: _>Вот интересно, а что вы делаете в Java при получении OOM?
Смотря какой код у меня исполняется. Есть применения, где при получении OOM мне нужно не просто 500 Internal Error, а успеть сделать что-то полезное (например, сохранить стейт воркфлоу) перед тем, как умереть.
_>Там написано не про это. Насчёт does not uglify the calling code я бы поспорил.
Другого описания работы исключений в haskell я не нашёл.
_>А что вы предлагаете делать при OOM?
Много что можно сделать, особенно если озаботиться заранеешным созданием объектов, которые потребуются для обработки OOM. Например, можно урезать осетра и попробовать продолжить с буфером меньшего размера (хоть и медленнее).
_>Это алгебраический тип, но при чём тут это?
То, что OOM могло бы быть одним из его слагаемых.
_>И что с ним делать в Expr? Давайте добавим. Как это отразится на expr0? Что вообще дальше делать с этим OOM?
Обрабатывать или прокидывать.
Ну, вот что нам делать с None в Optional<T>? Как замена int на Maybe<int> отразится на арифметике? Всё зависит от того, как сделан лифтинг в языке — автоматически, монадически, или никак.
_>Где бы он был в Java?
В Java он бы вылетел в виде unchecked exception. В воображаемой Java без исключений, но с not-nullable reference типами мы могли бы заставить new возвращать не T, а T?. В воображаемой Java с алгебраическими типами и наследованием мы могли бы заставить new T возвращать не T, а T | Error, и для общения с кодом, который требует чистый T этот результат пришлось бы очищать.
Здравствуйте, Константин Б., Вы писали:
M>>Как я понял, это код ошибки + полезный результат, упакованное вместе. Те же яйца, вид сбоку
КБ>Неправильно понял. Это не код ошибки, это объект ошибки. Т.е. как исключение только явно возвращаемое из функции. КБ>Совершенно лишенное недостатков кодов ошибок.
Не понял тогда, а как возвращается полезный результат?
Здравствуйте, Shmj, Вы писали:
S>Ну вот рекомендации в новомодных языках: https://docs.flutter.dev/app-architecture/design-patterns/result
S>- все-таки топят за Result objects для бизнес-логики.
S>А ведь это всю цепочку поддерживать. Как-то много лишних букв добавляется.
Для любой логики:
1. негативный результат, в т.ч. логическая ошибка операции — это result object
2. системные ошибки, ситуация "а хер его знает" — исключение
Т.е. должны быть оба этих механизма
Примеры:
1. не получается зарезервировать покупку
2. запрос к сервису фейлится по таймауту, ошибке днс, сервис возвращает html вместо json итд
Если кидать 2й кейс как result object, получаем много геморроя на ровном месте
Если кидать 1й кейс как исключения, полуаем так же много геморроя на ровном месте
Идеальный процессинг, на мой взгляд, такой:
controller_method(args) {
try {
const useCase = new ReserveItemUseCase(args);
return useCase.invoke(); // логические ошибки идут сюда, их логируем по месту возникновения
} catch(exception) {
logger.error({exception}, 'ReserveItemsUseCase');
throw Application.wrap(exception);
}
}
такой try catch при желании можно убрать в generic code, останется только useCase.
Проблема возникает в том случае, например, если логическую операцию нужно откатить или повторить если возникает исключение
В этом случае можно чз утилиту settled, которая возвращает тупл [exception, result]
Здравствуйте, Sinclair, Вы писали:
_>>Это алгебраический тип, но при чём тут это? S>То, что OOM могло бы быть одним из его слагаемых.
Вот аналог такого алгебраического типа на Java:
public class OOMDemo {
public static void main(String[] args) {
var expr = new If(new Mul(new Val(1), new Val(0)),
new Add(new Mul(new Val(4), new Val(10)),
new Val(2)),
new Mul(new Val(2), new Add(new Val(7), new Val(14))));
System.out.println(expr.eval()); // 42
}
}
sealed interface Expr {
int eval();
}
record Val(int value) implements Expr {
public int eval() {
return value;
}
}
record Add(Expr x, Expr y) implements Expr {
public int eval() {
return x.eval() + y.eval();
}
}
record Mul(Expr x, Expr y) implements Expr {
public int eval() {
return x.eval() * y.eval();
}
}
record If(Expr cond, Expr thenCase, Expr elseCase) implements Expr {
public int eval() {
if (cond.eval() == 0) {
return thenCase.eval();
} else {
return elseCase.eval();
}
}
}
record OOM() implements Expr {
public int eval() {
throw new OutOfMemoryError("OOM");
}
}
Добавил OOM в качестве слагаемого. Что дальше?
Re[17]: Result objects - все-таки победили Exceptions?
Здравствуйте, korvin_, Вы писали: _>Это возможно только если под этот твой процесс сохранениня стейта уже была выделена вся необходимая для его работы память, что в языках с GC практически невозможно сделать
Это ещё почему? Сразу выделяем всё на старте. _>соответственно твоё "сделать что-то полезное" точно так же наткнётся на ООМ.
Не обязательно.
_>Это в теории, на практике -- нет. Даже в С/C++ и прочих языках с "ручным" контролем памяти что-то сделать сложно. Поэтому приходит OOM-Killer
OOM-Killer — это следствие криворукой архитектуры линукса. Программы под винду пишутся на тех же С/С++, но при этом там нет (и не нужно) никакого OOM-Killer.
_>Поэтому на практике, что можно сделать: упасть. Гипервизор/оркерстратор перезапустит ноду/сервис/под/контейнер, а мы будем изучать логи, метрики, дампы, и по результату, либо добавим больше памяти, либо изменим параметры масштабирования, либо починим утечку, либо оптимизируем код.
Это очень, очень дорогостоящая процедура. Даже если удалось обойтись без обращения к разработчикам за починкой — всё равно: добавление памяти и перезапуск стоят дней простоя.
Не, я понимаю, что самый простой способ — это просто надеяться. Перезапускаем — может на этот раз карта ляжет так, что упавший процесс успеет отработать до того, как процесс с утечкой сожрёт всю память. _>Так и что бы это дало? Как его использовать? Напиши пример.
Повторюсь: пример зависит от области деятельности и от того, где именно вылетел OOM. Если я получил OOM при создании шестибайтового объекта — да, скорее всего продолжать вообще смысла нет, и можно просто перезапускать весь контейнер или даже всю ноду. А если я пытаюсь выделить буфер размером в 100М, то шанс велик, что после вылета ООМ у меня всё ещё ~50М свободно. И можно заняться чем-то другим, не столь прожорливым к памяти. Или попробовать выделить буфер вдвое меньше. Ну, как пример — сортируем файл интов размером в 100гб. Понятно, что нужно делать сортировку слиянием; и понятно, что чем больше размер чанка, тем быстрее мы закончим. Но это не означает, что неудача выделения чанка в 10G — это катастрофа и нужно делать паник. Можно и 100мб кусками его сортировать — просто это займёт больше времени.
_>Так Optional -- это другой тип по отношению к int.
Ну, так и Expr | OOM — это другой тип по отношению к Expr. _>Теперь, сам Optional же тоже требует памяти, значит Optional<Optional<T>>, и для него тоже нужна память, Optional<Optional<Optional<T>>>, и так далее.
Нет, зачем?
1. В хорошей системе типов Optional Optional == Optional.
2. Независимо от этого, new может всегда резервировать память под sizeof(optional)+sizeof(T), и возвращать none невзирая на то, кому из них не хватило места
4. Независимо от этого, вычислительная модель может быть построена на ref-типах (см. Java), и тогда Optional<T> вообще не требует никаких накладных расходов. Это ровно тот же "ссылка на T", только в отличие от "обычной" ссылки, эта может принимать значение none.
_>Так вот в Haskell/Ocaml и т.п. у нас нет new T, у нас сразу T, как 1, 2, true, false, "foo", "bar". Сразу значение.
Ну, это же иллюзия. Значения всё равно как-то конструируются. Иначе бы любая программа на Haskell была бы просто константой.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[16]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
_>>Вот интересно, а что вы делаете в Java при получении OOM? S>Смотря какой код у меня исполняется. Есть применения, где при получении OOM мне нужно не просто 500 Internal Error, а успеть сделать что-то полезное (например, сохранить стейт воркфлоу) перед тем, как умереть.
В .Net System.Drawing в свое время могло кидать OOM на ровном месте, при определенном сочетании параметров отрисовки. Например — отрисовка прерывистой линии когда расстояние меньше 1 пиксела.
Никакой нехватки памяти конечно же не было. Пришлось рендеринг примитивов завернуть в try-catch и просло логировать для дебага.
Re[18]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, korvin_, Вы писали: _>>Это возможно только если под этот твой процесс сохранениня стейта уже была выделена вся необходимая для его работы память, что в языках с GC практически невозможно сделать S>Это ещё почему? Сразу выделяем всё на старте.
Это проблематично сделать, учитывая, что даже String (любая конкатенация == аллокация) и любой массив в Java -- Object. При этом нужно быть осторожным с использованием любого библиотечного кода, который может что-то аллоцировать, в том числе и неявно, и даже языковых конструкций, типа for (var x : array) { ... }, т.к. она приводит к аллокации итератора. В Хаскелле контроллировать аллокации ещё сложнее, явного new нет, любое значение может быть (и скорее всего будет) аллоцировано динамически, любой "вызов" функции может породить thunk. Удачи "выделить сразу всё на старте".
_>>соответственно твоё "сделать что-то полезное" точно так же наткнётся на ООМ. S>Не обязательно.
Почти гарантированно.
_>>Это в теории, на практике -- нет. Даже в С/C++ и прочих языках с "ручным" контролем памяти что-то сделать сложно. Поэтому приходит OOM-Killer S>OOM-Killer — это следствие криворукой архитектуры линукса. Программы под винду пишутся на тех же С/С++, но при этом там нет (и не нужно) никакого OOM-Killer.
Может и криворукой, но что делается в Windows в таких случаях?
_>>Поэтому на практике, что можно сделать: упасть. Гипервизор/оркерстратор перезапустит ноду/сервис/под/контейнер, а мы будем изучать логи, метрики, дампы, и по результату, либо добавим больше памяти, либо изменим параметры масштабирования, либо починим утечку, либо оптимизируем код. S>Это очень, очень дорогостоящая процедура. Даже если удалось обойтись без обращения к разработчикам за починкой — всё равно: добавление памяти и перезапуск стоят дней простоя.
Какая процедура дорогостоящая? Перезапуск к Кубернетесе стоит секунды простоя. Добавление memory request/limits -- считанные минуты. При этом, это может коснуться только одного пода, остальные продолжат работать, если у них нет проблем. Тогда для пользователя никакого простоя не будет. Я уж не говорю о том, что ваш graceful shutdown при OOM это никак не решает. Памяти всё равно не хватило. О каких 24/7 вы говорили вообще?
S>Не, я понимаю, что самый простой способ — это просто надеяться. Перезапускаем — может на этот раз карта ляжет так, что упавший процесс успеет отработать до того, как процесс с утечкой сожрёт всю память. _>>Так и что бы это дало? Как его использовать? Напиши пример. S>Повторюсь: пример зависит от области деятельности и от того, где именно вылетел OOM. Если я получил OOM при создании шестибайтового объекта — да, скорее всего продолжать вообще смысла нет, и можно просто перезапускать весь контейнер или даже всю ноду. А если я пытаюсь выделить буфер размером в 100М, то шанс велик, что после вылета ООМ у меня всё ещё ~50М свободно. И можно заняться чем-то другим, не столь прожорливым к памяти. Или попробовать выделить буфер вдвое меньше. Ну, как пример — сортируем файл интов размером в 100гб. Понятно, что нужно делать сортировку слиянием; и понятно, что чем больше размер чанка, тем быстрее мы закончим. Но это не означает, что неудача выделения чанка в 10G — это катастрофа и нужно делать паник. Можно и 100мб кусками его сортировать — просто это займёт больше времени.
И долго вы будете гадать на кофейной OOM-гуще, сколько памяти вам надо? А если другой поток/процесс потребует столько же памяти? Если вы хотите буфер 10G, может, вам изначально стоило озаботиться наличием необходимой памяти?
_>>Так Optional -- это другой тип по отношению к int. S>Ну, так и Expr | OOM — это другой тип по отношению к Expr.
Но вы предлагали засунуть OOM в Expr, т.е. сделать не другой тип.
_>>Теперь, сам Optional же тоже требует памяти, значит Optional<Optional<T>>, и для него тоже нужна память, Optional<Optional<Optional<T>>>, и так далее. S>Нет, зачем? S>1. В хорошей системе типов Optional Optional == Optional.
Нет, в любой нормальной системе типов Optional Optional -- это Optional Optional, также как List of List of T -- это List of List of T, а не List of T. А если они у вас автоматически схлопываются, у вас дерьмовая система типов.
S>2. Независимо от этого, new может всегда резервировать память под sizeof(optional)+sizeof(T), и возвращать none невзирая на то, кому из них не хватило места S>4. Независимо от этого, вычислительная модель может быть построена на ref-типах (см. Java), и тогда Optional<T> вообще не требует никаких накладных расходов. Это ровно тот же "ссылка на T", только в отличие от "обычной" ссылки, эта может принимать значение none.
Она может не требовать, если компилятор особым образом компилирует Optional, а иначе это такой же тип-сумма, как любой другой со всеми вытекающими.
_>>Так вот в Haskell/Ocaml и т.п. у нас нет new T, у нас сразу T, как 1, 2, true, false, "foo", "bar". Сразу значение. S>Ну, это же иллюзия. Значения всё равно как-то конструируются. Иначе бы любая программа на Haskell была бы просто константой.
Конструируются, конечно, но вы никак не можете знать, на стэке или в куче. Любой невинный Int может запросто быть аллоцирован в куче и никак не получится преаллоцироват его для работы в OOM-хэндлере.
Re[19]: Result objects - все-таки победили Exceptions?
Здравствуйте, korvin_, Вы писали:
_>Может и криворукой, но что делается в Windows в таких случаях?
В Windows заранее резервируется нужное количество памяти. Если зарезервировать не удаётся, то процесс получает отказ. У Винды в принципе нет такой проблемы, как "я пообещала ста процессам по гигабайту рамы, а теперь они все за ней пришли, а на диске места нет".
_>Какая процедура дорогостоящая? Перезапуск к Кубернетесе стоит секунды простоя. Добавление memory request/limits -- считанные минуты. При этом, это может коснуться только одного пода, остальные продолжат работать, если у них нет проблем. Тогда для пользователя никакого простоя не будет.
Хм. Я, наверное, чего-то не понимаю. Вы подразумеваете, что кубер автоматически добавит памяти в контейнер, который упал из-за OOM?
_>Я уж не говорю о том, что ваш graceful shutdown при OOM это никак не решает. Памяти всё равно не хватило. О каких 24/7 вы говорили вообще?
Я повторно намекаю, что "всё равно не хватило" — это не единственный возможный сценарий ООМ. Если у нас там память течёт, то перезапуск контейнера с увеличенной памятью просто сделает чуть дольше ожидание следующего OOM.
_>И долго вы будете гадать на кофейной OOM-гуще, сколько памяти вам надо? А если другой поток/процесс потребует столько же памяти? Если вы хотите буфер 10G, может, вам изначально стоило озаботиться наличием необходимой памяти?
Ну вот я и пытаюсь ей изначально озаботиться — пробую выделить 10G. И меня очень разочарует паника и перезапуск в случае, если 10G нет. Хотелось бы чего-то более конструктивного.
_>Нет, в любой нормальной системе типов Optional Optional -- это Optional Optional, также как List of List of T -- это List of List of T, а не List of T. А если они у вас автоматически схлопываются, у вас дерьмовая система типов.
Tastes differ.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[6]: Result objects - все-таки победили Exceptions?
TB>>И вот тут оказывается, что у нас на дне дерева вызовов возвращается Result с каким-то Error для которого нет места в сигнатурах всех прочих функций. S>Почему это?
Напоминаю твой пост:
S>И вот тут оказывается, что если у нас на дне дерева вызовов возникает специфическая ситуация, которую мы хотим обработать поближе к корню, то у нас связаны руки.
Ну и почему это такое случается? И почему если такое может случиться с Checked Exceptions то не может случиться с Result? Какая нафиг разница-то?
S>Автовывод решает эту проблему. Сигнатуры в явном виде пишутся там, где нам принципиален тип. И да, там нужно обрабатывать ошибку.
Как автовывод поможет тебе поправить сигнатуры всех промежуточных функций? Надеешься что они шаблонные?
TB>>Заимплементить SomeTrait если в сигнатурах есть Box<dyn SomeTrait> S>Зачем?
Чтобы вернуть новый еррор для которого нет места в иерархии промежуточных функций.
S>>>3. Завернуть MySpecificException в UniversalException TB>>Завернуть MySpecificError в UniversalError или скастить в UniversalError. S>Зачем?
Чтобы вернуть новый еррор для которого нет места в иерархии промежуточных функций.
S>>>4. Выбросить потомка RuntimeException TB>>Выбросить панику S>Паника — это да, это наше всё.
А нафига тогда вводили все эти Result?
TB>>А нельзя тип исключения принять в качестве параметра шаблона? S>А толку? Если у меня код бросает два разных типа исключения, то какой из них будет подставлен в качестве параметра шаблона? Most specific supertype?
Оба. Для вывода бросаемого лямбдой исключения чтоб суммировал типы исключений, бросаемые функциями, вызываемыми лямбдой.
Кстати, а что делает Раст если лямбда вызывает функции, которые возвращают Result с разными Error? Правильно, шлёт нафиг и требует смаппить к одному варианту.
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[14]: Result objects - все-таки победили Exceptions?
Здравствуйте, SkyDance, Вы писали:
SD>>>Это не result object, а сообщение, SD>·>А какая разница-то? SD>Разница в том, что у вызывающего кода нет обязанности обработать его ровно там, где хочется обрабатывать только success/happy path. Код выглядит чистым и понятным, — код обработки ошибок вынесен туда, где достаточно контекста для оной обработки.
Я не очень понимаю. Как на ерланге написать код вроде
int add(string s1, string s2)
{
int i1 = parse(s1);
int i2 = parse(s2);
return i1 + i2;
}
и как где там будет посылаться ошибка парсинга? И кем-когда обрабатываться?
SD>·>Угу.. Т.е. по сути On Error Resume Next™. SD>Нет! В том-то и дело, что ровно наоборот: когда получен результат, продолжить выполнение. То есть в коде есть только happy path, а ошибки обрабатываются там, где им и положено.
Ну вот в случае parse — где им положено?
SD>Конкретно в случае с Erlang, его стандартная библиотека содержит т.н. "супервизоры", которые по дефолту представляют дерево retry'ев. Если ошибки никак не обрабатывать, а только указать failure domains, то сначала будут retry в самом внутреннем домене, потом в домене побольше, и так далее.
Про erlang я очень плохо знаю. Поэтому не очень понял суть этого высказывания.
SD>·>Ты куда-то не туда пошёл, уже к форматированию кода придираешься. SD>Это все части одной истории, имя которой — читаемость кода. Если код замусорен всеми этими объявлениями checked exceptions, бессмысленной "обработкой ошибок" в стиле "запиши в лог и пробрось выше", да еще и отформатирован вот так — одна строчка превращается в 20. И прочитать пять вложенных вызовов занимает час вместо 5 минут.
Так сложно вести беседу. Получается: "А вот исключения такие-то", а ты "но вот в криво отформатированном коде всё плохо". Отформатируй код по-другому. Делов-то..
SD>·>Так java-like unchecked исключения — оно и есть. SD>Так я за них и ратую, и оправдываю C#-стиль без checked вообще.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[8]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Почему шаблонные? Просто у них не указан вообще никакой тип. В каком-нибудь flow9 это прекрасно работает. Несмотря на отсутствие шаблонов
В Русте скорее всего в любой либе будут явно указаны все возможные ошибки.
Хотя в языке есть шаблоны. Просто шаблонные функции это зачастую лишняя нагрузка на компилятор.
flow9 — это что-то динамическое что ли? А если там автовывод то что если мы два оаза вызовем функцию передав туда разные лямбды возвращающие разные результаты?
TB>>Чтобы вернуть новый еррор для которого нет места в иерархии промежуточных функций. S>Конечно есть место.
Почему с чекед ексепшонами его нет а с резултами есть?
TB>>А нафига тогда вводили все эти Result? S>Для того, чтобы обойтись без паники там, где мы хотим обойтись без паники.
А если чисто логически по смыслу паника не нужна но мы не можем протащить новый еррор через функции левой библиотеки?
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[9]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали:
TB>flow9 — это что-то динамическое что ли? А если там автовывод то что если мы два оаза вызовем функцию передав туда разные лямбды возвращающие разные результаты?
Нет, полная статика. Но я вас обманул — шаблоны там всё-таки есть, хотя в большинстве случаев они автовыводятся.
TB>Почему с чекед ексепшонами его нет а с резултами есть?
Потому что автовывода ексепшн сигнатуры я не видел, а автовывод типа результата — видел.
В том же тайпскрипте вот такая функция:
function foo(b: boolean)
{
return b ? bar(): baz()
}
function bar(): number | Error1;
function baz(): number | Error2;
имеет тип number | Error1 | Error2.
TB>А если чисто логически по смыслу паника не нужна но мы не можем протащить новый еррор через функции левой библиотеки?
Да почему ж не можем-то?
Вот у нас "левая библиотека":
function foo<T1, T2>(b: boolean, a: ()=>T1, c: ()=>T2)
{
return b ? a(): c()
}
Если мы в неё скормим bar и baz, то результат по-прежнему будет иметь тип number | Error1 | Error2.
А если bar и bar, то number | Error1.
S>Потому что автовывода ексепшн сигнатуры я не виде
Так это вопрос не к идее проверяемых исключений а к реализации
> а автовывод типа результата — видел.
А я в Расте не видел автовывод типа ошибки за пределами библиотек совсем общих алгоритмов
TB>>А если чисто логически по смыслу паника не нужна но мы не можем протащить новый еррор через функции левой библиотеки? S>Да почему ж не можем-то?
Потому что такая библиотека и все ошибки явно прописали в сигнатуре
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[11]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали:
TB>Потому что такая библиотека и все ошибки явно прописали в сигнатуре
Значит, это плохая библиотека. Проблема с единственной в природе реализацией checked exceptions — в том, что в ней невозможно написать хорошую библиотеку*.
А языки с автовыводом типов не просто позволяют, а прямо-таки провоцируют писать хорошие библиотеки.
Внезапно оказывается, что многие библиотеки для тайпскрипта как раз работают с обобщёнными типами, и это вовсе не исчерпывается "функциональщиной", ограниченной map/reduce/filter.
Вполне себе гражданские вещи вроде тех же VSCode-библиотек или парсеров отлично работают примерно с чем угодно, и проблемы "я не смог вернуть нужный мне тип результата из лямбды" у прикладного программиста возникают крайне редко.
Это даёт мне повод надеяться на то, что данное направление стоит считать конструктивным.
* есть шанс на то, что современная Java всё-таки позволяет улучшить ситуацию; я на ней уже давно не пишу, поэтому уверенности нету. Но можно попробовать показать мне способ, который бы давал возможность написать хотя бы функцию map так, чтобы ей можно было пользоваться для checked-парсинга стрима строк в стрим интов.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[12]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, T4r4sB, Вы писали:
TB>>Потому что такая библиотека и все ошибки явно прописали в сигнатуре S>Значит, это плохая библиотека. Проблема с единственной в природе реализацией checked exceptions — в том, что в ней невозможно написать хорошую библиотеку*.
Я смотрю на Раст и там может и можно написать библиотеку выводящую типа ошиьки, но по факту никто так не делает в прикладных либах.
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[13]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали: TB>Я смотрю на Раст и там может и можно написать библиотеку выводящую типа ошиьки, но по факту никто так не делает в прикладных либах.
Может быть, потому что нельзя? Или очень сложно / контр-идиоматично?
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[14]: Result objects - все-таки победили Exceptions?
S>Может быть, потому что нельзя? Или очень сложно / контр-идиоматично?
Можно, так делают в алгоритмах. А в функции парсинга джсона в структуру — уже свой Еррор один на всю либу. А в функции открытия файла — свой еррор. А если ты хочешь передать что твоя функция может обломаться тзза отсутствия файла или кривого содержимого файла — то проще всего промапить в Box<dyn Error>, но никакого автовывода множества возможных ошибок не будет. Зато хотя бы ? поддерживает автокаст любой ошибки в trait Error
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[15]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали: TB>Можно, так делают в алгоритмах. А в функции парсинга джсона в структуру — уже свой Еррор один на всю либу. А в функции открытия файла — свой еррор. А если ты хочешь передать что твоя функция может обломаться тзза отсутствия файла или кривого содержимого файла — то проще всего промапить в Box<dyn Error>, но никакого автовывода множества возможных ошибок не будет. Зато хотя бы ? поддерживает автокаст любой ошибки в trait Error
Ну вот я и говорю — то, что в тайпскрипте работает само по себе из коробки, в расте требует каких-то унизительных приседаний, если вообще работает.
Я же показал код функции, которая смешивает типы ошибок. Там нет никаких ручных маппингов.
И её необязательно писать именно в виде single expression.
Можно писать в императивном стиле, с множественными return — и всё равно выведется общий тип.
Конкретно в тайпскрипте неудобно делать постоянные проверки, но он и не ставил перед собой цель порождать provably correct programs:
function foo()
{
let x = bar();
if(!isNumber(x)) return x;
let y = baz();
if(!isNumber(y)) return y;
return x+y;
}
Не знаю, насколько ужасно аналог такого кода будет выглядеть на Расте.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[16]: Result objects - все-таки победили Exceptions?
Я так понимаю, если промежуточная операция не удалась, то весь остальной код продолжает работать как ни в чём не бывало, и лишь операции с тем же FacilityID перестают выполняться? Звучит сомнительно
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[16]: Result objects - все-таки победили Exceptions?
TB>Я так понимаю, если промежуточная операция не удалась, то весь остальной код продолжает работать как ни в чём не бывало, и лишь операции с тем же FacilityID перестают выполняться? Звучит сомнительно
Почему сомнительно? Зачем проверять что операция не удалась на каждом ее этапе, если в принципе ничего не измениться если я это проверю только тогда, когда мне нужно определиться с ее финальным статусом? В большинстве случаев это вполне допустимо. В данном случае — ну не удалось сохранить файл, покажем юзеру мессагу он ткнет ретрай и сохраним еще раз, или отмену. Что даст осознание факта неудачи прям в тот самый момент? Даст лишь +100500 строк проверки на код ошибки если у нас сериализация здорового объекта. Такая вот крайность. Исключения — это можно сказать противоположная крайность. Истина — она всегда посередине.
Как много веселых ребят, и все делают велосипед...
Re[17]: Result objects - все-таки победили Exceptions?
Здравствуйте, ononim, Вы писали:
O>Почему сомнительно?
Потому что процесс все равно продолжается. С битыми данными, которые мы передаем в функцию не знающую ни про какой FacilityID.
Ну либо весь язык с самого начала дизайнить чтоб он требовал задавать множество FacilityID для каждой операции. Сдается мне что это будет еще и тормознуто — мы проверяем код ошибки не после функции а в начале каждой следующей функции
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[18]: Result objects - все-таки победили Exceptions?
Здравствуйте, SkyDance, Вы писали:
SD>>> {error, {right, parse_failed, Reason}} SD>>> {error, left_parse_failed, Reason} TB>>А почему по-разному написал?
SD>А потому что error-prone, блин.
SD>Исправил, сейчас написано одинаково.
Аааа, я на полном серьезе искал идиоматический подвох, вдруг так и задумано что ошибка парсинга правого и левого операндов это разные ошибки которые по-разному заворачиваются. Строго типизированная корректность же, доведенная до финала
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[17]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали:
TB>А что такое x, y для начала? Тогда я смогу описать этот код на Расте.
Ровно тот же пример, на который я ссылался в предыдущем посте. TB>В самом простом случае код такой: TB>
Здравствуйте, Sinclair, Вы писали:
S>А без явного указания типа результата никак?
Можно описать тип результата основываясь на типах аргументов.
А сказать "сам выведи по содержимому функции" — нельзя. И это очень хорошо потому что я знаю каково это когда поменял одну строчку в одной функции и из-за этого поползли изменения сигнатуры по всей кодовой базы, и иногда там где вообще не хочется, и не всегда понятно почему.
Один из принципов Раста — весь умный анализ сидит внутри функции. При этом вся инфа для межфункционального анализа зашита в явно прописанной сигнатуре функции.
S> И есть ли возможность вернуть Result<i32, BarError|BazError>?
Можно только если ты сам опишешь нужный таггед юнион и вручную пропишешь мапперы в него. С телефона не напишу
Кстати а A|B|C , (A|B)|C и A|(B|C) это одно и то же или разная фигня?
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[19]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали:
TB>Можно описать тип результата основываясь на типах аргументов. TB>А сказать "сам выведи по содержимому функции" — нельзя. И это очень хорошо потому что я знаю каково это когда поменял одну строчку в одной функции и из-за этого поползли изменения сигнатуры по всей кодовой базы, и иногда там где вообще не хочется, и не всегда понятно почему.
Не вижу проблемы, если указание типов опционально. То есть хочешь — и всё едет. Не хочешь — пишешь явно "здесь должно быть вот так", и дальше этой строчки изменения не поползут.
TB>Можно только если ты сам опишешь нужный таггед юнион и вручную пропишешь мапперы в него. С телефона не напишу
Ну, вот это и мешает писать нормально.
TB>Кстати а A|B|C , (A|B)|C и A|(B|C) это одно и то же или разная фигня?
Одно и то же. И B|(C|A) — тоже.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[2]: Result objects - все-таки победили Exceptions?
Здравствуйте, ononim, Вы писали:
S>>Но, как оказалось, народ идеи не понял. Так и вернулись к понятным дебилоидам кодам возврата, т.к. проверяемые исключения осилить не смогли. Так же и в Kotlin их решили не делать. O>Я думаю концепция GetLastError()/errno недооценена O>Но ее надо чутка расширить, параметром facility.И API сделать таким: O>
Я нифига не понял, как это предполагается использоваться.
Что будет, если код раньше поставил ошибку, а код позже решил обнулить ошибку ради проверки результата другого кода?
(см. man strtol, как это выглядит на практике. и он не один такой)
В случае исключений или объектов результатов нет проблемы с перекрытием прошлых ошибок, если нет явного (легче отловить) игнорирования ошибки. А тут как это обеспечивается?
The God is real, unless declared integer.
Re[3]: Result objects - все-таки победили Exceptions?
N>Я нифига не понял, как это предполагается использоваться. N>Что будет, если код раньше поставил ошибку, а код позже решил обнулить ошибку ради проверки результата другого кода?
Видимо разный FalicittID нужен для этого
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[21]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали:
TB>Я разрабатывал язык в котором по умолчанию любая функция шаблонная, которая по контексту определяет какие у неё параметры, сколько их и что она возвращает. Так вот это полный треш когда изменение в одной функции по цепочке приводило к тому, что компилятор что-то своё додумал в другом файле проекта и по итогу в лучшем случае ошибка компиляции, в худшем просто хрень выводится. TB>А ещё малозаметные проблемы когда компилятор из-за незначительных нюансов для двух наборов параметров навыводил разные перегрузки (ну типа в одном случае он решил передавать копию потому что объект временный, а в другом ссылку), и в бинарнике появляются две почти одинаковые функции. Раздувание бинарника на пустом места.
Да, такая штука очень плохо дружит с перегрузками. Во flow9 перегрузок нет, что ограничивает некоторые сценарии.
TB>В языке был механизм явно указывать сигнатуру, который изначально был нужен лишь для экспорта функций. Но на деле оказалось что всю бизнес-логику тоже намного проще писать явными сигнатурами, потому что тогда компилятору меньше работы, и если он что-то понял не так, то он сообщает об этом максимально близко к источнику проблемы.
Ну вот видите — это и есть примерно то, что нужно.
S>>Ну, вот это и мешает писать нормально.
TB>Как-то так. Объединять типы надо вручную TB>Проблема не нова TB>https://www.reddit.com/r/rust/comments/jwnsp4/anonymous_sum_types_for_rust_error_handling/
Да, там всё верно описано.
S>>Одно и то же. И B|(C|A) — тоже.
TB>А, то есть значок | грубо говоря в зависимости от операндов делает либо TB>- множество из двух элементов TB>- множество с добавленным элементов TB>- объединение множеств
Он всегда делает объединение множеств. Просто множество может состоять из одного элемента. TB>А там может оказаться алгебраическая сумма int64_t | long long int? Или это таки один тип?
В тайпскрипте таких типов нет, там есть просто number. Зато можно объединять что угодно.
Во flow9 примитивные типы есть, но они не могут быть частью алгебры типов. То есть объединить int и float нельзя.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[22]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Да, такая штука очень плохо дружит с перегрузками. Во flow9 перегрузок нет, что ограничивает некоторые сценарии.
Это как, любой шаблон инстанциируется только 1 раз что ли?
S>Он всегда делает объединение множеств. Просто множество может состоять из одного элемента.
Тогда получается, что элемент и множество из этого элемента — это одно и то же. Но это не так.
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[23]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали: TB>Это как, любой шаблон инстанциируется только 1 раз что ли?
Нет, просто перегрузок нет. Не получится объявить
sum(x: int, y: int) -> int
а потом
sum(x: float, y: float)->float
S>>Он всегда делает объединение множеств. Просто множество может состоять из одного элемента. TB>Тогда получается, что элемент и множество из этого элемента — это одно и то же. Но это не так.
Не обязательно. Считайте, что "отдельных" типов не существует — только множества, состоящие из одного типа.
То есть ровно везде, где вы пишете int, компилятор видит { int }.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[24]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, T4r4sB, Вы писали: TB>>Это как, любой шаблон инстанциируется только 1 раз что ли? S>Нет, просто перегрузок нет. Не получится объявить
А, ты про такие.
Я говорю про инстансы одного шаблона. Из-за того что функция шаблонная, компилятор генерит 100500 ненужных инстансов, которые по бинарному коду почти не различаются.
Ну а с неизвестными ошибками пришлось бы все функции объявлять шаблонными. Или там можно сказать "сигнатура жёстко именно такая только тип ошмбки сам выведи"?
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[25]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали:
TB>А, ты про такие. TB>Я говорю про инстансы одного шаблона. Из-за того что функция шаблонная, компилятор генерит 100500 ненужных инстансов, которые по бинарному коду почти не различаются.
Я под капот flow9 не лазил, но, думаю, там просто все шаблоны склеиваются.
Там можно не указывать типы аргументов — тогда они выведутся из использования. Но нельзя будет вывести два разных типа.
То есть вот такое вот выдаст ошибку:
Не получается типизировать аргумент foo.
Но можно сделать шаблонную функцию:
foo(a: ?)
{
Some(a);
}
Эта функция для аргумента любого типа x возвращает экземпляр типа Some<x>.
TB>Ну а с неизвестными ошибками пришлось бы все функции объявлять шаблонными. Или там можно сказать "сигнатура жёстко именно такая только тип ошмбки сам выведи"?
Во flow9 — можно. Потому что можно зафиксировать тип у аргументов функции, а результат не указывать.
Например так:
Результат тут выведется сам, это будет Some<string>.
Но там есть несколько сложностей с т.з. компилятора — в частности, я только что сломал ему типизацию, пытаясь написать для вас пример
Вот такой код приводит к рантайм фейлу (исключений там нет, так что программа сразу выпадает):
Тип результата у foo выводится в ? (а должен приводить к compile-time error), по факту в t1 возвращается строка "42", к которой потом не удаётся применить оператор +.
Но вы, наверное, имели в виду что-то другое — чтобы функция могла возвращать "int или любую ошибку", но не "int или string или ошибку".
Во flow9 такого нет (в нём вообще алгебра типов не особо могучая); а в TS — сходу не могу сказать. Я его всего полтора года изучаю, и пока что пришёл к выводу, что на любой код есть ещё более идиоматический способ записи
На первый взгляд такой возможности нет (потому что синтаксичкески в языке можно либо указать тип, либо не указать); но я не удивлюсь, если там окажется возможность как-то хитровывернуто это записать.
Ну, вот паттерн-матчинга в компиляторе TS нету, но в ts-pattern мало того, что тип аргумента сокращается по мере прохождения паттернов, так там и .exhaustive() есть.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[26]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Там можно не указывать типы аргументов — тогда они выведутся из использования. Но нельзя будет вывести два разных типа.
А, я понял. Тогда одна из проблем снимается. Но остается "почему тут вывелся не тот тип который как мне казалось тут должен появиться". Разумеется такое бывает и в Расте и в С++, но только у тех кто злоупотребляет шаблонами.
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[27]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали: TB>А, я понял. Тогда одна из проблем снимается. Но остается "почему тут вывелся не тот тип который как мне казалось тут должен появиться". Разумеется такое бывает и в Расте и в С++, но только у тех кто злоупотребляет шаблонами.
Да, такое есть. Но в С++ это всё осложнено громоздкими построениями, т.к. доступные в других языках из коробки вещи приходится конструировать из палок и верёвок. (Кстати, почти тем же грешит и TS — там особо вывернутые конструкции тоже выдают нечитаемые ошибки, если напороть в коде).
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[21]: Result objects - все-таки победили Exceptions?
Здравствуйте, T4r4sB, Вы писали: TB>Я разрабатывал язык в котором по умолчанию любая функция шаблонная, которая по контексту определяет какие у неё параметры, сколько их и что она возвращает. Так вот это полный треш когда изменение в одной функции по цепочке приводило к тому, что компилятор что-то своё додумал в другом файле проекта и по итогу в лучшем случае ошибка компиляции, в худшем просто хрень выводится. TB>А ещё малозаметные проблемы когда компилятор из-за незначительных нюансов для двух наборов параметров навыводил разные перегрузки (ну типа в одном случае он решил передавать копию потому что объект временный, а в другом ссылку), и в бинарнике появляются две почти одинаковые функции. Раздувание бинарника на пустом места. TB>В языке был механизм явно указывать сигнатуру, который изначально был нужен лишь для экспорта функций. Но на деле оказалось что всю бизнес-логику тоже намного проще писать явными сигнатурами, потому что тогда компилятору меньше работы, и если он что-то понял не так, то он сообщает об этом максимально близко к источнику проблемы.
Кстати, есть отдельный набор вопросов, не относящихся к теме топика, про ваш язык и опыт его разработки.
1. Есть ли где-то публичное описание этого языка?
2. Поверх чего он исполняется (JVM, WASM, натив)?
3. Компилятор или интерпретатор?
4. Каким тулчейном вы пользовались при его разработке? Языки программирования, библиотеки/фреймворки для парсинга/семантического разбора?
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, ononim, Вы писали:
TB>>Я так понимаю, если промежуточная операция не удалась, то весь остальной код продолжает работать как ни в чём не бывало, и лишь операции с тем же FacilityID перестают выполняться? Звучит сомнительно O>Почему сомнительно? Зачем проверять что операция не удалась на каждом ее этапе, если в принципе ничего не измениться если я это проверю только тогда, когда мне нужно определиться с ее финальным статусом? В большинстве случаев это вполне допустимо. В данном случае — ну не удалось сохранить файл, покажем юзеру мессагу он ткнет ретрай и сохраним еще раз, или отмену. Что даст осознание факта неудачи прям в тот самый момент? Даст лишь +100500 строк проверки на код ошибки если у нас сериализация здорового объекта. Такая вот крайность. Исключения — это можно сказать противоположная крайность. Истина — она всегда посередине.
Юзеру показывать классическое https://en.wikipedia.org/wiki/Abort,_Retry,_Fail%3F ? Извините, наелись ещё четверть века назад. В первую очередь исключения и обработка ошибок — это головняк программиста. А программисту нужна вся информация о проблеме, как можно больше. Поэтому чем раньше и точнее будет обнаружено место ошибки, тем лучше. Это в твоём примере 3 строчки и помедитировав, наверное, можно будет понять или случайно угадать где же оно навернулось. А в более менее реальном проекте таких строчек будет сотни, разнесённых по десяткам функций и файлов.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[5]: Result objects - все-таки победили Exceptions?
Точно! Зато как хорошо стало сейчас! Выбор из двух вариантов: "Something went wrong" (сам догадайся нажать F5) или "pending" (где-то там оно ретраит, и сколько еще будет — неизвестно).
Re: Result objects - все-таки победили Exceptions?
Здравствуйте, Shmj, Вы писали:
S>Ну вот рекомендации в новомодных языках: https://docs.flutter.dev/app-architecture/design-patterns/result
S>- все-таки топят за Result objects для бизнес-логики.
S>А ведь это всю цепочку поддерживать. Как-то много лишних букв добавляется.
S>Но! Exception как бы не определены в контрактах. Если ResultObject — это контракт, его понятно что нужно проверить и компилятор даже контролирует эту проверку. А Exception разве что в документации описан, которую могут не читать.
S>А ведь было же хорошее решение — т.н. проверяемые исключения в Java. Когда компилятор требовал проверки того или иного исключения, но так же была возможно обернуть в RuntimeException, если оно утратило смысл бизнес-логики или ожидаемого
S>Но, как оказалось, народ идеи не понял. Так и вернулись к понятным дебилоидам кодам возврата, т.к. проверяемые исключения осилить не смогли. Так же и в Kotlin их решили не делать.
Это все потому, что каждое новое поколение придумвает свои костыли для сетевого взаимодействия.
Сначала были всякие бинарные протоколы, потом майкрософт сделали DCOM/CORBA, там же появились всякие ESB и очереди сообщений, а потом распространение http(s) привело к тому, что все везде использует http(s) тулы и соотвественно коды возвратов ошибок.
И собственно те же всратые гошники попробовали бы написать модель посложнее, когда необходимо в конкретном месте разрушить контекст и отловить разрушение 5 вызовами раньше, то везде будут проверки на ошибку и возврат.
Т.е. сами по себе коды неплохи, но код — отложенная обработка ошибки, а исключение — нет.
Re[18]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>OOM-Killer — это следствие криворукой архитектуры линукса. Программы под винду пишутся на тех же С/С++, но при этом там нет (и не нужно) никакого OOM-Killer.
OOM-Killer это лучшее что можно сделать если кто-то пережрал памяти. Я очень сильно страдаю на домашней винде из-за того, что в винде его нет.
Тупая винда вводит в состояние комы всю систему нахрен вместо того, чтобы убить один процесс, это может быть браузер, в котором чисто случайно uBlock затупил на рекламном блоке, это может быть моя программа, в которой я неверно задал параметры модели.
В гугле по запросу "Windows OOM Killer" выводится "он там не нужен потому что там очень умная система выделения памяти". Тухлая лапша из говна, чтоб вешать лохам на уши. Я с завидной регулярностью ощущаю, как он якобы "не нужен".
Причём тупорылые дауны из микрософта до сих пор сука не догадались сделать сочетание Ctrl+Alt+Delete самым приоритетным, которое сразу останавливает нахрен всё и выводит диспетчер в максимально экономном режиме.
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Здравствуйте, Sinclair, Вы писали:
S>Я обычно просто нажимаю Ctrl-Shift-Esc.
Да, это сочетание я тоже знаю. Но оно не работает потому что система отчаянно трещит пытаясь запихать в своп мою модель, ролик с ютубаю, менеджер полупрозрачных окошек и прочую хрень, которая уже не нужна, ведь важнее выживание системы. Но вызов диспетчера плетется где-то там в конце общей очереди. Зачем давать диспетчеру высший приоритет, пусть пользователь помучается!
Так вот нет в винде никакого умного менеджера. Есть кучка педерастов решивших что они за пользователя лучше знают что ему нужно.
А в Линуксе я регулярно ловлю смерти по OOM. Как бы ты не вылизывал код, заказчик будет скармливать ему всё большие и большие объёмы данных, это нормально. И смерть одного процесса лучше чем смерть всей системы.
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Re[6]: Result objects - все-таки победили Exceptions?
Здравствуйте, SkyDance, Вы писали:
SD>·>Юзеру показывать классическое https://en.wikipedia.org/wiki/Abort,_Retry,_Fail%3F ? Извините, наелись ещё четверть века назад.
SD>Точно! Зато как хорошо стало сейчас! Выбор из двух вариантов: "Something went wrong" (сам догадайся нажать F5) или "pending" (где-то там оно ретраит, и сколько еще будет — неизвестно).
Сейчас это результат работы программистов, рисующих catch с пустым телом. А тут предлагается такое в дизайн яп и библиотек. Так что даже если захочешь сделать по-человечески, то вообще никак не сможешь.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[20]: Result objects - все-таки победили Exceptions?
Здравствуйте, Sinclair, Вы писали:
S>Подозреваю, что вы можете улучшить ситуацию, сняв галочку "Allow system to manage page file size".
Проверил, работает, но теперь Старкрафт не запускается
А зачем некоторые говнокодеры требуют своп даже когда оперативы хватает? Это связано с тем, что они на старте выделяют 100500 гигабайт памяти, но используют не всё? Тогда это фигня, линуксовый OOM killer срабатывает именно когда используемой памяти не хватает
А оно настраивается чтоб именно Старкрафту разрешили в своп лазать?
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Здравствуйте, T4r4sB, Вы писали: TB>Здравствуйте, Sinclair, Вы писали: TB>Проверил, работает, но теперь Старкрафт не запускается TB>А зачем некоторые говнокодеры требуют своп даже когда оперативы хватает? Это связано с тем, что они на старте выделяют 100500 гигабайт памяти, но используют не всё? Тогда это фигня, линуксовый OOM killer срабатывает именно когда используемой памяти не хватает
Откуда вы знаете, что они "используют не все"? Может быть, программа в какой-то момент собирается "использовать все", просто вы не попадаете в этот сценарий.
Во-вторых — да, бывают говнопрограммы, которые запрашивают "с запасом" без веских причин.
Но разработчиков должно интересовать не это, а то, что можно иметь некоторые гарантии от ОС. Если винда выдала мне память, то у меня есть гарантия, что при необходимости я эту память реально получу. А не привет от oom killer в самый неподходящий момент (например, когда я ещё не завершил ввод/вывод.
TB>А оно настраивается чтоб именно Старкрафту разрешили в своп лазать?
Нет. И вообще, понятие "лазать в своп" не отражает реальную картину.
Более конструктивный способ думать об этом — считать память эквивалентной диску.
См. например ответ Липперта на смежный вопрос.
Если Старкрафт перестал запускаться — значит, ему нужно больше виртуальной памяти, чем вы накрутили в настройках. Просто увеличьте размер своп-файла так, чтобы ему хватало.
Если всё ещё не хватает — значит, при запуске в "неограниченном" режиме он как раз положит вашу систему на лопатки, пытаясь откусить себе ещё больше свопа.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.