Ещё об обработке ошибок в Rust

Aug 24, 2015 22:46

С обработкой ошибок в Rust надо явно что-то придумывать. Мне категорически нравится, что в языке нет исключений, но без синтаксического сахара вроде хаскелевской конструкции do код с аккуратной обработкой ошибок визуально превращается в простыню с перекладыванием значений из одних типов в другие ( Read more... )

exceptions, code, rust, errors, error handling, language

Leave a comment

Comments 27

permea_kra August 24 2015, 21:01:07 UTC
>насколько я знаю, там аккуратно обрабатывать ошибки тоже не очень любят

Там не то чтобы этого не любят, там это можно делать слишком многими способами.

> непонятно как правильно реализовать сахар типа do

Он не нужен. Т.е. с ним конечно проще, но в цацкеле зачастую удобнее использовать операторы из class Monad напрямую. А конкретно для парсеров можно попробовать приспособить аппликативный интерфейс (Control.Applicative) - его, зачастую, достаточно, и вот его -то по идее должно быть возможно сделать. Тем более, что TCO в расте нет, а значит в монадическом коде можно легко попасть на растущий стек.

Reply

thesz August 25 2015, 09:46:41 UTC
Вот и я о том же.

Насчёт монадического кода - если конвертировать по Чёрчу, то переполнения стека не будет.

Reply

swizard August 25 2015, 13:25:55 UTC
Да, согласен насчёт Control.Applicative, похоже на правду.

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

Так что надо придумывать какие-то пути обхода.

Reply

permea_kra August 25 2015, 14:32:17 UTC
как раз свойства представления входного потока для аппликативного интерфейса разборщика глубоко пофиг. Вопрос именно в том, чтобы получился этот аппликативный интерфейс, т.е. чтобы сам разборщик был функтором. Итераторы для этого наружу вообще необязательно показывать.

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

Reply


antilamer August 25 2015, 02:30:53 UTC
У нас на С++ есть тип "ошибка, или значение типа T"; обращаемся с ним при помощи макросов, в 95% случаев получается достаточно удобно. В остальных 5% да, нужно написать if.
Например, ASSIGN_OR_RETURN(auto foo, GetFoo()), или RETURN_IF_ERROR(DoBar()). И коротко, и понятно, где может произойти ошибка и как она и куда передается. Как я понял, это примерно то, что делает try!. Можешь привести пример, когда читаемо написать не получается, а с настоящей монадой бы получилось?

Reply

thesz August 25 2015, 09:45:33 UTC
https://ro-che.info/ccc/28

Это всё, что я могу сказать. ;)

Reply

swizard August 25 2015, 13:42:42 UTC
Да, try! по-сути делает ровно то же самое, плюс некоторая мелкая магия по автопреобразованию типа ошибки к тому, который требуется сигнатурой.

Это хорошо работает, но только в случае линейного кода. Например, вместо такого:

for value in values.split(',') {
match value.parse() {
Ok(v) =>
result.push(v),
Err(e) =>
return Err(ParamError::SampleValue(value.to_owned(), e)),
}
}
После некоторой подготовки можно написать так:

for value in values.split(',') {
result.push(try!(value.parse()))
}
Но try! теряет полезность в замыканиях, потому что возвращает управление только из анонимной функции, а не из родительской (в КЛ, например, для этого есть return-from, которым можно вернуть управление из любого уровня вложенности).

Например, вот так можно собрать в массив длины всех строк, поданных на stdin:

let stdin = io::stdin();
let all_lines: Vec = stdin ( ... )

Reply

antilamer August 26 2015, 04:04:54 UTC
Да, понял, о чем ты. Сам с такой необходимостью не сталкивался, но согласен, что тут на макросах далеко не уедешь. С другой стороны, можно заявить, что это забота функции map, т.е. что надо предоставить overload "map с возможностью ошибки" с хорошо задокументированной семантикой обработки оных ошибок. С аппликативными функторами, правда, можно было бы обойтись и без этого :)

Reply


dmzlj August 25 2015, 04:49:33 UTC
Ну вот ASSIGN_OR_RETURN(auto foo, GetFoo()) - это и есть нечитабельно. а

wtf = do
foo <- GetFoo
doSomethingWith foo

довольно читабельно, хотя и вырожденный случай.

Reply

antilamer August 26 2015, 04:07:43 UTC
Ну почему не читабельно? Мы вот читаем и ничего :)
Не очень красиво, конечно - но если оценивать по критерию "количество строк", то получается столько же; по критерию "понятно ли, что именно происходит", то может быть даже и лучше (по "<-" не всегда понятно, какая монада имеется в виду); по критерию "сколько ошибок происходит из-за неправильного понимания такого или похожего кода" - не могу припомнить ни одной ни в кодревью, ни в продакшне...

Reply

dmzlj August 26 2015, 04:42:57 UTC
Ну потому, например, что auto foo это у нас стейтмент, а стоит на месте выражения. Макрос выглядит как функция, но вообще никак им в итоге не является. Отвратительно, на самом деле.

Reply

antilamer August 26 2015, 06:20:20 UTC
Что я могу сказать... Твоя претензия мне понятна, но я ее не разделяю, у меня другая система ценностей в вопросах читаемости :)

Reply


thesz August 25 2015, 09:43:29 UTC
Твоя иерархия успехов это комбинация монад Maybe и Either (успех/неуспех и успех/конкретная ошибка). При этом монадичность оных не используется - если у тебя нет использования >>=, или ты можешь переписать всё на >>, то это аппликативный функтор.

Поэтому всё более-менее просто, даже do не надо. Разбор строки даёт Either (parse :: String -> Either String a), наличие/отсутствие параметра Maybe. Поэтому разбор комбинируется просто: fmap parseVec64f как раз будет иметь тип Maybe String -> Maybe (Either String Vec64f).

Reply

swizard August 25 2015, 13:45:59 UTC
Согласен насчёт аппликативных функторов. Ответил в другой ветке про это.

Reply

swizard August 25 2015, 13:51:58 UTC
> fmap parseVec64f как раз будет иметь тип Maybe String -> Maybe (Either String Vec64f).

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

А как быть, если внутри parseVec64f используется функция parse64f, тип ошибки у которой другой - например, перечисление вроде InvalidSymbol Char | Overflow | EmptyString. И, например, используется какая-то другая функция с другим типом ошибки.

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

Reply

thesz August 25 2015, 14:24:42 UTC
В принципе, Either a b вполне себе обобщается до нужных результатов и ошибок.

Выражение fmap parse не изменится, если ты изменишь тип ошибки.

Reply


theiced August 25 2015, 09:48:22 UTC
просто статическая тупизация - говно.

Reply


Leave a comment

Up