Как стать автором
Обновить

Комментарии 81

Меня это достаточно сильно огорчает, потому что я не начинаю активно программировать на Rust именно из-за неудобств с обработкой ошибок. Даже достаточно посредственная поддержка со стороны IDE, нестандартный синтаксис и другие мелкие проблемы не останавливают. А теперь, похоже, ждать придется ещё дольше.
На Хабре есть такая статья «Что же там такого тяжелого в обработке исключений C++?». В статье рассказывается про две стратегии обработки исключений, из коей можно сделать выводы, что вне зависимости от выбранного механизма, есть некоторый (малый или умеренный) оверхед.

Не знаю, как обстоят дела с оверхедом механизма ошибок в Rust, но есть подозрение, что их затруднения как раз связаны с желанием имплементировать механизм ошибок, за который не придётся платить в рантайме. Что весьма похвально для системного языка. Вообще, одна из самых лучших вещей в Rust это то, что они по сути на полную катушку используют принцип C++ «вы не платите за то, что не используете», и при этом приправляют его статическими гарантиями.

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

При использовании таблиц исключений, оверхед при их (исключений) отсутствии нулевой.
НЛО прилетело и опубликовало эту надпись здесь
Судя по вашему комментарию, вас данная новость сильно расстроила. Если честно, не писал ничего серьёзного на Rust, хотя и балуюсь иногда с ним. Поэтому я немного не в курсе, неужели сейчас обработка ошибок на Rust настолько ужасна, чтобы это стало для вас ключевым фактором?
Я в основном пишу на C#. Я привык к нормальным исключениям, дополнительной информации в них, стек трейсам и общему базовому классу (причём не только для исключений).

Ещё я пишу на С++. Там с исключениями похуже, но всё же терпимо. Именно С++ я хочу заменить на Rust. Отсутствие исключений меня останавливает в первую очередь.

У нас тут 2015 год на носу, в конце концов. Я считаю, что возвращаться на 20 лет назад не стоит, и современные языки программирования должны отличаться от их двадцатилетних предшественников определенным гарантируемым набором фич. Все уже привыкли к классам, вызове функции в виде a f b (метод), а не f a b, свойствам, функциям высшего порядка, обобщенным классам, делегатам и, наконец, исключениям. Отказ от этого — это откат на 20 лет назад.

А отдельную группу людей, в 2014-м году пишущую прикладные вещи на Си и дарующую нам еженедельно новую уязвимость, я просто не понимаю. Это непрактично, небезопасно и не выгодно.
Я привык к нормальным исключениям, дополнительной информации в них, стек трейсам и общему базовому классу (причём не только для исключений).

То есть речь только о неудобстве отладки?
Ещё я пишу на С++. Там с исключениями похуже, но всё же терпимо. Именно С++ я хочу заменить на Rust. Отсутствие исключений меня останавливает в первую очередь.

C++ — один из моих основных языков. Писал на нём как с исключениями, так и без них (были проекты, где они были отключены и включать их было нельзя). Не сказал бы, чтобы использование исключений что-то так уж кардинально меняло. Наверное на шарпе или расте всё иначе. Можете привести пример, чтобы стало понятно?
Возьмём пример из статьи. Пусть есть стек из 50 функций, на дне которого находится способная провалиться функция div. Давайте рассмотрим варианты обработки этой ошибки.

  1. Обработка а-ля Option из Rust. Если что-то не получилось, каскадом выходим сразу из всех 50 функций (немного кода в каждой функции).
    На вершине стека вы получаете молчаливый отказ. Надо объяснять почему это так плохо?
  2. Обработка а-ля Result из Rust с автоматическим конвертированием Err (проброс через try! + интероперации, немного кода в каждой функции, куча кода за пределами функций для интеропераций).
    На вершине стека вы в лучем случае получаете DivisionByZero. Полностью бесполезный, у вас там снизу 50 функций.
  3. Обработка а-ля Result, но с ручным конвертированием в Error и заполнением данными (огромная куча кода везде).
    На выходе вы получите дебаг информацию, зависящую от того, как вы написали обработку ошибок в каждой из 50 функций.
  4. Без обработки ошибок, но с одним throw именно в том месте, где может быть ошибка — в div. А может и без throw, пусть язык этим занимается.
    На вершине стека вы получаете полный стек трейс, сообщение об ошибке и какие-то дополнительные данные, ничего для этого не сделав.
  5. Оборачивание исключений в этих 50 функциях в нужных местах.
    Получаете production-ready код с морем дебаг информации, обернув в нужном месте исключения.


Мне кажется очевидно, какие варианты выигрывают.
Без обработки ошибок, но с одним throw именно в том месте, где может быть ошибка — в div. А может и без throw, пусть язык этим занимается.
На вершине стека вы получаете полный стек трейс, сообщение об ошибке и какие-то дополнительные данные, ничего для этого не сделав.

Оборачивание исключений в этих 50 функциях в нужных местах.
Получаете production-ready код с морем дебаг информации, обернув в нужном месте исключения.

То есть я не ошибся и основное применение — удобный сбор информации для отладки? А на вершине стека просто отправка этой информации куда-либо?
Это ещё и возможность продолжить работу после ошибки. Цена этого в Rust слишком высока.
На вершине стека вы в лучем случае получаете DivisionByZero. Полностью бесполезный, у вас там снизу 50 функций.
Там есть возможность с помощью макросов file! и line! получить номер строки в файле. Просто напишите свой try!, который будет не просто возвращать ошибку, а ещё и добавлять к ней информацию о том, где она пробрасывалась. Это вариант 3, но «кучу кода» пишет компилятор. Эти же макросы могут быть использованы и в месте генерации ошибки.

Локальные переменные вы, конечно, не получите (а, может, я просто плохо знаю макросы Rust). Но это всё же лучше, чем стандартный вариант.
А какой, простите, смысл делать относительно сложную систему типов и потом одним махом компрометировать ее введением исключений?

Checked-исключения не предлагать, вспомним Java.
А что подразумевается под компрометацией?
Возможность исключения никак не выражается в сигнатуре функции, соответственно тайпчекер спокойно пропустит код, который упадёт в рантайме. С этой точки зрения Result кажется более подходящим средством обработки ошибок.
А чем типизированный Result принципиально отличается от checked-исключений в Java, которые вы просили «не предлагать»?

Собственно, строго типизированные исключения не являются чем-то таким особенным, если понять, что это просто монада, и в любом языке, где есть средства для работы с монадами, автоматический проброс и гарантированная обработка с полной строгой типизацией реализуются, по сути, автоматически.

Исключения в Java тоже можно рассматривать в качестве такой монады, которая насильно засунута в каждый закоулок языка (т.е. условно говоря — каждое выражение в Java на самом деле имеет не тип T, а Result<T, ExceptionList>). Проблема с ними там в том, что в Java убогие дженерики, которые вообще никак не покрывают throws, что не позволяет объявлять higher-order functions с корректной сигнатурой («я бросаю все, что бросает переданная мне функция, плюс X, минус Y»). Это, кстати, пытались прикрутить в Project Lambda на ранних этапах, но после радикального упрощения оно пошло под нож.
У меня немного опыта с Java, но мне всегда сhecked-исключения казались отличной идеей. Как я понимаю, вы относитесь к ним довольно негативно. Если не трудно, расскажете почему?
Я к ним не отношусь никак, ибо на Java не пишу уже пару лет.

Checked-исключения в теории однозначно лучше unchecked. Но у исключений в принципе очень плохо с composability (кстати, как это лучше по-русски сказать?). То, что в функциональном стиле можно записать в виде непрерывной цепочки вызовов, в Java превратится в лестницу вложенных try-catch. Косвенный довод в пользу моего мнения — наличие в Scala типа Try[T], который как раз превращает код, бросающий исключения в код, возвращающий результат в виде алгебраического типа данных.
Трассировку стека Rust умеет выдавать.
А что происходит, например, при делении на ноль? Крашится?
Да, происходит panic таска (для искуственного создания есть макрос panic!). Если это основной таск, то приложение закрывается.
Если в panic есть полная инфа (место возникновения, stack trace), то больше и не надо.
Тут я кажется понял соображения, которыми руководствовались создатели Rust.

Исключения нужно бросать, но не ловить. Если какой-то процесс падает, пусть падает полностью. Если программист не знает точно, что может пойти не так, он может обернуть выполнение в таск. Это заставит не использовать исключения в рабочих сценариях — только если на самом деле произошла невосстановимая ошибка и надо перейти к следующему элементу, залогировав ошибку.

Тот же пример с делением — не стоило вычислять функцию вслепую, не сделав анализ всех ситуаций. Если на верхнем уровне программист знает, когда должно произойти деление на 0, можно не вызывать функцию. А если не знает — вызов чреват ошибками. Вдруг что-то пошло непредвиденно, но исключение не выпало.
Разве что не хватает «finally», чтобы при возникновении panic вышестоящие уровни могли правильно де-инициализироваться.
А локальные переменные выше по стеку будут в этом случае правильно уничтожены? Если да, всё вообще хорошо.
НЛО прилетело и опубликовало эту надпись здесь
Тогда всё чётко, о чём ещё мечтать ))
Как ни странно, finally в Rust есть. Только немного громоздкий.
Странненькая ссылка )
Ой, сорри) не знаю, как так получилось)
Вот правильная: doc.rust-lang.org/std/finally/
Это заставит не использовать исключения в рабочих сценариях — только если на самом деле произошла невосстановимая ошибка и надо перейти к следующему элементу, залогировав ошибку.
Во-первых, восстановиться после panic нельзя, только из другого таска. Во-вторых, не всегда можно отдать код в отдельный таск, есть требовательные к, например, производительности места. В-третьих, писать код с вездесущим try и проверкой на 0 перед делением — слишком дорого. Выше я написал комментарий с возможными сценариями.
Проверять на 0 прямо перед делением и прокидывать наверх результат слишком хлопотно. Проверка должна быть на самом верхнем уровне, где можно понять, что ф-ция не определена.
Если это основной таск, то приложение закрывается.


Не основной, а последний. В Rust нет основных тасков как в Go, все треды равноправны.
Серебряной пули не вышло… ну да ладно. На низком уровне исключений сейчас и так нет, далеко не все реализации C++ имеют поддержку исключений, вот я ими и не пользуюсь переносимости ради.

setjmp — эпохальный костыль, вместе с возвращением NULL. В Rust оно хоть выглядит приличнее и проверяется на этапе компиляции.
Серебряной пули не вышло
Ну я надеюсь исключения в нормальном виде всё-таки когда-нибудь будут. :) Уж очень аппетитно выглядит всё остальное.
НЛО прилетело и опубликовало эту надпись здесь
Ну что, язык Go с его “return nil, err” передаёт горячий привет своему «убийце»:)
Да, забавно смотрится, как абсолютно такой же подход к обработке ошибок просто оброс Rust-specific синтаксисом.

Цитирую документацию:
// This is a three level match pyramid!
    match checked::div(x, y) {
        Err(why) => fail!("{}", why),
        Ok(ratio) => match checked::ln(ratio) {
            Err(why) => fail!("{}", why),
            Ok(ln) => match checked::sqrt(ln) {
                Err(why) => fail!("{}", why),
                Ok(sqrt) => sqrt,
            },
        },
    }
они будто бы даже гордятся тем, что можно городить пирамидку из вызовов и проверок их Result на ошибки.
Хаскелисты смотрят на это с недоумением.
Обработка ошибок по своей природе — вещь монадическая, неужели нельзя навертеть пару макросов, чтобы реализовать монадический бинд? Что-то вроде (с синтаксисом rust знаком весьма поверхностно):
checked::div(x, y) >>= |ratio| checked::ln(ratio) >>= |ln| checked::sqrt(ln)

В haskell подобные цепочки разворачиваются в такой же код, что и в вашем примере.

Отсутствие исключений, честно говоря, вообще не огорчает.
Я думаю учитывая функциональные возможности Rust что-то такое предполагалось.
Вообще вся эта конструкция на расте записывается так:
let ratio = checked::div(x, y).unwrap();
let ln = checked::ln(ratio).unwrap();
let sqrt = checked::sqrt(ln).unwrap();

Либо, если хочется именно пайпа, то делается примерно как в хаскелле:
checked::div(x, y).and_then(|ratio| checked::ln(ratio).and_then(|ln| checked::sqrt(ln).unwrap()))
НЛО прилетело и опубликовало эту надпись здесь
А чего я, простите, должен добиться? В оригинальном коде используется проверка с вызовом удалённого сейчас макроса fail!, заменой которого в данной ситуации и является panic!, поэтому unwrap делает ровно то, что нужно.
Полагаю, что в оригинальном коде fail! исключительно для примера (неудачного), а суть отрывка как раз в демонстрации возможности не паникуя обрабатывать ошибки через мэтчинг.
Разница с монадой в том, что в последней у вас результатом вычисления последовательности выражений будет тоже Result — у которого можно проверить Error. Т.е. это эквивалент плюсового:
try {
  foo();
  bar();
  baz();
} catch (Error) {
  ...
}


Где вы не проверяете отдельно каждый вызов foo, bar и baz на Error, а ловите его один раз при выходе из блока.
Уберите в моём втором варианте последний unwrap, будет как раз то, что надо.
Там ниже уже написали про try!
Следующий раздел из документации, которую процитировал Xlab, говорит:
Chaining results using match can get pretty untidy; luckily, the try! macro can be used to make things pretty again. The try! macro expands to a match expression, where the Err(err) branch expands to an early return Err(err), and the Ok(ok) branch expands to an ok expression.

Т. е. в тех случаях, когда нужно просто пробросить ошибку наверх, достаточно написать:

let sqrt = try!(checked::sqrt(x));
do_smth(sqrt);

И это будет эквивалентно:
match checked::sqrt(x) {
    Err(why) => return Err(why),
    Ok(sqrt) => {
        do_smth(sqrt);
    }
}

Чистый match предполагается применять только в случае окончательной обработки ошибок на верхнем уровне.
А как этот макрос работает? И doc.rust-lang.org/guide-macros.html#motivation, например?
В него do_smth не передаётся никаким образом, вроде, а в развёрнутом коде оно есть.
Ну, ясное дело. Исходный код я почитал. Передаётся туда одно выражение, по которому и матчится, а do_smth как там оказывается?
Он там не оказывается. Макрос try! разворачивается в зависимости от аргумента либо в return why из Err(why), либо в value из Ok(value). Этот самый value потом можно чему-то присвоить: let sqrt = try!(checked::sqrt(x)); и уже после этого над ним изобразить do_smth(sqrt);.
А, действительно, привык к Хаскелю. Тогда с try всё понятно, спасибо, а biased_match по ссылке?
Почитал документацию внимательнее, оказалось, я был неправ. Макрос try! определен следующим образом:
macro_rules! try(
    ($e:expr) => (match $e { Ok(e) => e, Err(e) => return Err(e) })
)

То есть это:
let sqrt = try!(checked::sqrt(x));
do_smth(sqrt);

Развернется в это:
let sqrt = match checked::sqrt(x) { Ok(e) => e, Err(e) => return Err(e) };
do_smth(sqrt);

В макросе по привиденной вами ссылке разобраться с текущим уровнем знания Rust'а не могу, извиняйте.
Спасибо. Макрос try! оказался простым, а вот реализация biased_match от меня ускользает. Ощущение, что там какой-то хак, потому что прямой разворот макроса выглядит каким-то странным.
Если знание английского позволяет, можете заглянуть сюда: www.reddit.com/r/rust/
На этот раздел подписаны 6,5 тысяч людей, в том числе те, кто контрибьютит в Rust (серые и сиреневые бейджи «rust»), а также сами создатели языка (оранжевые бейджи).

Уверен, что с вашим вопросом вам там смогут помочь.
Спасибо, если иными способами не разберусь, попробую.
Хотя, в общем-то, и там ничего такого нет. Разворачивается оно в это:
let g1, val = match x {
	Good1(g1, val) => g1, val
	_ => { return 0 }
}
let result = match g1.body {
	Good2(result) => result
	_ => { panic!("Blah") }
}
return result + val;
Такая возможность существует в виде методов на Result и Option. Кроме того, в коммьюнити языка одна из наиболее желаемых фич — это higher-kinded types, которые открывают дорогу, в том числе, к обобщённым монадам и обобщённым операторам для работы с ними, наподобие do-нотации хаскелля или for comprehensions скалы. К сожалению, в 1.0 этого не будет просто по причине нехватки времени.
Гм. В каждой статье про Rust всегда найдётся умник, который сравнит его с Go, но называть его «убийцей» — это пожалуй впервые. Который раз уже вам объясняют: Go не конкурент Rust по-крайней мере в области системного программирования, потому как ни один системщик не станет всерьёз рассматривать в качестве языка разработки язык со сборщиком мусора. Go имеет свою конкретную, вполне узкую область — это язык для написания бэкендов. Rust же позиционируется как системный язык общего назначения, то есть современная альтернатива C/С++.
Как раз наоборот — под каждой статьёй про Go находятся умники, которые начинают рассказывать, какой Go негодный язык, и как всё то, что ужасно в Go (да и во всех остальных языках), будет в Rust-е сделано правильно и шоколадно.

Так что когда оказалось, что Rust, по сути, использует ту же работу с ошибками, что и Go (за которую последнего ругают все кому не лень), то удержаться от иронии было сложно:)
Что ж все нервные такие… Ну извините, если обидел.
Я, если что, вас не минусовал и не обижался. Думаю, просто подобные сравнения уже всех немного достали, а тут вы под горячую руку попались.
Здесь тред немного политический, хоть и хабр и в не политики. Так что оставляя здесь комментарий не в пользу Rust вы рискуете быть неправильно понятым, так как все нейтрально мыслящие иронию поняли как надо, мне кажется :)

Меня тоже достали сравнения, но блин, это как в ситуации с Android vs iOS — первые сравнивают и критикуют, вторые берут и пользуются. Меня интересует расклад дел и прогноз на будущее, так что я много читаю мнений. И вот почему-то «нелюбителям» Go не лень каждый раз сравнивать его с Rust и прочими хаскелями, подчеркивая ничтожность, почему бы не заметить здесь обратное наблюдение? Что как ни крути, а один из главнейших минусов разработчики «по-настоящему правильного» языка взяли да оставили без новизны подхода :)

Go имеет свою конкретную, вполне узкую область — это язык для написания бэкендов
Один брякнул и все повторяют из уст в уста, что значит для бекэндов? Нет, я не спорю, что в realtime задачах при ограниченном наборе ресурсов или при большом количестве данных требуется специализированный подход, но Go привязывать к «backend» смысла нет. Вот я бы поспорил, удобнее ли на Rust писать GUI программы, чем на Go.
НЛО прилетело и опубликовало эту надпись здесь
Честно говоря, из именно системного, написанного на Java, знаю только i2p-демона. Возможно, конечно, что я упускаю какую-то большую область, где пишут системные вещи на Java. Не просветите? Честно говоря, обижать никого не хотел.
Вообще, строго говоря, в go можно отключить gc и освобождать память руками (неявно — зануляя слайсы и т.д., через задницу, в общем =)
НЛО прилетело и опубликовало эту надпись здесь
Какой-то заученный ответ, падения не из-за nil, а из-за невозможности продолжения выполнения программы. И если программа продолжила там, где не должна была, то это ошибочка-то посерьёзнее падения будет.
НЛО прилетело и опубликовало эту надпись здесь
Исключения, кроме оверхеда, плохи еще тем, что добавляют неявные побочные эффекты, а в Rust все-таки есть установка на явность.
Кстати, интересный вопрос: а каким образом можно на двух стульях усидеть — и явность сохранить, и избавиться от вложенных конструкций обработчиков ошибок в цепочках вызовов? И без монадических хитростей, в идеале :)
Ну и отлично!
А мне не совсем понятен подход с не owner str и мутабельным owner String (ака StringBuilder).
Также интересна судьба абстрактных типов, например fn get_iter(&self) -> Iterator<i64>;.
Общее мнение насчёт abstract return types — они нужны, но пока можно потерпеть. Соответствующий RFC был закрыт с комментарием, что он нуждается в переработке. Скорее всего, после 1.0 к нему вернутся.
Не совсем понял, если функция возвращает значение типа Result<f64, MathError>, как в примере из статьи, то можно ли «замапить» его на другую функцию с помощью map/flatMap?

Грубо говоря, вот что-то похожее на такой пример будет работать в Rust?

fn div(x: f64, y: f64) -> Result<f64, MathError>
fn scale(n) -> Result<f64, MathError>

div(1, 2) map scale(100)
Да, безусловно. Выше уже приводили пример. Соответствующий метод называется and_then():
div(932, 442).and_then(|r| scale(r))
Извините, совершенно непонятно, почему подобная новость заслуживает отдельной статьи. В Rust есть исключения в смысле stack unwinding — это макрос panic!(). Да, такие «исключения» нельзя ловить, но это трудно сделать кроссплатформенно и чревато небезопасностью работы с памятью, поэтому в Rust так делать нельзя.

RFC по вашей ссылке — это предложение добавить в язык удобный способ пропагации ошибок, похожий на исключения, но не несущий их проблем, потому что этот способ всегда можно переписать через match/return. Фактически, это синтаксический сахар. Он не подразумевает какое-то разворачивание стека и обработку, ничего того, чего нельзя сделать прямо сейчас.

Ну и если на то пошло, то этот конкретный RFC всего лишь отложили до после 1.0, потому что к 1.0 его просто не успели бы реализовать достаточно качественно. Как тут уже сказали, 1.0 — это не конец развития, а только его начало.
Кстати в довольно используемом в последнее время языке Objective-C приблизительно та же ситуация с исключениями. Они как бы в языке есть, но используются исключительно для runtime error ситуаций, когда нужно записать что-то в лог и умереть.
Все фреймворки построены вокруг возвращения ошибки через возвращаемое значение или параметр-указатель.

При этом я не могу сказать, что за несколько лет написания кода под iOS мне это как-то сильно мешало.
Мы пишем в функциональном стиле на F# и сознательно отказались от исключений в пользу возвращаемого типа Choice (по сути тот же Result в Rust). Если результатом функции может оказаться ошибка, то это видно из её сигнатуры, код становиться более явным.
Если результатом функции может оказаться ошибка, то это видно из её сигнатуры, код становиться более явным.

Может быть я задам глупый вопрос, но всё-таки. Вот у нас есть задача: гарантированно обрабатывать ошибки, возвращаемые из функции. В разных языках есть похожие методы её решения: Result/Choice/Maybe и т. п. Но почему никто не делает нормальные checked exceptions? Чем эта концепция хуже опционального типа для отлова ошибок?
Их сложнее имплиментировать, за них надо платить, их сложнее сделать кросс-платформенными, при этом плюсы от них не такие уж очевидные.

P.S. По-моему вы веткой промахнулись )
А есть какое-нибудь волшебное место, где разработчики Rust последовательно обосновывают решения по дизайну языка? Взвешивают за и против различных вариантов? Это был бы просто кладезь. Заглянул в дев-блог, но там как-то пусто.
Есть репозиторий, где публикуются все предложения по внесению изменений в язык (RFC), и там же и обсуждаются.
github.com/rust-lang/rfcs
Есть протоколы встреч, на которых принимаются решения, включать то или иное предложение в язык или нет.
github.com/rust-lang/meeting-minutes
Есть ещё форум, на котором люди периодически обсуждают предложения перед их публикацией, но там относительно много шума и мало чего доходит даже до стадии RFC.
discuss.rust-lang.org/
Спасибо большое, это то, что нужно.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации