За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок. Сейчас я больше склоняюсь ко второму подходу, так как исключения действительно часто становятся этаким неявным goto, кроме того использование их в многопоточном и асинхронном коде становится болью. С другой стороны, отказ от проверок кодов возврата тоже плохо. Давайте пофантазируем как можно было бы организовать идеальную обработку ошибок. Я пока придумал объеденить оба подхода через дополнительную сущность — ну пусть будет поток выполнения (ExecutionFlow), экземпляр которого имеет каждый поток. Данная сущность неявно присутствует в вызове каждой функции, хранит стек вызовов и флажок ошибки. Проще проиллюстрировать псевдокодом:
int plus2([неявно ExecutionFlow executionFlow], int x)
{
if (x < 10)
return RangeError("bla-bla"); // Выход из функции и установка ошибки executionFlow.setError(RangeError("bla-bla"))
return x + 2; // Просто выход и возврат значения, неявно оборачивается в ReturnValue<int>
}
void foo() { ... }
Вызывающий код:
var y = foo(5);
if (y.success) // проверка, также устанавливает в объекте ReturnValue<int> и ExecutionFlow флажок checked = true
int z = bar(y); // неявно приводится к int, если перед приведением не проверить success, то программа пишет стек и завершается
if (!foo().success)
print(executionFlow.stackTrace());
var error = executionFlow.error();
print(error.message())
return error
foo(); // не проверили success, флажок не установился, значит при вызове следующей функции (любой) будет стек и аварийное завершение
Как считаете имеет право на жизнь? Есть ли языки с таким подходом к обработке ошибок?
Еще дополнение — нельзя просто написать foo.success и забить, надо обязательно проверить if, за этим следит компилятор и чтобы жизнь медом не казалось if не должен быть пустым. Чтобы просто проигнорировать проверку надо вместо success вызвать что-то вроде foo().IWantToSkipThisCheck
Пока что не встречал ситуаций, когда мне не хотелось бы использовать исключения. Поэтому считаю коды возврата отвратительной идеей. Про асинхронный код:
Здравствуйте, MTD, Вы писали:
MTD>Как считаете имеет право на жизнь? Есть ли языки с таким подходом к обработке ошибок?
Зависит от окружения. Для шарпа, скажем, почти всегда сработает TryXxx + парный метод с исключениями. Все остальные способы требуют гораздо больше телодвижений и ведут к адскому лапшекоду — приходится или городить обёртки для чужого api, или поддерживать несколько стилей обработки ошибок.
Я очень сомневаюсь, что получится совместить явную проверку результатов выполнения и компактность кода. Т.е. или ищем язык с автоматом проверяемыми предусловиями, или велкам в копипасту if(error) по всему коду.
Здравствуйте, MTD, Вы писали:
MTD>За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок. Сейчас я больше склоняюсь ко второму подходу, так как исключения действительно часто становятся этаким неявным goto, кроме того использование их в многопоточном и асинхронном коде становится болью.
Это два разных подхода с совершенно различными целями и средствами. В случае кодов — это обычно ветвление бизнес-логики, в результате которого нужно продолжить сценарий по какой-то отдельной ветке. В случае реальной ошибке — это обработка через исключения, в котором необходимо корректно убить само приложение которое в неизвестно каком состоянии (с записью ошибки в лог), либо максимальная очистка контекста вызова в случае серверного приложения (с записью ошибки в лог).
Что исключения что коды ошибок не решают проблему, а порождают новые.
Если код описывает последовательность действий, при условии что всё гладко то код простой.
Если что-то пошло не так, то надо выполнять действия которые обычно вне компетенции данного кода и вне знаний вызывающего.
Поэтому любая попытка обрабатывать коды внутри приводит к увеличению сложности, а снаружи к увеличению кол-ва абстракций.
А еще код наровит выделять разные ресурсы, которые кто-то должен освобождать и останавливать (если дочерние потоки или всякие подписки).
Соответственно языку следовало бы иметь неявно ExecutionFlow, который определял бы определял политику реакции на не штатные ситуации. И через него же выделять ресурсы, что память что дочерние потоки исполнения. И операторы которые явно задают как реагировать на нештатное поведение. И политику и мониторинг выделения ресурсов туда же засунуть, например выполнять не более 100мс (или даже в тактах) и памяти не более 10Мб и не более 4 потоков.
value=method1(args) or method2(args);
action1(); // если код возврата никем не обрабатывается то компилятор автоматически делает примерно такое
// { var res=action1(); if (res && executionFlow.NoErrorAllowed && !executionFlow.DisableThisError(here)) return executionFlow.ReturnError(here,res,"action1" ... ); }
action2() or action2b();
action3() or ignore;
action4()
MTD>За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок. Сейчас я больше склоняюсь ко второму подходу, так как исключения действительно часто становятся этаким неявным goto, кроме того использование их в многопоточном и асинхронном коде становится болью. С другой стороны, отказ от проверок кодов возврата тоже плохо. Давайте пофантазируем как можно было бы организовать идеальную обработку ошибок. Я пока придумал объединить оба подхода через дополнительную сущность — ну пусть будет поток выполнения (ExecutionFlow), экземпляр которого имеет каждый поток. Данная сущность неявно присутствует в вызове каждой функции, хранит стек вызовов и флажок ошибки. Проще проиллюстрировать псевдокодом:
MTD>Как считаете имеет право на жизнь? Есть ли языки с таким подходом к обработке ошибок?
Право на жизнь имеет любой подход. Erlang знатно обрабатывает ошибки.
Здравствуйте, MTD, Вы писали:
MTD>Давайте пофантазируем как можно было бы организовать идеальную обработку ошибок.
Всё давно придумано. В Rust, насколько мне известно, нет исключений. В языках с поддержкой алгебраических типов данных это выражается типами-суммами Option<T> (aka Maybe) и Result<T, E>. В обычных языках это можно эмулировать а-ля «коды ошибок на стероидах». Возвращаемые данные нужно явно unwrap'ить (ну или лучше использовать монадический bind) — в любом случае нельзя «пропустить» проверку. В случае ошибки её контекст тоже сохраняется, то есть это не просто целочисленный код. Механизм исключений в языке при этом в каком-то виде может быть (раскрутка стека), просто они неперехватываемые (panic).
Здравствуйте, vsb, Вы писали:
vsb>Почему это не асинхронный? something() возвращает асинхронный результат.
Потому что ты синхронно дожидаешься результата. Если бы ты продолжил выполнение другого кода с неблокирующимися вызовами, то да код стал бы асинхронным и внезапно стало бы непонятно, как работать с исключениями.
Здравствуйте, MTD, Вы писали:
vsb>>Почему это не асинхронный? something() возвращает асинхронный результат.
MTD>Потому что ты синхронно дожидаешься результата.
Ничего я не дожидаюсь.
> Если бы ты продолжил выполнение другого кода с неблокирующимися вызовами, то да код стал бы асинхронным и внезапно стало бы непонятно, как работать с исключениями.
Это асинхронный код с неблокирующимся вызовом. Он отдаёт управление сразу по достижении слова await.
Здравствуйте, MTD, Вы писали:
MTD>Ты что-то путаешь — await означает ждать и так он себя в известных мне языках и ведет, кстати, сейчас мы про какой язык говорим?
await — это генерация компилятором конечного автомата с выходом из метода и освобождения потока и входом при окончании исполнения асинхронного метода.
Здравствуйте, GlebZ, Вы писали:
GZ>await — это генерация компилятором конечного автомата с выходом из метода и освобождения потока и входом при окончании исполнения асинхронного метода.
Да понятно, в приведенном коде точка синхронизации — получение х, пока его не получим на следующую строчку не попадем. Я не прав?
GZ>>await — это генерация компилятором конечного автомата с выходом из метода и освобождения потока и входом при окончании исполнения асинхронного метода.
MTD>Да понятно, в приведенном коде точка синхронизации — получение х, пока его не получим на следующую строчку не попадем. Я не прав?
Безусловно. Остаток процедуры будет запущен после выполнения асинхронно функции и при наличии кванта времени в диспетчере. Поэтому это действительно асинхронное неблокирующеся исполнение.
Здравствуйте, GlebZ, Вы писали:
GZ>Остаток процедуры будет запущен после выполнения асинхронно функции
Вот это и есть синхронный код, да он притворяется асинхронным, но сущность его именно такова. В настоящем асинхронном коде таких явных точек синхронизации нет, после выполнения операции вызывается обработчик — это позволяет лучше нагрузить процессор не тратя время на ожидание.
Здравствуйте, MTD, Вы писали:
GZ>>Остаток процедуры будет запущен после выполнения асинхронно функции
MTD>Вот это и есть синхронный код, да он притворяется асинхронным, но сущность его именно такова.
Всё наоборот. Это асинхронный код, который притворяется синхронным.
> В настоящем асинхронном коде таких явных точек синхронизации нет, после выполнения операции вызывается обработчик — это позволяет лучше нагрузить процессор не тратя время на ожидание.
Это не точка синхронизации. Это указание кода, который будет выполнен при получении результата. С нагрузкой процессора тут проблем нет, поток будет выполнять следующий обработчик.
Собственно поинт в том, что если в языке есть удобные средства для работы с асинхронным кодом (в JavaScript, Kotlin, вроде в C# тоже такое есть), то исключения прекрасно подходят для работы с ошибками. Если таких средств нет, с асинхронным кодом работать очень неудобно в любом случае.
Здравствуйте, MTD, Вы писали:
MTD>За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок. Сейчас я больше склоняюсь ко второму подходу, так как исключения действительно часто становятся этаким неявным goto
Вас же, надеюсь, никто заставляет использовать исключения в качестве неявного goto?
MTD>кроме того использование их в многопоточном и асинхронном коде становится болью.
Хз, у нас вроде не становится. В чем боль? Task'и, PLinq, класс Parallel — оборачивают исключение в AggregateException и пробрасывают его в вызывающий код.
Может не надо ничего фантазировать, а надо просто научиться пользоваться исключениями и многопоточностью?
Здравствуйте, MTD, Вы писали:
MTD>За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок. Сейчас я больше склоняюсь ко второму подходу, так как исключения действительно часто становятся этаким неявным goto, кроме того использование их в многопоточном и асинхронном коде становится болью. С другой стороны, отказ от проверок кодов возврата тоже плохо. Давайте пофантазируем как можно было бы организовать идеальную обработку ошибок. Я пока придумал объеденить оба подхода через дополнительную сущность — ну пусть будет поток выполнения (ExecutionFlow), экземпляр которого имеет каждый поток. Данная сущность неявно присутствует в вызове каждой функции, хранит стек вызовов и флажок ошибки. Проще проиллюстрировать псевдокодом:
MTD>Как считаете имеет право на жизнь? Есть ли языки с таким подходом к обработке ошибок?
Поздравляю! Вы изобрели монаду!
Типа Error Monad.
Языки в которых такое есть: Haskell
F#
OCaml
Idris
Насчёт первых двух уверен на 100%, насчёт остальных — поменьше, поскольку не работаю с ними.
Наверняка есть и другие языки с поддержкой монад вообще и монады ошибок в частности.
Здравствуйте, vsb, Вы писали:
GZ>>>Остаток процедуры будет запущен после выполнения асинхронно функции MTD>>Вот это и есть синхронный код, да он притворяется асинхронным, но сущность его именно такова. vsb>Всё наоборот. Это асинхронный код, который притворяется синхронным.
Семантически этот код — именно синхронный. То, что компилятор где-то внутри генерит — это просто деталь реализации и не более. Если я возьму другой компилятор, который будет создавать синхронный код в этом месте, то не изменится НИЧЕГО — ни код, ни поведение программы.
Здравствуйте, MozgC, Вы писали:
MC>Может не надо ничего фантазировать, а надо просто научиться пользоваться исключениями и многопоточностью?
Исключения — плохая вещь, безотносительно того, умеешь ей пользоваться или нет.
Ну то есть как плохая, просто отличить плохой код с исключениями от хорошего кода с исключениями — сложно. Рассуждать о программах с исключениями — сложно.
Здравствуйте, vsb, Вы писали:
vsb>любой цикл или условный оператор это "неявный goto".
Цикл и иф имеют точное место продолжения выполнения. Исключения и гото — мы не знаем, куда летим. Поэтому циклы и ифы — структурные операторы, а исключения и гото — нет.
Здравствуйте, MTD, Вы писали:
MTD>Давайте пофантазируем как можно было бы организовать идеальную обработку ошибок. Я пока придумал объеденить оба подхода через дополнительную сущность — ну пусть будет поток выполнения (ExecutionFlow), экземпляр которого имеет каждый поток. Данная сущность неявно присутствует в вызове каждой функции, хранит стек вызовов и флажок ошибки.
Обработке ошибок присуща природная сложность и не очень понятно, как можно было бы серьезно упростить жизнь программиста. Сейчас лично я думаю так. Часть ошибок, которые сейчас являются рантайм-ошибками, должна перейти на стадию компиляции. Пример — NRE. Часть ошибок должна стать фатальными и не обрабатываться вообще (stack overflow, недостаток памяти). Часть ошибок можно отсечь частичными функциями — не исключение при вызове "log(-1)", а ошибка компиляции. Оставшиеся обрабатывать а-ля Rust — возвращать Result<Type> вместо Type.
MTD>Как считаете имеет право на жизнь? Есть ли языки с таким подходом к обработке ошибок?
Как справедливо заметили выше, это похоже на error monad. Так что да, где-то используется.
Здравствуйте, AlexRK, Вы писали:
ARK>Здравствуйте, vsb, Вы писали:
GZ>>>>Остаток процедуры будет запущен после выполнения асинхронно функции MTD>>>Вот это и есть синхронный код, да он притворяется асинхронным, но сущность его именно такова. vsb>>Всё наоборот. Это асинхронный код, который притворяется синхронным.
ARK>Семантически этот код — именно синхронный. То, что компилятор где-то внутри генерит — это просто деталь реализации и не более. Если я возьму другой компилятор, который будет создавать синхронный код в этом месте, то не изменится НИЧЕГО — ни код, ни поведение программы.
Нет, это не так. Синхронный код заблочит поток и будет ничего не делать и ждать, ждать, ждать и так пока не закончится вызов something(), после чего перейдёт к следующей строке. Между something() и следующей строкой не вклинится другой код. А асинхронный код вернёт управление сразу, сразу начнёт выполняться какой-то там другой код, а то, что после вызова something(), выполнится отдельно потом.
CD>Поздравляю! Вы изобрели монаду! CD>Типа Error Monad.
CD>Языки в которых такое есть: CD> CD> Haskell CD> F# CD> OCaml CD> Idris CD>
CD>Насчёт первых двух уверен на 100%, насчёт остальных — поменьше, поскольку не работаю с ними. CD>Наверняка есть и другие языки с поддержкой монад вообще и монады ошибок в частности.
А в этих самых монадах есть неявное преобразование к инту, как в задумке в первом посте?
Здравствуйте, Terix, Вы писали:
ARK>>Семантически этот код — именно синхронный. То, что компилятор где-то внутри генерит — это просто деталь реализации и не более. Если я возьму другой компилятор, который будет создавать синхронный код в этом месте, то не изменится НИЧЕГО — ни код, ни поведение программы.
T>Нет, это не так. Синхронный код заблочит поток и будет ничего не делать и ждать, ждать, ждать и так пока не закончится вызов something(), после чего перейдёт к следующей строке. Между something() и следующей строкой не вклинится другой код. А асинхронный код вернёт управление сразу, сразу начнёт выполняться какой-то там другой код, а то, что после вызова something(), выполнится отдельно потом.
Это все я знаю. Я говорю про семантику кода, а не про детали конкретной реализации. Блин, вроде всё в предвдущем посте расписал — всё равно люди не понимают.
Вот этот код — "a = f() + 3;" — синхронный или асинхронный? (Важное уточнение для прибитых к контексту людей: это не C#.)
Семантически — синхронный, то есть выглядит и взаимодействует с другим кодом как синхронный.
Реализован как синхронный или асинхронный? Может быть и так, и так. На текущем уровне абстракции нам это пофигу.
ARK>Это все я знаю. Я говорю про семантику кода, а не про детали конкретной реализации. Блин, вроде всё в предвдущем посте расписал — всё равно люди не понимают.
В предыдущем посте написано, что поведение программы не будет меняться в зависимости от того, какой код сгенерировал компилятор — синхронный, или асинхронный. А оно будет меняться, потому что в случае асинхронного кода, что-то может вклиниться между something и следующей строкой, а в случае синхронного кода — не может.
Здравствуйте, Terix, Вы писали:
ARK>>Это все я знаю. Я говорю про семантику кода, а не про детали конкретной реализации. Блин, вроде всё в предвдущем посте расписал — всё равно люди не понимают.
T>В предыдущем посте написано, что поведение программы не будет меняться в зависимости от того, какой код сгенерировал компилятор — синхронный, или асинхронный. А оно будет меняться, потому что в случае асинхронного кода, что-то может вклиниться между something и следующей строкой, а в случае синхронного кода — не может.
Да, согласен, упустил этот момент. Если говорить только о C#.
MTD>Как считаете имеет право на жизнь? Есть ли языки с таким подходом к обработке ошибок?
Обязательные проверки с одной стороны — добро, по очевидным причинам. С другой стороны нередко они просто мешают и захламляют код и тратят время и нервы.
Тут ниже уже отписались по этому поводу, но те не менее есть такой вариант.
Соответственно функция принимает один Option и возвращает другой. Таких операций можно сделать много, но в результате у нас будет какой-то Option.
У Option есть метод get, который кинет эксепшн, если какая-то из операций прошла плохо.
Если мы пробуем провести операцию на уже побитом Option, она проводится не будет, а нам молча вернут ещё один дефектный Option.
Преимуществом тут является, что не нужно мучаться с эксепшнами и то, что не надо проверять ошибки на каждый чих, а проверить только там, где нужно.
Недостатком то, что примитивный тип приходится оборачивать и код получается непривычным
Здравствуйте, Terix, Вы писали:
T>Если мы пробуем провести операцию на уже побитом Option, она проводится не будет, а нам молча вернут ещё один дефектный Option. T>Преимуществом тут является, что не нужно мучаться с эксепшнами и то, что не надо проверять ошибки на каждый чих, а проверить только там, где нужно.
теряется информация об ошибке и месте/условиях ее появления.
Здравствуйте, night beast, Вы писали:
NB>Здравствуйте, Terix, Вы писали:
T>>Если мы пробуем провести операцию на уже побитом Option, она проводится не будет, а нам молча вернут ещё один дефектный Option. T>>Преимуществом тут является, что не нужно мучаться с эксепшнами и то, что не надо проверять ошибки на каждый чих, а проверить только там, где нужно.
NB>теряется информация об ошибке и месте/условиях ее появления.
Сейчас в мейнстримных языках, в том месте, где произойдёт ошибка, будет сгенерирован эксепшн. Метод map его поймает и сохранит для потомков. В эксепшне будет написано где, что и как. Эксепшн BadOptionException обернёт этот эксепшн и подробности можно будет получить.
Здравствуйте, Terix, Вы писали:
T>>>Если мы пробуем провести операцию на уже побитом Option, она проводится не будет, а нам молча вернут ещё один дефектный Option. T>>>Преимуществом тут является, что не нужно мучаться с эксепшнами и то, что не надо проверять ошибки на каждый чих, а проверить только там, где нужно.
NB>>теряется информация об ошибке и месте/условиях ее появления.
T>Сейчас в мейнстримных языках, в том месте, где произойдёт ошибка, будет сгенерирован эксепшн. Метод map его поймает и сохранит для потомков. В эксепшне будет написано где, что и как. Эксепшн BadOptionException обернёт этот эксепшн и подробности можно будет получить.
допустим.
как решаются проблемы с нарушением инвариантов объекта при исключении?
Здравствуйте, night beast, Вы писали:
NB>Здравствуйте, Terix, Вы писали:
T>>>>Если мы пробуем провести операцию на уже побитом Option, она проводится не будет, а нам молча вернут ещё один дефектный Option. T>>>>Преимуществом тут является, что не нужно мучаться с эксепшнами и то, что не надо проверять ошибки на каждый чих, а проверить только там, где нужно.
NB>>>теряется информация об ошибке и месте/условиях ее появления.
T>>Сейчас в мейнстримных языках, в том месте, где произойдёт ошибка, будет сгенерирован эксепшн. Метод map его поймает и сохранит для потомков. В эксепшне будет написано где, что и как. Эксепшн BadOptionException обернёт этот эксепшн и подробности можно будет получить.
NB>допустим. NB>как решаются проблемы с нарушением инвариантов объекта при исключении?
Да, пожалуй, что никак. Надо руками, как и в случаях с исключениями.
Здравствуйте, Terix, Вы писали:
NB>>допустим. NB>>как решаются проблемы с нарушением инвариантов объекта при исключении?
T>Да, пожалуй, что никак. Надо руками, как и в случаях с исключениями.
ну с исключениями хотя бы понятно где бахнуло, а при Optional нарваться на последствия можно в совершенно не связанном с этим Optional месте.
Здравствуйте, Terix, Вы писали:
NB>>ну с исключениями хотя бы понятно где бахнуло, а при Optional нарваться на последствия можно в совершенно не связанном с этим Optional месте.
Ты имеешь в виду такой случай? T>
T>Option<int> a = plus(a); //тут ошибка
T>Option<string> b = getWeirdString(a); //Внутри getWeirdString мы делаем get у Option и ловим эксепшн
нет. что-то вроде такого:
Option<int> a = x.foo(); //тут ошибка
...
x.bar(); //пытаемся работать с кривым x
Здравствуйте, night beast, Вы писали:
NB>Здравствуйте, Terix, Вы писали:
NB>>>ну с исключениями хотя бы понятно где бахнуло, а при Optional нарваться на последствия можно в совершенно не связанном с этим Optional месте.
NB>Ты имеешь в виду такой случай? T>>
T>>Option<int> a = plus(a); //тут ошибка
T>>Option<string> b = getWeirdString(a); //Внутри getWeirdString мы делаем get у Option и ловим эксепшн
NB>
NB>нет. что-то вроде такого: NB>
NB>Option<int> a = x.foo(); //тут ошибка
NB>...
NB>x.bar(); //пытаемся работать с кривым x
NB>
Это ведь то же самое, что
int a = -1;
try {
a = x.foo();
} catch(Exception e) {
Log.Info("нехорошо вышло, да"); //хрень какая-то с a вышла, но это нас не очень волнует
}
x.bar(); //а обработать проблему с кривым иксом в блоке catch мы забыли
Здравствуйте, Terix, Вы писали:
T>Это ведь то же самое, что T>
T>int a = -1;
T>try {
T> a = x.foo();
T>} catch(Exception e) {
T> Log.Info("нехорошо вышло, да"); //хрень какая-то с a вышла, но это нас не очень волнует
T>}
T>x.bar(); //а обработать проблему с кривым иксом в блоке catch мы забыли
T>
да, но с исключениями это приходится писать явно.
или Optional это просто дополнение к исключениям? тогда нормально.
почему-то решил что как альтернатива предлагается.
Здравствуйте, MTD, Вы писали:
MTD>За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок.
Тут всё разобрано по косточкам.
Много букв, но ничего лучше про обработку ошибок я не видел. http://joeduffyblog.com/2016/02/07/the-error-model/
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, night beast, Вы писали:
NB>теряется информация об ошибке и месте/условиях ее появления.
`Option<T>` (сумма `Some<T>` и `None`) просто «частный случай» типа `Either<L, R>` или `Result<T, E>` (сумма `Some<T>` и `Error<E>`). `Option<T>` используется, когда причина ошибки неважна или единственна (например, ключ в словаре не найден). Если вариантов ошибки может быть больше, то можно возвращать `Result<T, E>`, и класть туда `Error<E>`, где `E` — информация об ошибках, туда можно сохранить контекст возникновения (фаза луны, состояние программы, etc.).
Обычно это удобно в языках с паттерн-матчингом или монадами в стандартной библиотеке. Если адаптировать этот подход на какой-нибудь ООП-язык типа C#, то в `Result<T, E>` ты просто кладёшь наследника от `Exception`, например, который замыкает в себе информацию об условиях появления ошибки.
Здравствуйте, Terix, Вы писали:
CD>>Поздравляю! Вы изобрели монаду! CD>>Типа Error Monad.
CD>>Языки в которых такое есть: CD>> CD>> Haskell CD>> F# CD>> OCaml CD>> Idris CD>>
CD>>Насчёт первых двух уверен на 100%, насчёт остальных — поменьше, поскольку не работаю с ними. CD>>Наверняка есть и другие языки с поддержкой монад вообще и монады ошибок в частности.
T>А в этих самых монадах есть неявное преобразование к инту, как в задумке в первом посте?
Господи Боже, ну конечно же нет! Вы, должно быть, сбрендили — неявные преобразования! Скажете тоже...
Здравствуйте, MTD, Вы писали:
MTD>За время моей трудовой деятельности я участвовал как в проектах использующих исключения, так и в проектах использующих коды ошибок. Сейчас я больше склоняюсь ко второму подходу, так как исключения действительно часто становятся этаким неявным goto, кроме того использование их в многопоточном и асинхронном коде становится болью. С другой стороны, отказ от проверок кодов возврата тоже плохо. Давайте пофантазируем как можно было бы организовать идеальную обработку ошибок. Я пока придумал объеденить оба подхода через дополнительную сущность — ну пусть будет поток выполнения (ExecutionFlow), экземпляр которого имеет каждый поток. Данная сущность неявно присутствует в вызове каждой функции, хранит стек вызовов и флажок ошибки. Проще проиллюстрировать псевдокодом:
MTD>
MTD>int plus2([неявно ExecutionFlow executionFlow], int x)
MTD>{
MTD> if (x < 10)
MTD> return RangeError("bla-bla"); // Выход из функции и установка ошибки executionFlow.setError(RangeError("bla-bla"))
MTD> return x + 2; // Просто выход и возврат значения, неявно оборачивается в ReturnValue<int>
MTD>}
MTD>void foo() { ... }
MTD>
MTD>Вызывающий код:
MTD>
MTD>var y = foo(5);
MTD>if (y.success) // проверка, также устанавливает в объекте ReturnValue<int> и ExecutionFlow флажок checked = true
MTD> int z = bar(y); // неявно приводится к int, если перед приведением не проверить success, то программа пишет стек и завершается
MTD>if (!foo().success)
MTD> print(executionFlow.stackTrace());
MTD> var error = executionFlow.error();
MTD> print(error.message())
MTD> return error
MTD>foo(); // не проверили success, флажок не установился, значит при вызове следующей функции (любой) будет стек и аварийное завершение
MTD>
MTD>Как считаете имеет право на жизнь? Есть ли языки с таким подходом к обработке ошибок?
Имеет, конечно, но проблемно из-за того, что заставляет программиста проверять ошибки в рантайме, а надо бы свалить это на компилятор.
Either<Error, Int> plus2([неявно ExecutionFlow executionFlow], int x)
{
if (x < 10)
return Left(RangeError("bla-bla"));
return Right(x + 2);
}
int z = plus2(4).getOrElse { throw WtfException } //если произошла любая ошибка, бросаем исключение. Фигурные скобки - это лямбда.
int z = plus2(4) match { // если RangeError, бросаем исключение, если другая ошибка - возвращаем -1
case Right(result) => result
case Left(RangeError(msg)) => throw WtfException("Error: " + msg)
case Left(otherError) => -1
}
Either<Error, Int> result = plus2(4)
if (result.isLeft) {
// ошибка
}
return result.get() // бросит исключение, если ошибка
Здравствуйте, Terix, Вы писали:
T>Ну, значит, монады не переизобретены, а улучшены
Не улучшены, а ухудшены (протечка абстракции).
Фокус монады состоит в том, что вся арифметика выполняется внутри монады, — там просто не нужны преобразования.
Здравствуйте, night beast, Вы писали:
NB>да, но с исключениями это приходится писать явно. NB>или Optional это просто дополнение к исключениям? тогда нормально. NB>почему-то решил что как альтернатива предлагается.
Если использовать Option как монаду, то — либо bar(string) просто не получит управление, либо bar(Option<string>) получит завёрнутые данные с кодом возврата, потому что оно знает, что с этим делать.
Обрати внимание на сигнатуры.
Можно сказать, что это и есть "писать явно".
Здравствуйте, Qbit86, Вы писали:
Q>Механизм исключений в языке при этом в каком-то виде может быть (раскрутка стека), просто они неперехватываемые (panic).
Можно примеры языков, где панику невозможно перехватить?
Здравствуйте, DarkEld3r, Вы писали:
Q>>Механизм исключений в языке при этом в каком-то виде может быть (раскрутка стека), просто они неперехватываемые (panic).
DE>Можно примеры языков, где панику невозможно перехватить?
Ну и чтобы два раза не вставать: в Rust так же есть возможность перебросить пойманную панику и проверить тип. То есть, возможности более-менее соответствуют исключениям, разве что пользоваться этим менее удобно, ну и "не принято".
It is not recommended to use this function for a general try/catch mechanism. The `Result` type is more appropriate to use for functions that can fail on a regular basis. Additionally, this function is not guaranteed to catch all panics, see the "Notes" section below.
Здравствуйте, Qbit86, Вы писали:
Q>Здравствуйте, DarkEld3r, Вы писали:
DE>>есть std::panic::catch_unwind.
Q>
It is not recommended to use this function for a general try/catch mechanism. The `Result` type is more appropriate to use for functions that can fail on a regular basis. Additionally, this function is not guaranteed to catch all panics, see the "Notes" section below.
Ну а я что написал выше? Не рекомендуется, да. Но использовать-то можно.
Если смущает примечание, то оно вообще о другом. В плюсах такое, кстати, тоже есть, только не в стандарте, но популярные компиляторы реализуют что-то типа -fno-exceptions. И если мы пишем приложение, а не библиотеку, то стратегию обработки паники можем контролировать.
В любом случае, Rust не входит в список языков, где невозможно перехватить панику. И мне всё ещё интересно есть ли такие языки.
Здравствуйте, vsb, Вы писали:
MTD>>Вот это и есть синхронный код, да он притворяется асинхронным, но сущность его именно такова. vsb>Всё наоборот. Это асинхронный код, который притворяется синхронным.
И опять неверно. ))
Это кооперативная многозадачность вместо вытесняющей.
Приведенный код синхронный, разумеется, ничего асинхронного в нём нет.
>> В настоящем асинхронном коде таких явных точек синхронизации нет
Вот еще. А если я использую библиотеку кооперативной многозадачности? Например, как в Windows 3.x при вызове любой операции ввода-вывода? Там абсолютно то же самое происходило, только не надо было явно указывать await. Т.е. весь ввод-вывод был синхронный для логического потока исполнения, но асинхронный в "масштабе" компьютера.
vsb>после выполнения операции вызывается обработчик — это позволяет лучше нагрузить процессор не тратя время на ожидание.
Процессор и не тратит время на ожидание.
Вся дикость вот этого сниппета const x = await something() имеет корнями высокую цену ядерных переключений потоков в СОВРЕМЕННЫХ процах с защищённой памятью и ничего более. Если бы ядро ОС переключало потоки с достаточной эффективностью, то можно было бы ожидать выполнение операции something ср-вами самой ОС (через хендл асинхронной операции). Но такой код ведь зовут синхронным, верно?
vsb>Это не точка синхронизации. Это указание кода, который будет выполнен при получении результата.
Это и есть точка синхронизации задач прямо по-определению.
Дотнетный Task — это всего-навсего юзер-спейсный аналог процесса (потока) и ничего более.
vsb>Собственно поинт в том, что если в языке есть удобные средства для работы с асинхронным кодом
Нету. Приведенный код — не асинхронный.
Прибит гвоздями к объекту Task, который сам по себе достаточно тяжеловесен.
А необходимость объявлять окружающий этот сниппет метод как async — так вообще дичь и ха-ха 3 раза. ))
Для сравнения, ни одна нейтивная библиотека кооперативной многозадачности этого не требует.
Кароч, это всё нубство на ровном месте. Костыль на безрыбье, чтобы раком не встать.
vsb>(в JavaScript, Kotlin, вроде в C# тоже такое есть), то исключения прекрасно подходят для работы с ошибками. Если таких средств нет, с асинхронным кодом работать очень неудобно в любом случае.
В дотнете и так неудобно с асинхронным кодом работать. Например, ты не можешь написать свой независимый механизм кооперативной диспетчеризации и использовать его аналогично ср-вами языка. Ну вот не нравятся мне тяжеловесные и тупые Task, у меня есть своя легковесная lock-free реализация Promise/Future, которая на порядки удобней (потому что сам этот подход хорошо проработан во многих языках). Вот как мне использовать продолжения от моих Future в аналогичном синтаксисе await, ы? )) Аж никак. Только явно описывать продолжения на анонимных ф-иях, что есть дичь. Но зато это будет тот самый настоящий асинхронный код, со всеми его "плюшками" (в плохом смысле этого слова).
Здравствуйте, Terix, Вы писали:
T>В предыдущем посте написано, что поведение программы не будет меняться в зависимости от того, какой код сгенерировал компилятор — синхронный, или асинхронный. А оно будет меняться
Не будет. Если на каждый дотнетный Task создавать по системному потоку и в месте указания оператора await ожидать синхронно завершения операции, то поведение будет полностью идентичным. Разница будет лишь в эффективности.
T>потому что в случае асинхронного кода, что-то может вклиниться между something и следующей строкой, а в случае синхронного кода — не может.
В случае синхронного "вклиниться" может другой поток, исполняющий другой Task. Как и сейчас, собсно.
отдельная операция
для списка файлов
отрываем файл
пишем данные
закрываем
1. ошибка: не удаётся записать файл (нет прав, файл уже есть, временно нет связи, кончилась квота, ...)
1.1. остановить обработку списка и кинуть исключение
1.2. продолжить обработку списка, отложив эту ошибку в список проблем.
1.3. удалить то что уже сделано и вернуть ошибку операция не выполнена
1.4. другой вариант
2. ошибка: Операция для отдельного файла затянулась, (вместо секунд для таких же файлов, на часы)
2.1. не обращать внимания на зависания.
2.2. контролировать время обработки списка и диагностировать отклонения скорости.
2.3. добавить timeout. в случае превышение отложить в конец списка.
2.4. другое
3. отложенная ошибка: файл успешно записан в кэш системы, но не на диск. а вот при записи на диск записало с ошибками или не записало вообще.
3.1. забить
3.2. мониторить системные события
3.3. добавить процедуру проверки и дообработки списка
3.4. другое
4. событие: внешние прерывание (отмена операции)
4.1. прервать по завершению обработки последнего файла
4.2. прервать немедленно и удалить последний файл
4.3. прервать немедленно и удалить то что уже обработали до этого
4.4. не обращать внимания
4.5. другое
5. событие: исключение в коде (что пошло не так)
5.1. всё прервать и выкинуть дальше
5.2. добавить в список проблем и попробовать остальные файлы в списке
5.3. попробовать заново с последнего файла
5.4. другое
Какой из современных языков позволяет обрабатывать подобные ситуации не усложняя исходный код лишними языковыми конструкциями.
В идеале как в CSS указываем слекторами где и правилами меняем поведение по умолчанию на нужное.
Здравствуйте, Jack128, Вы писали:
V>>Вот как мне использовать продолжения от моих Future в аналогичном синтаксисе await, ы? )) Аж никак. J>Вообще то можно. Ключевые слова: AsyncMethodBuilder, Awaiter pattern
Здравствуйте, DarkEld3r, Вы писали:
DE>В любом случае, Rust не входит в список языков, где невозможно перехватить панику. И мне всё ещё интересно есть ли такие языки.
Тот на котором мидори написана. http://joeduffyblog.com/2016/02/07/the-error-model/
Our overall solution was to offer a two-pronged error model. On one hand, you had fail-fast – we called it abandonment – for programming bugs. And on the other hand, you had statically checked exceptions for recoverable errors. The two were very different, both in programming model and the mechanics behind them. Abandonment unapologetically tore down the entire process in an instant, refusing to run any user code while doing so. (Remember, a typical Midori program had many small, lightweight processes.) Exceptions, of course, facilitated recovery, but had deep type system support to aid checking and verification.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн