Pull to refresh

Comments 86

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

Как раз наоборот — «понимать, отслеживать и думать что делать» программист должен при явном возврате кода ошибки, при использовании же исключений думать о них нужно только на том уровне, где их можно/имеет смысл обработать.
Я понимаю, о чём Вы, но не совсем разделяю. Допустим, у вас есть такой код внутри функции:

foo(x);
bar(y);

Спаведливо (интуитивно? очевидно?) полагать, что сначала вызовется foo, а потом bar, как и написано. Однако, исключение в foo, которое здесь не отлавливается, может сорвать вызов bar. Таким образом, может быть нарушена целостность объекта, инвариант состояния, и т. д. (допустим, разрыв пары lock/unlock).

Чтобы понять, что произойдёт в действительности, программисту нужно знать, какие исключения и при каких условиях бросают foo и bar, что уже совсем не очевидно.
Если код был разработан с учетом использования исключений, то описанная вами ситуация не может быть. А если иначе, то это некорректный код.
На эту тему в одной из статей на хабре про Go было обсуждение — плюсы и минусы исключений и подхода Go рассматривали.
Конкретно по Вашему примеры функция bar(x) и не должна вызваться, т.к. до этого функция foo(x) не отработала. Если не так, то плучим неопределённое поведение. Ну плюс исключений, если нужно вот так подряд методы вызывать, не нужно каждый раз проверять, что метод вызвался корректно и если нет, то прервать вызов методов. А интересно, как в Rust такой, на мой взгляд, типичный use case реализуется — нужно вызывать подряд несколько методов, например по работе с с СУБД, и если была ошибка, то сделать rollback, а если после всех этих вызовов небыло ошибки, то commit?
А как в Rust с управлением памятью? Есть GC?
Вообще фишки языка мне понравились, спасало за обзор!
Для вызова нескольких методов подряд используют ранний возврат с помощью макро try!
fn write_info(info: &Info) -> io::Result<()> {
    let mut file = try!(File::create("my_best_friends.txt"));
    // Early return on error
    try!(file.write_all(format!("name: {}\n", info.name).as_bytes()));
    try!(file.write_all(format!("age: {}\n", info.age).as_bytes()));
    try!(file.write_all(format!("rating: {}\n", info.rating).as_bytes()));
    Ok(())
}

Garbage collector'а нет в языке, но разные аллокаторы можно подключать через библиотеки (например Arena).
Вот что значит отсутсвие исключений. Практически каждая строчка кода теперь должна оборачиваться в try.
И в чём проблема? Длинна строчки увеличится на 6 символов? Зато явно видно, какие операции могут вернуть ошибку, а какие — нет.
Вместо того, чтобы отметить регион, где происходит ошибка, нужно оборачивать каждый метод отдельно. Хотя это, конечно, дисциплинирует, чтобы не писать try catch во всё тело метода, но все же выглядит довольно странно, мягко говоря.
Если это настолько раздражает и делает работу невыносимой, то можно написать плагин к компилятору, который введёт какой-нибудь макрос try!!, который анализирует вложенные в него выражения и оборачивает все вызовы, возвращающие Result, в match + return.
Думаю, можно и без плагина обойтись. Просто try_block! как новый макрос кажется вполне возможным.
а к нему ещё и catch! чтобы подключить логику очистки и восстановления после ошибки
Проблема в том, что с исключениями вы не будете знать, происходит ли в этом регионе ошибка и если происходит то какая. Те же ошибки, связанные с выделением памяти в плюсах может выкинуть вообще любая функция. То есть если исключения есть, то нужно ожидать, что всё бросает исключения.
Регион? Ну так-то да, регион, но ошибка из одного места региона не равна ошибке из другого места региона. Тут сказывается не столько формальная сила типизации языка, сколько реальная типизация ошибок, обусловленная предметной областью задачи. Если ошибки произошли в разных местах этого региона, то и источники у них разные, и корректная их обработка тоже будет различаться. Какой смысл остаётся от региона, если все захваченные им ошибки всё равно потом нужно распределять по обработчикам?

Если я не прав, то в качестве контрпримера приведите ситуацию, когда мы можем создать регион, в двух разных точках которого потенциально возможно получение ошибок одинакового типа (с формальной точки зрения) и главное — одинаковую обработку этих ошибок (фактическая типизация предметной области).
Обычно это решается обычным полиморфизмом, ибо ошибки (я работаю с .Net, поэтому в качестве примера привожу его) просто наследуются, и например при работе с базой достаточно ловить просто какой-нибудь DbException. Ну и все ошибки отличаются по большому счету только текстом, поэтому вся обработка заключается в написании подобных блоков
try
{
 ...
}
catch (Exception e)
{
   Logger.Log(e.GetType().Name, e.Message, e.StackTrace);
}
Ну, не каждая строчка вообще, а только каждая потенциально опасная строчка.
На мой взгляд, это вполне приемлемая цена за отсутствие исключений.
  try {
    innocent_looking_function();
  }
  catch ( const boost::exception& e ) {
    // Everyone uses boost
    handle_error( boost::diagnostic_information( e ) );
  }
  catch ( const std::exception& e ) {
    // Everyone uses std
    handle_error( e.what() );
  }
  catch ( const CException& e ) {
    // The library provider has defined his own CException thrown by reference
    handle_error( e.what() );
  }
  catch ( CException* e ) {
    // But there is a 20 year old MFC stuff as well; do include magic in order to compile
    handle_error( e->Text() );
    e->Delete();
  }
  catch( ... ) {
    // I have no idea what else can be thrown
    handle_error(_T("No idea what was thrown"));
  }
Ну это плюсовая проблема стандарта, а не проблема исключений. В той же BCL, да и джаве, есть набор стандартных эксепшнов, которые прописаны ажно в стандарте (ECMA335).
В плюсах тоже есть набор стандартных исключений.
Стандартных, потому что они просто есть в бусте каком-нибудь, или они прописаны прямо в стандарте? Потому что в той же CLR есть например параграф I.10.5 Exceptions, где черным по белому написано, что, где и почем. У плюсов я такого не видел, с радостью увижу выдержки из C++11 или C++14 стандартов.
Например,
18.8 Exception handling
The header [exception] defines several types and functions related to the handling of exceptions in a C++ program.
И это я не говорю о всяких bad_* классах, которые тоже исключения и их определения раскиданы по стандарту.
Хм, спасибо за информацию.
Ну, собственно, вот, чтобы не быть голословным. Часть стандартной библиотеки, а значит — описаны в стандарте. Если нужна именно ссылка на стандарт, то вот здесь, параграф 18.8, страницы 461-466.
В зависимости от API, это делается с помощью RAII. Например, вот так:

fn do_work(connection: &mut Connection) -> Result<(), SqlError> {
    let mut txn = connection.begin();
    try!(txn.insert(...));
    try!(txn.update(...));
    ...
    txn.commit()
}


Здесь connection.begin() возвращает объект, у которого в деструкторе транзакция отменяется. Если этот объект выходит из области видимости, то транзакция отменяется. Макрос try!() как раз обеспечит ранний выход из области видимости. Метод commit() «поглощает» объект (принимает по значению и перемещает в себя), выполняя коммит транзакции. Поскольку txn уходит «во внутрь» метода, он не будет уничтожен прямо здесь, и роллбэка не произойдёт.

А как в Rust с управлением памятью? Есть GC?

Собственно, безопасное управление памятью без GC — это одна из ключевых фичей Rust. Если кратко, то управление памятью осуществляется за счёт умных указателей, гарантии которых невозможно нарушить из-за концепций владения и заимствования (например, вы не сможете получить ссылку на внутренность какого-нибудь умного указателя и затем уничтожить сам указатель — компилятор вам не даст). В частности, в стандартной библиотеке есть Rc, который является умным указателем с подсчётом ссылок (и его аналог, Arc, который использует атомарные операции — он медленее, но зато потокобезопасный).

Умного указателя с полноценным GC нету, фактически, только потому, что никто пока что не потрудился его сделать. Как выяснилось на практике, почти всегда без сборки мусора можно обойтись и совершенно не потерять в удобстве.
Часто любят в функционально-монадическом стиле писать что-то вроде (привет, Haskell):

assert_eq!(Ok(2).and_then(sq).and_then(sq), Ok(16));
assert_eq!(Ok(2).and_then(sq).and_then(err), Err(4));
assert_eq!(Ok(2).and_then(err).and_then(sq), Err(2));
assert_eq!(Err(3).and_then(sq).and_then(sq), Err(3));


Правда, результат надо всё равно выбросить наружу с помощью `try!` или сделать `unwrap()`.
На эту тему немало копий сломано, но если от кодов ошибок ушли к исключениям, то наверное это нужно? Я просто когда смотрю километровые стектрейсы из 30-40 методов в каком-нибудь IIS не представляю, СКОЛЬКО кода добавилось бы при пробросе каждой из этих ошибок выше, тем более, что по стектрейсу можно определить, что где отвалилось, а с кодами ошибок… Мартин писал про это давным-давно, и я с каждым годом убеждаюсь в его правоте. Единственное преимущество кодов ошибок — они более быстрые, потому что разворачивание стектрейса и всё прочее с последующим захватом требуют некоторых ресурсов, не сильно затратных в случае .Net или Java, но которых желательно избежать в системном языке. Но говорить о том, что коды ошибок удобнее — это извините меня, называется.
image
Вот это пруфы так пруфы!
P.S. Полностью согласен — сам работал с проектом, где использовались коды вместо Exception. Очень не удобно обрабатывать, много лишних проверок.
В данном случае это «Чистый код» Мартина, но и у Макконнелли уверен есть что-то в этом духе. Мне очень нравится раст, но с решением убрать исключения из языка я категорически не согласен.
Программа либо устанавливала флаг ошибки, либо возвращала код, который проверялся вызывающей стороной.

У обоих решений имеется общий недостаток: они загромождают код на стороне вызова. Вызывающая сторона должна проверять ошибки немедленно после вызова. К сожалению об этом легко забыть.

Кажется, автор не принимал во внимание алгебраические типы. С ними невозможно «забыть» проверить ошибки, а добавление try! (в простом случае) не особо нагромождает код:

let x = try!(foo());
bar(x);

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

Сомнительное повышение качества. Хорошо разделять системы, работающие независимо друг от друга. Здесь же — обработка ошибок непосредственно влияет на алгоритм (прерывая его), а алгоритм — на обработку ошибок (требование сохранять инварианты).
АОП было придумано затем, чтобы вообще вынести обработку ошибок чуть ли не в другой файл. Наверное, в этом есть какой-то смысл, а не деградация, не правда ли?
Кажется, автор не принимал во внимание алгебраические типы. С ними невозможно «забыть» проверить ошибки, а добавление try! (в простом случае) не особо нагромождает код:

Вполне принимал, в любом языке можно спокойно написать класс, как тот же Nullable в шарпе, и им пользоваться, с тем же успехом, только толку-то?

Например, пусть у нас есть функции A B C D E F, каждая вызывает последующую, а в F может произойти какая-то ошибка. E вызывает F, получает ошибку. Обработать эту ошибку она не может, поэтому возвращает ошибку в качестве своего возвращаемого значения. Дальше уже D не может обработать ошибку (не её уровень ответственности), она её передает дальше, и так, пока не докатиться до А, которая уже собственно и делает что-то с этой ошибкой — пишет лог, выводит мессаджбокс, что угодно.

В результате мы фактически эмулируем всё тот же стектрейс, только нам нужно это писать в каждом месте, где мы не можем обработать ошибку и вынуждены передавать её дальше, а это очень частая задача. Да, всё прописано явно, но лучше уж было бы неявно, как в других языках. Это как проверяемые исключения в джаве, задумывалось, как очень крутая штука, а получилось как всегда.
Набросал пример на шарпе. Как это будет выглядеть на расте?
void Run()
{
    try
    {
        var a = A();
        Console.WriteLine(a);
    }
    catch (ArgumentException ex)
    {
        Console.WriteLine("Something is wrong, message = {0}\tStacktrace={1}", ex.Message, ex.StackTrace);
    }
}

int A()
{
    return B();
}

int B()
{
    return C();
}

int C()
{
    return D();
}

int D()
{
    return E();
}

int E()
{
    return F();
}

int F()
{
    throw new ArgumentException();
}

то есть мы пробрасываем исключения вверх, причем ловим только ArgumentException(), остальное отправляется выше.

Раст — офигенен, но я уверен, что они в какой-то итерации вернут исключения. Без них просто каменный век какой-то, честное слово.
Я думаю, не вернут. Вот мои соображения относительно того, почему:

  • Исключения могут усложнять понимание интерфейса, особенно в большом проекте. Любой пользователь должен знать, какие исключения бросает функция. Случай по-умолчанию — это не-обработка ошибок. Т.е. если идти по пути наименьшего сопротивления, ошибка вылетит наверх и завалит приложение. С возвращаемыми значениями программист либо обрабатывает, либо явно отказывается от обработки ошибки. Я полагаю, по этой причине в Google, Parallels и других компаниях исключения C++ не используют.
  • Исключения создают много сложностей в системном программировании и в программировании на голом железе. Не забывайте, что Rust — это системный язык программирования, причём системный «как Си», а не «как Go» (последний не проектировался для работы на голом железе). ABI с исключениями не стандартизован. Требуется немаленькая система поддержки исключений времени исполнения (для каждого кадра стека есть таблица с типами исключений, которые он ловит).
Просто мне кажется, вы про каких-то идеальных программистов пишете. Со вторым пунктом я согласен, я это сразу написал, а насчет первого — во-первых вы не ответили на вопрос выше, что делать, если ошибка обработана корректно может быть только на верхнем уровне абстракции — эмулировать тот же проброс исключений? Второе, по вашему первому пункту, да, крутые бородатые программисты будут писать один раз и правильно, но большинство моих знакомых обычно не покрывают каждое возможное исключение каждой функции. То есть я понимаю, что в софте для каких-нибудь самолетов или АЭС это выполнено, но для какого-нибудь более приземленного программирования вроде какой-нибудь змейки или тетриса обычно этого не делают.

Просто именно смысл в том, что обычно ошибка передается вызывающему коду, с пояснением, что произошло. И обрабатывается на верхнем уровне, например словили эксепшн при загрузке значения в базу => отвалился метод ORM добавления в базу => отвалилась функция провайдера добавления в базу => отвалился метод класса сущности => отвалился метод коллекции => отвалился метод нажатия на кнопку «Добавить в базу» => обработали ошибку и вывели окошко «ошибка при добавлении в базу». В случае шарпа все исключения, которые функция не может обработать, она просто пробрасывает выше по-умолчанию либо явно переупаковывает в какое-то своё исключение, тут же нужно будет на каждом шаге для каждого возможного исключения писать обработчик… Либо так, либо я чего-то не понимаю.
То есть я понимаю, что в софте для каких-нибудь самолетов или АЭС это выполнено, но для какого-нибудь более приземленного программирования вроде какой-нибудь змейки или тетриса обычно этого не делают.

Ну делайте везде `.unwrap()`, можете это делать в реализации, тогда в интерфейсе вообще будет возврат чистого значения T (а не Result<T, String>, например).

Rust предполагает либо явную обработку возвращаемых значений, либо перехват паники на границе потока — можно потенциально ломающееся вычисление сделать в отдельном потоке, и узнать, завалился ли он, в родителе: doc.rust-lang.org/stable/std/thread/struct.JoinHandle.html#method.join

Ещё можно запустить замыкание в том же потоке с поимкой паники ( doc.rust-lang.org/stable/std/thread/fn.catch_panic.html ), но это сейчас нестабильно — идёт обсуждение того, как это влияет на безопасность ( github.com/rust-lang/rust/issues/25662 )

Да, исключения удобно бросать-ловить — как раз потому, что они в интерфейсе никак не декларируются. И как раз поэтому они создают трудности в больших проектах или там, где критична надёжность. Вот в Java, насколько я знаю, можно объявить, какие исключения может бросить метод.

Думаю, ничего особо нового мы тут не выведем. Да, решение Rust — это компромисс. Как и большинство решений в принципе — мало у чего нет минусов. Я думаю, что в цели Rust этот компромисс вписывается хорошо (хотя вы вольны не согласиться).
(Не хочу разводить сра полемику, но выскажусь.)

Мне кажется, следует разделять обработку ошибок и обработку отказов. Ошибки можно адекватно обработать только по месту или рядом, когда есть доступ к контексту ошибки. Об возможных ошибках стоит быть предупреждённым заранее и корректно их обрабатывать. Отказы могут случиться где угодно и адекватно обработать их можно только на высоком уровне абстракции. Контекст при этом полезен для отслеживания причины отказа, но скорее всего никак не поможет обработать возникшую ситуацию. К отказу надо быть просто готовым, чтобы изолировать его влияние на работу системы.

Ваш пример с базой данных — это как раз отказ. Функциональность кнопки совсем не работает. Всё, что можно в этом случае сделать — это вывести стектрейс (контекст) в лог, выдать пользователю окошко с отмазкой, и… всё. От того, что именно там внутри произошло, больше ничего не зависит.
А потом через пару лет вы в функции C() запускаете несколько копий D() в разных потоках…
Достаточно просто и предсказуемо.

enum Error { ArgumentError, AnotherError, OneMoreError }
impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            &Error::ArgumentError => write!(f, "ArgumentError"),
            &Error::AnotherError => write!(f, "AnotherError"),
            &Error::OneMoreError => write!(f, "OneMoreError"),
        }
    }
}

fn f() -> Result<i32, Error> {
  Err(Error::ArgumentError)
}

fn e() -> Result<i32, Error> {
  f()
}

fn d() -> Result<i32, Error> {
  e()
}

fn c() -> Result<i32, Error> {
  d()
}

fn b() -> Result<i32, Error> {
  c()
}

fn a() -> Result<i32, Error> {
  b()
}

fn main() {
  match a() {
    Ok(x) => println!("Result:{}",x),
    Err(Error::ArgumentError) => println!("Error caught"),
    Err(e) => println!("Error uncaught {}", e),
  }
}
Поиграю в адвоката дьявола.

На самом деле, это не эквивалентно, потому что во всех функциях бросаемые ошибки на самом деле могут быть разные. Тогда `f` возвращает только `ErrorF`, `e` — `ErrorF | ErrorE`, и так далее. Придётся объявить кучу типов, и они ещё и сочетаться друг с другом не будут.

Действительно неудобно.

Другое дело, что такой код на Rust вряд ли кто-то станет писать.
Согласен, что объявлять кучу типов неудобно. Но это хорошо, это заставит программиста быть аккуратнее. Программа, где не обработана хотя бы одна ошибка, просто не скомпилируется.
Подумайте об этом с другой стороны — куда проще делать везде `.unwrap()` и не париться, чем так возиться.

Если человек не хочет обрабатывать ошибки, он способ найдёт :)
На эту тему немало копий сломано, но если от кодов ошибок ушли к исключениям, то наверное это нужно?

Ну вы же понимаете, что такой аргумент транзитивно применяется в любой ситуации по мере их возникновения. Получается, всё, что старое — хуже нового :)
На эту тему немало копий сломано, но если от Python ушли к Node.js, то наверное это нужно?
Интересно, а можно ли ввести исключения опционально?
Например в С++ исключения по умолчанию включены, но можно объявить функцию со спецификатором noexcept или throw(), который указывает что фунцкия не выбрасывает исключений. Можно ли придумать обратную схему — когда для функции нужно явно указывать что она выбрасывает исключения, иначе код не компилируется?
Уже было в Java. Называется checked exceptions.
Спасибо! На первый взгляд это вроде-бы неплохо выглядит — и исключения есть, и все явно прописано в описании функций (т.е. никаких неожиданностей).
Поскольку не писал на Java — интересно, как к этой возможности относятся практикующие java-программисты? Это хорошо или плохо?
В основном стонут, потому что, утрируя, в методе-точке входа нужно прописать все исключения, которые теоретически могут возникнуть в программе, от IO и коннекшнов к базе, и заканчивая веб-сокетами и кастомными исключениями.

Что касается опциональных исключений — то это внесет путаницу, должен быть единообразный подход к обработке ошибок. Текущий дизайн раста лучше, чем если присобачить к нему исключения и сказать «пишите, как хотите».
Интересно, а можно ли ввести исключения опционально?
И рисковать в будущем поддерживать два варианта интерфейсов: с ADT и с исключениями. В язык легко что-то добавить в «хоть каком-то», в «отключаемом» виде, но потом придётся тащить бремя совместимости.

Никогда ещё плашки «небезопасно!», «не везде поддерживается!», «нестабильно!» никого не останавливали, если очень надо. В итоге что-то добавляется как эксперимент, люди всё равно это используют, а потом и выкинуть уже не получается.
А как вы себе это представляете?

Если на уровне объявления на функцию вешается атрибут типа `#![throws]`, то он же заражает все функции, вызывающие данную. Так, если у вас хотя бы одна функция в самой глубине графа вызовов бросает, то заражается по сути весь граф. Полезность аннотации и «опциональности» теряется.

Это так не только синтаксически — как только кто-то где-то бросает исключение, все, кто вызывает бросающего, должны иметь площадки для приземления чтобы вызывать `finally` и, если надо, `catch`.
Дело в том, что этот код:

    int method(int x) throws SomeException {
    }


изоморфен этому:

    fn method(x: i32) -> Result<i32, SomeException> {
    }


И там и там мы явно сообщаем компилятору, что мы либо возвращаем какое-то полезное значение, либо ошибку.
В джаве можно при этом уровнем выше спокойно написать try{method()}catch(e:SomeException){}, в расте let _ = method() и заставить компилятор замолчать, но это делается явно.
Да и ещё в джаве throws не для всех типов исключений обязателен, в отличие от раста.
В расте просто этот подход реализован за счёт мощной системы типов, а в джаве изначально система типов была намного слабее, так что пришлось придумывать новую синтаксическую структуру со throws. Только вот создатели джавы испугались идти таким путём для всех ошибок, вот и придумали разделение checked/unchecked exceptions.

Ни про какие «флаги ошибки» в расте и речи не идёт, не говоря о сишном ужасе с выставлением глобального флага ошибки в errno.
Всё явно, в самом типе указана как возможность ошибки, так и типы ошибок.
Ну и если продолжать эту аналогию, то checked exceptions в расте — это Result<T, Err>, а unchecked exceptions — это паника (panic!()).
С той разницей, что паника это вообще что-то очень-очень редкое, с ограниченным использованием, из-за чего программа полностью теряет смысл своей работы, а значит приводит к гарантированному падению потока, а Result<T, E> — обычное явление, которое компилятор заставляет хоть как-то обработать. И да, конечно можно явно Result привести к панике через unwrap(), но и в джаве это делается аналогично, только через исключения.

Я это к чему: возвращаемые из метода исключения — это по сути часть интерфейса метода, и должно кодироваться в типе, как это сделано в расте, а в джаве изначальное отсутствие дженериков это сделать не позволяло, вот и родилось полурешение с checked exceptions + throws.
Вы видите, как и что возвращают функции, и вы не можете взять результат, не проверив на потенциальные ошибки (привет, Go!).
В Гоу «panic» есть, который является эквивалентом исключений.
Нет, не является. Паника в Go — не механизм обработки ошибок, а скорее механизм обработки исключительных ситуаций (когда программа не может продолжаться так, как задумано), типа сигналов в C.
Вы бы контекст цитаты посмотрели в тексте.
Кстати, на сигналы не похоже вообще.
Пусть так, но моё утверждение от этого слабее не становится. Читаем обработку ошибок в Go, видим IO методы, возвращающие пару (результат, ошибка):

func Open(name string) (file *File, err error)
...
f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}

Насколько я вижу, ничто не запрещает использовать «f» несмотря на возникшую ошибку.
UFO just landed and posted this here
InterruptedException — такого понятия в нативных потоках нет в принципе, поэтому если оно вам нужно, то придётся делать вручную.
OutOfMemoryException — обработка этой ошибки — это прерогатива аллокатора. Дефолтный аллокатор (тот, что в libstd) сейчас паникует (или завершает программу, не помню точно — в любом случае, обработать его нельзя). В 99% случаев в прикладных программах это удовлетворительное поведение. Если вы пишите что-то близкое к железу, то там вы можете реализовать свои способы аллокации памяти (отключив libstd) и самостоятельно обрабатывать такие ошибки.
Дефолтный аллокатор (тот, что в libstd) сейчас паникует (или завершает программу, не помню точно — в любом случае, обработать его нельзя).

Завершает через `abort!()`
Если вы пишите что-то близкое к железу, то там вы можете реализовать свои способы аллокации памяти (отключив libstd) и самостоятельно обрабатывать такие ошибки.

Я бы сказал, не «можете», а «должны». Вы где-нибудь видели ОС или программы для голого железа, пользующиеся при этом стандартной библиотекой языка? :)
Rust оказывался в 5-7 раз медленнее cPython в некоторых случаях (у нас был JSON и 1-2 глобальных переменных). Не помню в чем там дело, на форумах объясняли, но в итоге мы его (Rust) использовать не стали — сыроват, очень вероятны серьезные проблемы с производительностью, возможно даже нерешаемые.
А можно ссылку на форумы или куда-нибудь ещё, где ситуация описывается? Не может быть Rust настолько медленнее. Вы в release собирали? Простите за банальный вопрос :) Никогда не слышал о нерешаемых проблемах с производительностью в Rust.
UFO just landed and posted this here
Спасибо за информацию! На всякий случай уточните, собирали ли Вы с включённой оптимизацией.

Как тестировали? Могу ли я воспроизвести данный тест у себя на компьютере?

Nickel — не единственный вариант. Вот тут можно увидеть альтернативы — arewewebyet.com
UFO just landed and posted this here
Спасибо! Надеюсь, это развеит сомнения по поводу скорости в Rust.

Имейте ввиду, что основной целью последнего чуть ли не кода разработки была стабилизация API. Оптимизация же не была приоритетна, но ей обязательно займутся сейчас, после 1.0, не нарушая интерфейсов. Уверен, у разработчиков ещё есть пара спрятаных козырей, которые позволят ещё более разогнать язык.
Дьявол, как говорят, — в деталях. Дело не в языке, а в конкретных программах, до которых ещё не добрались оптимизаторы. По факту там яблоки с грушами сравнивают. На GСС используют SSE, OpenMP и явную многозадачность. На Rust код как из учебника (740 символов на regex-dna против 2579 на С).

Единственный пример, до которого «добрались» — k-nucleotide — почему-то рвёт GCC и по скорости, и по памяти, и по размеру кода. Так что я не вижу в этих цифрах проблемы, всё образуется ;)
Я бы не упоминал этот пример. Добраться-то до него добрались, но если посмотреть на код победителя (C++), то мы опять сравниваем людоеда с бегемотом: в Rust намного больше кода и там реализована своя хэш-таблица (зачем? уж не знаю).
Давайте мы не будем ссылаться на benchmarks game, м? :)
Я бы рад, но в чем причина? По большинству запросов по сравнению производительности языков он первый в списке гугла, значит по-видимому достаточно авторитетный источник.
Например, в том, что часто сравниваемые программы реализуют разные алгоритмы.
Тогда какой сторонний источник сравнения языков существует? Или ответ просто «Надо — пиши свой бенчмарк, никто такой ерундой не занимается»?..
Мне хорошие сравнения неизвестны.
Господа из Iron (web-framework на базе http-сервера hyper) приводят вот такой тест производительности:
Iron averages 84,000+ requests per second for hello world and is mostly IO-bound, spending over 70% of its time in the kernel send-ing or recv-ing data.*

* Numbers from profiling on my OS X machine, your milage may vary.

Для понимания, какого порядка эти цифры, смотрите, например, этот тест: у них победил Haskell'ный warp с результатом 81701 запрос в секунду.

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

С цитированием тестов производительности надо быть осторожным. Нужно много контекста, чтобы делать выводы, более точные, чем «ну это цифры примерно одного порядка» — железо, остальная часть ПО, методология — всё сильно влияет.
UFO just landed and posted this here
github.com/reem/rust-event

890K запросов в секунду на довольно хилом железе. У меня на десктопном i5 второго поколения этот бенчмарк выдавал 2.7M запросов в секунду.

Понятно, что это дичайшая синтетика, но потенциал очевиден.
Там написано, что это «event loop».

Что такое «event loop» и чем это отличается от веб-сервера? От веб-фреймворка?
Сомневаюсь насчёт «нерешаемых» проблем.

Семантически код получается похожим на C++ (местами сильно проще из-за отсутствия исключений). Работает на оптимизаторе llvm. Тот же самый clang++ породит похожий код во многих случаях.
Rust, эх Rust, я в него влюбился с первого вгляда. Но стоило немного копнуть, как сразу видны некоторые детские болезни:

1) Несмотря на то, что язык уже зарелизился, до исх пор есть нестабильное АПИ. Например, vec1.push_all(vec2.as_slice()) дает сразу две ошибки о нестабильных функциях.

2) Есть ошибки в borrowing механизме, например,
while let Some(it) = iter.peek() {
println!("{}", it.next());
}
приводит к ошибке.

3) Скудная документация, например, как мне определить обобщенную функцию, которая принимает mutable peekable iteroator of chars?
fn func<I: Iterator>(iter: &mut Peekable) -> String
Так?
fn func(iter: &mut Peekable) where (I: Iterator) -> String
Или так?
Причем в документации нигде не написано про where.
1) Обещали стабильность самого языка, а не стандартной библиотеки. Как правильно, нестабильные методы являются вторичными. К примеру, вместо vec.push_all() можно использовать более универсальный vec.extend()

2) Не вижу в этом ошибки механизма. Что такое it у Вас? Если это другой итератор (тогда iter — итератор над итераторами?), то он должен быть изменяемым для вызова it.next(), в то время как peek() позволяет только подсмотреть следующее значение, но никак не поменять его.

Полагаю, на самом деле Вы хотели так:

while let Some(val) = iter.next() {
   println!("{}", val);
}


3) Смотрим на Peekable, видим структуру с параметром I: Iterator. Ваша функция, следовательно, может выглядеть так:

fn func<I: Iterator>(iter: &mut Peekable<I>) -> String

Документация описывает where и приводит ряд примеров.
1) Про это я в курсе. И про то, что extend быстрее.

2) Нет. it — это val y вас (чертова kotlin'овская привычка). Я наткнулся на это недоразумение когда писал сканнер. Стандартный способ — peek/match/next. Для исправления кода нужно использовать .by_ref(), либо loop{}, что, имхо, костыль.

3) Спасибо за ссылку!
Вы просто в примере написали println!("{}", it.next()), что явно указывает, что it — итератор. Чем всё-таки не подходит мой пример с while/next?

Стандартный способ — peek/match/next.

Это где так, в Kotlin?

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

while let Ok(msg) = receiver.try_recv() {
   println!("{}", msg);
}
Прошу прощения, там должен быть iter.next().

Не-не, scanner в смысле lexer. Например, stackoverflow.com/questions/23969191/using-the-same-iterator-multiple-times-in-rust
То есть ссылку от peek() Вы не используете? В таком случае, можно было и так:

while iter.peek().is_some() {
   println!("{}", iter.next());
}
Ну насчёт as_slice() — это вполне естественно. Это вообще по-хорошему должен быть задепрекейченный метод, вместо него Deref и slicing syntax.

Есть ошибки в borrowing механизме

Вот такой код работает:
fn main() {
    let v = vec![1, 2, 3, 4];
    let mut it = v.iter().peekable();
    while let Some(_) = it.peek() {
        println!("{:?}", it.next());
    }
}


Заметьте, здесь игнорируется результат it.peek(). Если этого не сделать, будет ошибка borrow checker'а, причём совершенно правильная — если бы он разрешил такой код, то после вызова it.next() ссылка, которую бы вернул it.peek(), могла бы стать невалидной, потому что Peekable-итератор буферизует следующий элемент внутри себя, и it.next() его бы уничтожил.
Это я понимаю. Я не понимаю, почему нельзя проверить, что невалидная ссылка _действительно_ используется? И ругаться только в этом случае? В результате, следующий говнокод без проверок и прочего не компилируется: pastebin.com/DjjVcHB5 Я попробую его причесать и прислать позже (вечером). Но думаю, основная идея понятна.
Я дополню предыдущих комментаторов.

Нестабильные API в Rust будут всегда. Это часть версионной модели языка, как в браузерах — stable, beta, nightly. Нестабильные вещи появляются в nightly и доступны только там, чтобы люди с ними экспериментировали, но не тащили в проекты раньше времени.

Учитывая это, я всё же с вами соглашусь, что многие из базовых вещей почему-то нестабильны. Но чаще всего у этого есть понятная причина (которую ещё и компилятор в ошибке приводит!), и понятна цель авторов — хотят сделать не «как везде», а «как лучше», и да, не боятся из-за этого иногда лишний раз что-то переизобрести.
Тут мне вспоминается питон с его from __future__ import feature =) Только в расте к таким вещам компилятор намного более строг.
Sign up to leave a comment.

Articles