LINUX.ORG.RU

Просто посоветуйте, как делать обработку ошибок

 , , ,


0

3

Доброго времени суток!

Начинаю попытки создать для Яра спецификацию на то, что называется «исключениями». Вводные таковы:

1. Под капотом Common Lisp с его сигнальным протоколом.И библиотеки, написанные на CL. Поэтому придётся как-то уважать возможности CL.

2. Мне нравится та идея, что исключения являются частью сигнатуры функции. В связи с этим хотелось бы идей на тему того, как реанимировать «checked exceptions». Считается, что программы нужно писать слоями. А исключения проходят сквозь слои. Поэтому исключения нужно либо переупаковывать (что мне не кажется хорошей идеей), либо нарушается модульность. Есть ли какие-то ещё идеи на эту тему? В лиспе типы являются first-class сущностями, кроме того есть тип (А или Б или С). Ничто в принципе не мешает в библиотеке завести некий тип «все-мои-исключения», который будет одним из параметров процесса компоновки. В этом случае прикладной код может идентифицировать типы исключений, полученных от библиотеки, при этом не завися от этих типов. Что думаете на тему того, чтобы вот таким образом реализовать checked исключения без перупаковывания?

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

определить модуль ру.розинмн.либа1/работа-с-файлами
  :пакет 
    пакет
      экспорт (
        Все-мои-исключения, //тип
        Вернуть-все-мои-исключения //реализация try .. catch
        )
    кнп
кном

/* функция, реализующая try .. catch. Функция вызывает свой
аргумент "что-делать-с-файлами" и перехватывает все исключения
файловой системы. Если они есть, то возвращает их, иначе 
возвращает то, что вернула ф-я "что-делать-с-файлами" */
функция Вернуть-все-мои-исключения(
  что-делать-с-файлами -- (: функция() -- тип-любой-кортеж :)
  ) -- (: тип-кортеж(возможно-исключение -- 
                     ?Все-мои-исключения, 
                    возврат -- тип-любой-кортеж) :)


// Клиент выглядит так:
функция дело()
  Вернуть-все-мои-исключения(
    лямбда
     С-открытым-файлом
       чёто 
       делать  
     кнС
    кнл
  )
кнф

3. Есть проблема «исключение в деструкторе». Для лиспа она не так актуальна, но я хочу в языке конструкции, подобные RAII. Проблема «исключение при отпускании ресурса», как мне кажется, является некоей разновидностью проблемы «исключение в деструкторе». Хотелось бы комментариев прежде всего от лисперов: значима ли эта проблема в лиспе? Если да, то как она правильно решается.

4. Хоть я уже неоднократно подходил к расту и го, я всё же не могу осилить, чем сделанное там отличается по сути от исключений в Яве и С++ и какой в этом смысл. Просветите.

5. А лучше всего, если кто-нибудь возьмётся написать спецификацию, мы её вместе поругаем, поправим и доведём до совершенства.

★★★★★

Последнее исправление: den73 (всего исправлений: 3)

просто выучи яву и прекрати мучения с яром - лучше у тебя все равно не получается, а common lisp не стоит так уродовать.

anonymous
()

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

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

В CL handler-bind + restart-case может не раскручивать стек, поэтому всё менее критично.

я всё же не могу осилить, чем сделанное там отличается по сути от исключений в Яве и С++ и какой в этом смысл.

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

К слову, есть std::panic::recover, так что аналог try/catch всё равно сварганить можно.

monk ★★★★★
()

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

anonymous
()
Ответ на: комментарий от monk

С учётом того, что panic может всё равно прилететь откуда угодно, смысл неясен.

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

Предложишь решение получше?

DarkEld3r ★★★★★
()

Не надо checked-исключения. Если хочешь информацию о дополнительных результатах вызова функции явно в сигнатуре, то помещай её куда следует: в тип возвращаемого значения. Вся суть исключений именно в том, что они динамические.

А так, интересно было бы увидеть рестарты и протоколы обработки исключений по типу Dylan. Или какой-нибудь упор на контракты типа Racket.

anonymous
()
Ответ на: комментарий от DarkEld3r

скажем, потенциальный «out of memory» может где угодно возникнуть, а обрабатывать его очень мало кто захочет

Обычный unwrap тоже кидает панику. А используется через строку.

Предложишь решение получше?

Common Lisp conditions.

Смысл исключений в том, что позволяет принять решение о том, что с ним делать через несколько слоёв. Типовой пример: файл ресурса на середине перестал читаться, надо спросить Abort/Retry/Ignore. Без исключений получаем, что надо всё состояние (открытый сокет, позицию) протащить через все слои Network / FS / GUI Framework / GUI, чтобы можно было сделать Retry. При том, что это состояние — особенность реализации слоя Network. И за освобождение ресурса начинает отвечать GUI, что вообще нехорошо, так как разработчик GUI может ситуацию проигнорировать.

monk ★★★★★
()

Concepts, Techniques, and Models of Computer Programming

там была глава про exceptions

dimon555 ★★★★★
()
Ответ на: комментарий от monk

Яве и С++
Там вместо исключений сделали как в Си статус возврата.

Иногда лучше жевать, чем говорить.

К слову, есть std::panic::recover, так что аналог try/catch всё равно сварганить можно.

Или использовать, собственно, try/catch

anonymous
()
Ответ на: комментарий от anonymous

Ох, прошу прощения, криво прочёл исходное сообщение.

anonymous
()

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

anonymous
()
Ответ на: комментарий от monk

Обычный unwrap тоже кидает панику. А используется через строку.

Это претензия уровня «abort завершает программу». Ну да, на то он и нужен.

Где можно на использование через строку посмотреть? Надеюсь речь не о примерах, где оно делается ради краткости? Да и применение может быть вполне оправданным - если мы хотим просто упасть. Не зря же планируют ввести выбор «стратегии» для паники - помимо текущей раскрутки стека добавят как раз terminate, чтобы упасть побыстрее. Потому что оно бывает востребовано.

Думаю, что мысль ты понял, но на всякий случай повторюсь: если (в расте) панику совсем убрать, то «все» функции будут возвращать кучу всякого «мусора», который в 99% случаев не нужен. Скажем, невозможность выделить память (к которой ещё больше вопросов из-за overcommit-а) мало кто обрабатывать хочет. Ещё паника используется для ошибок типа выхода за границы массива. И это именно ошибка программиста. Какую вменяемую обработку тут можно придумать? Разве что игнорирование проблемы.

Common Lisp conditions.

Да, штука мощная. Но ведь эта оно не сильно «дешевoe»? То есть для языка ТС может и подходит, но для «типа системного» не особо, так?

Опять же, проблема «явности» никак не решается ведь? То есть, это более гибкий механизм чем «традиционные исключения», но точно так же можно забыть обработать.

И за освобождение ресурса начинает отвечать GUI, что вообще нехорошо, так как разработчик GUI может ситуацию проигнорировать.

Если у нас есть RAII, то закрывать ресурс можно хоть в деструкторе исключения, раз мы тащим с собой состояние.

DarkEld3r ★★★★★
()
Ответ на: комментарий от anonymous

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

В Qt есть сигналы на эти случаи? Где на них посмотреть?

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Где можно на использование через строку посмотреть? Надеюсь речь не о примерах, где оно делается ради краткости?

https://github.com/servo/servo/blob/95883dcbcfe10104c7a78ac532cb9aeb4bda855f/...

https://github.com/servo/servo/blob/daad09d44245228fba9118316937add71bec7c58/...

Да и применение может быть вполне оправданным - если мы хотим просто упасть

Напоминает мне " || die" в Perl. Из-за этого было очень тяжело написать непадающую программу с использованием внешних библиотек с GUI. Так как любая нештатная ситуация роняла программу. При сохранении недавно встретилось «Фатальная ошибка. Кончилось место на диске.» — очень обидно было всё заново делать.

В общем, unwrap в библиотеке однозначно плохая идея.

Но ведь эта оно не сильно «дешевoe»? То есть для языка ТС может и подходит, но для «типа системного» не особо, так?

Чем это? Вполне дешёвое. Технически handler-bind можно сделать в виде обычного параметра. Что-то вроде

inf f(int x, THandlers handlers)
{
   ...
   g(x, handlers);
}

int g(int x, THandlers handlers)
{
   try {
   ...
   } catch(e) {
      restart(handlers, e, new restarts(restarts::continue,
                                        do_nothing,
                                        restarts::abort,
                                        terminate);
   }
}

TRestart my_handler(exception e, TRestarts restarts
{
  if (good(e)) return restarts::continue;
  return restarts::terminate;
}

int main()
{
   f(5, new handlers(my_handler));
}

Так что не дороже обычного try/except. Только поддержка от компилятора надо

monk ★★★★★
()
Ответ на: комментарий от DarkEld3r

То есть, это более гибкий механизм чем «традиционные исключения», но точно так же можно забыть обработать.

Так в этом и суть. Разработчик библиотеки пишет всевозможные рестарты, а на самом верху (где GUI) можно выбрать правильную стратегию или даже спросить у пользователя. В варианте раста нельзя получить доступ к данным не обработав ошибку. В стандартном try/catch из catch нельзя вернуться в функцию, где была ошибка. Поэтому эти оба варианта заставляют обрабатывать ошибки как можно ближе к месту возникновения. В CL наоборот — в местах потенциальной ошибки ставим рестарты, а обрабатываем максимально далеко (там, где точно известно как именно хотим обработать).

monk ★★★★★
()
Ответ на: комментарий от DarkEld3r

за освобождение ресурса начинает отвечать GUI, что вообще нехорошо

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

Я про систему Rust'а. Там исключений нет.

monk ★★★★★
()
Ответ на: комментарий от monk

https://github.com/servo/servo/blob/95883dcbcfe10104c7a78ac532cb9aeb4bda855f/...

Серьёзно? Это тесты.

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

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

Так что не дороже обычного try/except.

От которого в расте как раз хотят уйти.

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Так что не дороже обычного try/except.

От которого в расте как раз хотят уйти.

Наличие std::panic::recover не дешевле.

monk ★★★★★
()
Ответ на: комментарий от monk

Так в этом и суть.

Мы о разном говорим просто. Давай разделим два вопроса:

1. Вопрос наглядности - он же «внезапно прилетевшее исключение». Нельзя ничего гарантировать (можно забыть), нужно читать доку, чтобы узнать что надо обработать. Тут рестарты никак не помогают ведь (то есть ничем не лучше «традиционных исключений»)?

2. Гибкость обработки. Тут рестарты выигрывают, конечно. Хотя и можно поспорить насколько плохо без них живётся.

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Про библиотеки согласен. Но опять же, я не вижу способа как это решить на уровне языка.

Так CL conditions же. В терминах Rust можно добавить просто глобальный массив обработчиков и ключ у unwrap. + команду типа OnError. Впрочем, всё это можно и библиотекой замутить. Хотя и ООП тогда можно библиотекой...

monk ★★★★★
()
Ответ на: комментарий от monk

Я про систему Rust'а. Там исключений нет.

«Слова нет, а исключения есть.» Во первых, в панику передать любой объект можно, вытащить и обработать тоже. Менее удобно, чем при наличии try/catch, но можно.

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

Ну или я тебя не понял.

DarkEld3r ★★★★★
()
Ответ на: комментарий от monk

Наличие std::panic::recover не дешевле.

Потому его и хотят сделать опциональным.

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Вопрос наглядности - он же «внезапно прилетевшее исключение».

Теоретически, можно дописывать к сигнатуре throws как в Java. В статических языках можно его вычислять.

Практически, не хочется к

double f(double x)
{
   if(assureNotZero(x)) return 1/x;
   return 0;
}
писать, что «может бросить деление на ноль». А если не писать, то что делать, если assureNotZero на самом деле не проверяется на не ноль.

Поэтому проблема, имхо, неустранима.

monk ★★★★★
()
Ответ на: комментарий от DarkEld3r

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

Да, верно. Я зарапортовался чуток.

monk ★★★★★
()
Ответ на: комментарий от monk

Поэтому проблема, имхо, неустранима.

Тоже к такому выводу прихожу... «а жаль».

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Предложишь решение получше?

Да всё что угодно лучше, чем цепепе с его причудами :-) Мало того, что сам подход исключений там отстоище, так ещё и глюки :-) Вот, полюбуйтесь :-)

#include <fstream>
#include <iostream>

int main(int argc, char *argv[])
{
  std::ifstream f;
  f.exceptions(std::ios_base::failbit);
  try {
    f.clear(std::ios_base::failbit);
  } catch (std::ios_base::failure&) {
    std::clog << "Catched!? Wow!" << std::endl;
  }
  return 0;
}

~ $ g++ --version
g++ (GCC) 5.3.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

~ $ g++ -ogccbug gccbug.cpp
~ $ ./gccbug
terminate called after throwing an instance of 'std::ios_base::failure'
  what():  basic_ios::clear
Aborted

цепепе он такой :-)

anonymous
()
Ответ на: комментарий от anonymous

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

то помещай её куда следует: в тип возвращаемого значения.

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

Вся суть исключений именно в том, что они динамические.

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

А так, интересно было бы увидеть рестарты и протоколы обработки исключений по типу Dylan. Или какой-нибудь упор на контракты типа Racket.

Угу, записал.

Concepts, Techniques, and Models of Computer Programming - там была глава про exceptions

Спасибо, записал.

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

Почти убедил. В целом меня всегда устраивало, как это сделано в лиспе - по-моему, сделано толково. Но всё же интересно выслушать и другие мнения. Кстати, а почему быстро ушли от того, чтобы делать классические «обработчики сигналов», не связанные со структурой выполнения программы? Есть же сигналы в смысле прерываний, например, Control-Break. Без возможности обработать их, по-моему, нормальные программы не получится написать. Хотя я смотрю, вроде бы в лиспе можно сделать обработку Control-Break с помощью функции signal с единственным обработчиком, который будет делать то, что надо, а потом нормально возвращаться (отклоняя обработку). Другое дело, не знаю, как в SBCL повесить обработку на Control-Break - в моём случае он сразу молча погибает. Ещё бы интересно сделать это, чтобы сам механизм сигналов при этом не требовал выделения памяти - и будет вполне себе системный уровень. Не?

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

Почему и нет? В лиспе такое сплошь и рядом.

А если не писать, то что делать, если assureNotZero на самом деле не проверяется на не ноль.

Ага. Вот это ты зришь в корень. Конкретно из этой проблемы можно выкрутиться двумя способами. В обоих способах делается if-подобная конструкция «поделить или обработать ноль». В первую ветку попадаем, если x<>0 и там делим на x. Во вторую - если x=0 и пользователь делает, что хочет. Вопрос, как реализовать эту конструкцию. Два варианта:

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

2. Ввести тип «ненулевое-число» и операцию приведения к нему, которая опять же может вернуть код неудачи (пустоту в случае Раста). В этом случае уже система типов гарантирует, что делить на «ненулевое-число» можно и что мы не сможем привести ноль к этому числу (приведение типов поступит особым образом).

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

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

1. Ошибки усложняют программу на одно измерение пространства.

2. Нам это не нравится и мы пытаемся избавиться.

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

4. Не решая всей сложности проблемы ошибок, можно хотя бы локализовать последствия.

Способы:

4.1. Связать ресурсы с процессом, как это делает операционная система. Грохнем процесс, но ОС останется жива.

4.2. Связать ресурсы со стеком (RAII). В этом случае мы можем грохнуть нить или часть стека - живучесть процесса повышается. Здесь возникает проблема того, что освобождение ресурса - это тоже осмысленное действие, требующее, чтобы программа была в порядке. Если возникла серьёзная проблема, может оказаться невозможно освободить ресурс силами процесса. В С++ это проявляется как «исключение в деструкторе», но проблема находится не в языке, а в природе вещей.

5. Если речь идёт о разделении на библиотеку и приложение, то возникает вопрос о сигнатуре библиотеки. Документация заведомо ненадёжна. Надёжны коды возврата и checked исключения. checked исключения не прижились либо из-за многословности, либо потому что они нарушают модульность, а модульность была объявлена догмой. Коды возврата делают программы, по мнению некоторых, слишком многословными. И их можно забыть проверить.

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

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

Дополняйте, если я что-то упустил. Это пока не ответ на мой вопрос, а лишь анализ ситуации.

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 3)
Ответ на: комментарий от den73

И ещё забыл упомянуть, что есть ещё проблема падучей библиотеки. Неплохо бы, если бы была возможность гарантировать, что данная библиотека никогда не падает, а все проблемы адресует клиенту. Но такой возможности нет ни в одном современном языке. Единственный способ защитить клиента от падения библиотеки - это вынести её код в отдельный процесс ОС и использовать клиент-серверную технологию. ИМХО.

den73 ★★★★★
() автор топика
Ответ на: комментарий от den73

Почему и нет? В лиспе такое сплошь и рядом.

Такое не только в лиспе, но не для базовых возможностей ведь? Или в лиспе именно для них?

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

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

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

den73 ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.