Здравствуйте, 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[3]: 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 со словами "тут иногда что-то где-то крэшится, найдите и почините". Это гораздо, гораздо дороже, чем сразу заставить программиста написать код обработки.
Абсолютно такое же ощущение/практика. Единственно, что это не только с исключениями так работает, но и с кодами возврата, и с просто некорректными результатами ака "обычно не бывает" — которые просто игнорируются, пока это возможно, если "не давать по рукам". У исключений есть даже преимущество в этом случае — они хотя бы не игнорируются по умолчанию, а вылетают выше уровнем и их легче задетектать, чем проигноренный результат от которого никаких следов может и не остаться к моменту неадекватного поведения.
Здравствуйте, hi_octane, Вы писали:
_>И вопрос 5-летней давности всё ещё актуален — кто-то пробовал замерять, сколько сейчас все эти проверки жрут времени по сравнению с кодом без проверок но с исключениями? Или среди фанатов такие вопросы поднимать моветон?
https://youtu.be/-pE3T6wsIrM?si=BoUaLvTtE0b06xG2&t=989
вкратце, если исключения не кидать, то коды возврата в два раза медленнее. Если кинуть, то можно сразу стреляться. Мало того, что выброс исключения аллоцирует память в куче, так ещё и захватывает глобальные мьютексы и на большом количестве потоков происходит существенная деградация.
Re[7]: Result objects - все-таки победили Exceptions?
Здравствуйте, 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[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[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. На нижнем уровне его генерируют, на верхнем должны теперь понимать, коль скоро на верхнем мы его теперь хотим обрабатывать. А треш и угар все это пройдет как нож через масло.
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[10]: Result objects - все-таки победили Exceptions?
Здравствуйте, 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[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[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[6]: 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[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"). Такую практику (только без экстра заворачивания) и "упросили" до значка ?, что ни разу не улучшает читаемость (и "писаемость" тоже).