Ржавая очевидность

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

    Особенно отмечу, что не пытаюсь полностью описать язык, а только одну его сторону. Это мой личный взгляд на философию Rust, не обязательно совпадающий с официальной позицией разработчиков! Кроме того, Rust не будет очевиден пришельцу из других языков: кривая обучение довольно резкая, и не раз компилятор заставит вас мысленно сказать «wtf» на пути к просветлению.

    Опасный код — unsafe


    «Обычный» код считается безопасным по доступу к памяти. Это пока не доказано формально, но такова задумка разработчиков. Безопасность эта не значит, что код не упадёт. Она значит, что не будет чтения чужой памяти, как и многих других вариантов неопределённого поведения. Насколько мне известно, в обычном коде всё поведение определено. Если вы попытаетесь сделать что-то незаконное, что не может быть отслежено во время сборки, то худшее, что может случиться, это контролируемое падение.

    Если же вы делаете что-то за пределами простых правил языка — вы обрамляете соответствующий хитрый код в unsafe {}. Так, например, можно найти небезопасный код в реализации примитивов синхронизации и умных счётчиков (Arc, Rc, Mutex, RwLock). Заметьте, что это не делает данные элементы опасными, ибо они выставляют наружу совершенно безопасный (с точки зрения Rust) интерфейс:

    // в этом примере наш объект владеет GL контекстом и гарантирует,
    // что вызовы к нему идут только из родительского потока
    fn clear(&self) {
        unsafe { self.gl.clear(gl::COLOR_BUFFER_BIT) }
    }
    

    Итак, если вам на глаза попалась функция с блоком unsafe, нужно внимательно присмотреться к содержимому. Если нет — будьте спокойны, поведение функции строго определено (нет undefined behavior). Пользуясь случаем… привет, С++!

    Исключения, которых нет


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

    Вот и автор ZeroMQ решил, что эта сложность только мешает, и разработчики Rust с ним согласны. У нас нет исключений, а потенциальные ошибки являются частью возвращаемых (алгебраических) типов:

    fn foo() -> Result<Something, SomeError>;
    ...
    match foo() {
       Ok(t) => (...), //успех!
       Err(e) => (...), //ошибка!
    }
    

    Вы видите, как и что возвращают функции, и вы не можете взять результат, не проверив на потенциальные ошибки (привет, Go!).

    Ограниченный вывод типов


    Глядя на функцию в Python, я далеко не всегда понимаю, что она делает. Попадаются, конечно, хорошие имена и названия параметров, но это ничем не проверяется и не гарантируется. Это свойство позволяет языку быть компактным и акцентировать внимание на действиях, а не данных.

    # комментарии, конечно, помогают, но типы были бы надёжнее
    def save_mesh(out, ob, log): # -> (Mesh, bounds, faces_per_mat):
        ... # 50 строк на питоне без единого типа внутри
    

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

    Локальные переменные


    Это свойство кажется таким простым и очевидным… пока не появляются ребята из Oberon (привет!). У глобальных переменных есть положительные моменты, но они затрудняют понимание фрагментов кода.

    // entity - локальная переменная, указатель на текущий элемент коллекции
    for entity in scene.entities {
        // её область жизни - данный цикл и не строчки больше
       entity.draw()
    }
    


    Неизменяемые переменные


    Ах да, переменные по умолчанию нельзя изменить дважды. Хотите менять после инициализации — будьте добры указать слово mut. При этом компилятор гарантирует это свойство:

    fn foo(x: &u32) -> u32 {
       ... // мы знаем, что переменная не поменялась, и компилятор знает
       *x + 1 
    }
    

    Или с изменяемым состоянием:

    fn foo(x: &mut u32) {
      ... // мы сознательно меняем значение, но мы знаем, как и компилятор,
      // что никто другой не меняет и даже не читает его, пока мы здесь
     *x = *x + 1;
    }
    

    Сравните это с const в C:

    unsigned foo(const unsigned *const x) {
       ... // мы может и знаем, что переменная не поменялась в теле этой функции
      // но ничто не мешает менять её в другом потоке, так что компилятор ничего не знает
      return *x + 1;
    }
    


    Указатели, которых вы не увидите


    В Rust есть указатели, но они применяются сугубо в малочисленных кусках опасного кода. Причина тому — разыменование любого указателя есть небезопасная операция. Пользовательский код обильно построен на ссылках. Ссылка на объект гарантирует его существование, пока она жива. Так что в Rust нет проблемы нулевых указателей.

    Конечно, я слышу громкие возгласы, что нулевые указатели просто несут смысл несуществующего объекта, который мы в Rust всё равно так или иначе выражаем со всеми вытекающими логическими ошибками. Да, есть Option<&Something>, однако это не совсем то же самое. С точки зрения Rust, ваш код, скажем на Java, изобилует указателями, которые могут в один прекрасный момент упасть при доступе. Вы может и знаете, какие из них не могут быть нулевыми, но держите это в голове. Ваш коллега не может читать ваши мысли, да и компилятор не способен уберечь вас самих от провала памяти.

    В Rust семантика отсутствующего объекта очевидна: она явна в коде, и компилятор обязывает вас (и вашего коллегу) проверить существование объекта при доступе. Большинство же объектов, с которыми мы имеем дело, передаются по простым ссылкам, их существование гарантированно:

    fn get_count(input: Option<&str>) -> usize {
        match input {
            Some(s) => s.len(),
            None => 0,
        }
    }
    

    Конечно, вы всё также можете упасть на месте, где ожидаете чего-то, чего нет. Но падение это будет осознанным (через вызов unwrap() или expect()) и явным.

    Модули


    Всё, что в области видимости, можно найти по местным объявлениям и ключевому слову use. Расширять область видимости можно прямо в блоках кода, что ещё более усиливает локальность:

    fn myswap(x: &mut i8, y: &mut i8) {
        use std::mem::swap;
        swap(x, y);
    }
    

    Проблема по существу есть только в C и С++, но там она весьма доставляет. Как понять, что именно в области видимости? Нужно проверить текущий файл, потом все включаемые файлы, потом все их включаемые, и так далее.

    Композиция вместо наследования


    В Rust нет наследования классов. Вы можете наследовать интерфейсы (traits), но структуры всё равно должны явно реализовывать все интерфейсы, которые унаследовал нужный вам интерфейс. Допустим, вы видите вызов метода object.foo(). Какой именно код будет исполнен? В языках с наследованием (особенно — множественным), вам нужно поискать данный метод в классе типа object, потом в его родительских классах, и так далее — пока не найдёте реализацию.

    Наследование — мощное оружие, позволяющее добиться красивого полиморфизма в огромном количестве задач. В Rust до сих пор не утихают споры, как получить нечто похожее, при этом сохранив красоту языка. Однако я убеждён, что оно затрудняет понимание кода.

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

    Явная реализация обобщений


    Отдельно хочется отметить момент, что для удовлетворения определённого интерфейса, его нужно явно указать:

    impl SomeTrait for MyStruct {...}
    

    Делать это можно там, где объявлен интерфейс либо целевая структура, но не в произвольном месте. Привет, Go, где царит магия неявных реализаций. Нет, концепция в Go очень красивая и оригинальная, я не спорю, но вот очевидность происходящего я бы поставил под сомнение.

    Обобщённые ограничения


    Шаблоны в С++ — это, как ни странно, элементы мета-программирования. Этакие повзрослевшие макросы, полные по Тьюрингу. Они позволяют сэкономить кучу кода и творить настоящие чудеса (привет, Boost!). Однако, сказать, что конкретно случится в момент подстановки конкретного типа — трудно. Какие требования к подставляемому типу — тоже не сразу понятно.

    В Rust (и во многих других языках) вместо шаблонов есть обобщения. Их параметры обязаны предоставлять определённый интерфейс для подстановки, и корректность таких обобщений проверяется достоверно компилятором:

    // входные параметры должны быть сравнимы друг с другом
    pub fn max<T: Ord>(v1: T, v2: T) -> T
    

    Стоит отметить, что комитет признаёт важность концепции, так что скоро мы можем увидеть нечто подобное и в С++.

    Неспециальные обобщения


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

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

    Rust — не тёмная магия: он не оживляет мертвецов и не превращает воду в вино. Точно также, он не решает все проблемы нашего ремесла. Однако, он заставляет нас думать и писать код таким образом, что потенциальные проблемы оказываются на поверхности. В каком-то смысле, Rust искривляет реальность программирования, позволяя нам легче передвигаться в ней, как warp-drive.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 86

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

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

        foo(x);
        bar(y);
        

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

        Чтобы понять, что произойдёт в действительности, программисту нужно знать, какие исключения и при каких условиях бросают foo и bar, что уже совсем не очевидно.
          0
          Если код был разработан с учетом использования исключений, то описанная вами ситуация не может быть. А если иначе, то это некорректный код.
            0
            На эту тему в одной из статей на хабре про Go было обсуждение — плюсы и минусы исключений и подхода Go рассматривали.
            Конкретно по Вашему примеры функция bar(x) и не должна вызваться, т.к. до этого функция foo(x) не отработала. Если не так, то плучим неопределённое поведение. Ну плюс исключений, если нужно вот так подряд методы вызывать, не нужно каждый раз проверять, что метод вызвался корректно и если нет, то прервать вызов методов. А интересно, как в Rust такой, на мой взгляд, типичный use case реализуется — нужно вызывать подряд несколько методов, например по работе с с СУБД, и если была ошибка, то сделать rollback, а если после всех этих вызовов небыло ошибки, то commit?
            А как в Rust с управлением памятью? Есть GC?
            Вообще фишки языка мне понравились, спасало за обзор!
              +2
              Для вызова нескольких методов подряд используют ранний возврат с помощью макро 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).
                +1
                Вот что значит отсутсвие исключений. Практически каждая строчка кода теперь должна оборачиваться в try.
                  0
                  И в чём проблема? Длинна строчки увеличится на 6 символов? Зато явно видно, какие операции могут вернуть ошибку, а какие — нет.
                    +2
                    Вместо того, чтобы отметить регион, где происходит ошибка, нужно оборачивать каждый метод отдельно. Хотя это, конечно, дисциплинирует, чтобы не писать try catch во всё тело метода, но все же выглядит довольно странно, мягко говоря.
                      +1
                      Если это настолько раздражает и делает работу невыносимой, то можно написать плагин к компилятору, который введёт какой-нибудь макрос try!!, который анализирует вложенные в него выражения и оборачивает все вызовы, возвращающие Result, в match + return.
                        0
                        Думаю, можно и без плагина обойтись. Просто try_block! как новый макрос кажется вполне возможным.
                          0
                          а к нему ещё и catch! чтобы подключить логику очистки и восстановления после ошибки
                        0
                        Проблема в том, что с исключениями вы не будете знать, происходит ли в этом регионе ошибка и если происходит то какая. Те же ошибки, связанные с выделением памяти в плюсах может выкинуть вообще любая функция. То есть если исключения есть, то нужно ожидать, что всё бросает исключения.
                          0
                          Регион? Ну так-то да, регион, но ошибка из одного места региона не равна ошибке из другого места региона. Тут сказывается не столько формальная сила типизации языка, сколько реальная типизация ошибок, обусловленная предметной областью задачи. Если ошибки произошли в разных местах этого региона, то и источники у них разные, и корректная их обработка тоже будет различаться. Какой смысл остаётся от региона, если все захваченные им ошибки всё равно потом нужно распределять по обработчикам?

                          Если я не прав, то в качестве контрпримера приведите ситуацию, когда мы можем создать регион, в двух разных точках которого потенциально возможно получение ошибок одинакового типа (с формальной точки зрения) и главное — одинаковую обработку этих ошибок (фактическая типизация предметной области).
                            –1
                            Обычно это решается обычным полиморфизмом, ибо ошибки (я работаю с .Net, поэтому в качестве примера привожу его) просто наследуются, и например при работе с базой достаточно ловить просто какой-нибудь DbException. Ну и все ошибки отличаются по большому счету только текстом, поэтому вся обработка заключается в написании подобных блоков
                            try
                            {
                             ...
                            }
                            catch (Exception e)
                            {
                               Logger.Log(e.GetType().Name, e.Message, e.StackTrace);
                            }
                            
                        +1
                        Ну, не каждая строчка вообще, а только каждая потенциально опасная строчка.
                        На мой взгляд, это вполне приемлемая цена за отсутствие исключений.
                          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"));
                          }
                        
                          0
                          Ну это плюсовая проблема стандарта, а не проблема исключений. В той же BCL, да и джаве, есть набор стандартных эксепшнов, которые прописаны ажно в стандарте (ECMA335).
                            +2
                            В плюсах тоже есть набор стандартных исключений.
                              0
                              Стандартных, потому что они просто есть в бусте каком-нибудь, или они прописаны прямо в стандарте? Потому что в той же CLR есть например параграф I.10.5 Exceptions, где черным по белому написано, что, где и почем. У плюсов я такого не видел, с радостью увижу выдержки из C++11 или C++14 стандартов.
                                +1
                                Например,
                                18.8 Exception handling
                                The header [exception] defines several types and functions related to the handling of exceptions in a C++ program.
                                  0
                                  И это я не говорю о всяких bad_* классах, которые тоже исключения и их определения раскиданы по стандарту.
                                    0
                                    Хм, спасибо за информацию.
                                    +2
                                    Ну, собственно, вот, чтобы не быть голословным. Часть стандартной библиотеки, а значит — описаны в стандарте. Если нужна именно ссылка на стандарт, то вот здесь, параграф 18.8, страницы 461-466.
                          +3
                          В зависимости от 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 нету, фактически, только потому, что никто пока что не потрудился его сделать. Как выяснилось на практике, почти всегда без сборки мусора можно обойтись и совершенно не потерять в удобстве.
                            0
                            Часто любят в функционально-монадическом стиле писать что-то вроде (привет, 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()`.
                            +4
                            На эту тему немало копий сломано, но если от кодов ошибок ушли к исключениям, то наверное это нужно? Я просто когда смотрю километровые стектрейсы из 30-40 методов в каком-нибудь IIS не представляю, СКОЛЬКО кода добавилось бы при пробросе каждой из этих ошибок выше, тем более, что по стектрейсу можно определить, что где отвалилось, а с кодами ошибок… Мартин писал про это давным-давно, и я с каждым годом убеждаюсь в его правоте. Единственное преимущество кодов ошибок — они более быстрые, потому что разворачивание стектрейса и всё прочее с последующим захватом требуют некоторых ресурсов, не сильно затратных в случае .Net или Java, но которых желательно избежать в системном языке. Но говорить о том, что коды ошибок удобнее — это извините меня, называется.
                            image
                              +1
                              Вот это пруфы так пруфы!
                              P.S. Полностью согласен — сам работал с проектом, где использовались коды вместо Exception. Очень не удобно обрабатывать, много лишних проверок.
                                +2
                                В данном случае это «Чистый код» Мартина, но и у Макконнелли уверен есть что-то в этом духе. Мне очень нравится раст, но с решением убрать исключения из языка я категорически не согласен.
                                +5
                                Программа либо устанавливала флаг ошибки, либо возвращала код, который проверялся вызывающей стороной.

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

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

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

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

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

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

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

                                  В результате мы фактически эмулируем всё тот же стектрейс, только нам нужно это писать в каждом месте, где мы не можем обработать ошибку и вынуждены передавать её дальше, а это очень частая задача. Да, всё прописано явно, но лучше уж было бы неявно, как в других языках. Это как проверяемые исключения в джаве, задумывалось, как очень крутая штука, а получилось как всегда.
                                    0
                                    Набросал пример на шарпе. Как это будет выглядеть на расте?
                                    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(), остальное отправляется выше.

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

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

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

                                          Ну делайте везде `.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 этот компромисс вписывается хорошо (хотя вы вольны не согласиться).
                                            +3
                                            (Не хочу разводить сра полемику, но выскажусь.)

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

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

                                            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),
                                              }
                                            }
                                            
                                              +1
                                              Поиграю в адвоката дьявола.

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

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

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

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

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

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

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

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

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

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

                                                  Я это к чему: возвращаемые из метода исключения — это по сути часть интерфейса метода, и должно кодироваться в типе, как это сделано в расте, а в джаве изначальное отсутствие дженериков это сделать не позволяло, вот и родилось полурешение с checked exceptions + throws.
                                            0
                                            Вы видите, как и что возвращают функции, и вы не можете взять результат, не проверив на потенциальные ошибки (привет, Go!).
                                            В Гоу «panic» есть, который является эквивалентом исключений.
                                              +1
                                              Нет, не является. Паника в Go — не механизм обработки ошибок, а скорее механизм обработки исключительных ситуаций (когда программа не может продолжаться так, как задумано), типа сигналов в C.
                                                0
                                                Вы бы контекст цитаты посмотрели в тексте.
                                                  0
                                                  Кстати, на сигналы не похоже вообще.
                                                  +2
                                                  Пусть так, но моё утверждение от этого слабее не становится. Читаем обработку ошибок в 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
                                                    +1
                                                    InterruptedException — такого понятия в нативных потоках нет в принципе, поэтому если оно вам нужно, то придётся делать вручную.
                                                    OutOfMemoryException — обработка этой ошибки — это прерогатива аллокатора. Дефолтный аллокатор (тот, что в libstd) сейчас паникует (или завершает программу, не помню точно — в любом случае, обработать его нельзя). В 99% случаев в прикладных программах это удовлетворительное поведение. Если вы пишите что-то близкое к железу, то там вы можете реализовать свои способы аллокации памяти (отключив libstd) и самостоятельно обрабатывать такие ошибки.
                                                      +1
                                                      Дефолтный аллокатор (тот, что в libstd) сейчас паникует (или завершает программу, не помню точно — в любом случае, обработать его нельзя).

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

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

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

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

                                                              Имейте ввиду, что основной целью последнего чуть ли не кода разработки была стабилизация API. Оптимизация же не была приоритетна, но ей обязательно займутся сейчас, после 1.0, не нарушая интерфейсов. Уверен, у разработчиков ещё есть пара спрятаных козырей, которые позволят ещё более разогнать язык.
                                                                0
                                                                Мне попадалось другое исследование, где раст оказывался в среднем в ~2.5-3 раза медлнее С

                                                                benchmarksgame.alioth.debian.org/u64q/rust.html
                                                                benchmarksgame.alioth.debian.org/u64/benchmark.php?test=fasta&lang=all&id=1&data=u64
                                                                  +4
                                                                  Дьявол, как говорят, — в деталях. Дело не в языке, а в конкретных программах, до которых ещё не добрались оптимизаторы. По факту там яблоки с грушами сравнивают. На GСС используют SSE, OpenMP и явную многозадачность. На Rust код как из учебника (740 символов на regex-dna против 2579 на С).

                                                                  Единственный пример, до которого «добрались» — k-nucleotide — почему-то рвёт GCC и по скорости, и по памяти, и по размеру кода. Так что я не вижу в этих цифрах проблемы, всё образуется ;)
                                                                    0
                                                                    Я бы не упоминал этот пример. Добраться-то до него добрались, но если посмотреть на код победителя (C++), то мы опять сравниваем людоеда с бегемотом: в Rust намного больше кода и там реализована своя хэш-таблица (зачем? уж не знаю).
                                                                    +2
                                                                    Давайте мы не будем ссылаться на benchmarks game, м? :)
                                                                      0
                                                                      Я бы рад, но в чем причина? По большинству запросов по сравнению производительности языков он первый в списке гугла, значит по-видимому достаточно авторитетный источник.
                                                                          0
                                                                          Например, в том, что часто сравниваемые программы реализуют разные алгоритмы.
                                                                            0
                                                                            Тогда какой сторонний источник сравнения языков существует? Или ответ просто «Надо — пиши свой бенчмарк, никто такой ерундой не занимается»?..
                                                                              0
                                                                              Мне хорошие сравнения неизвестны.
                                                                +1
                                                                Господа из 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
                                                                    +1
                                                                    github.com/reem/rust-event

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

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

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

                                                                Семантически код получается похожим на C++ (местами сильно проще из-за отсутствия исключений). Работает на оптимизаторе llvm. Тот же самый clang++ породит похожий код во многих случаях.
                                                                +1
                                                                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.
                                                                  0
                                                                  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 и приводит ряд примеров.
                                                                    0
                                                                    1) Про это я в курсе. И про то, что extend быстрее.

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

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

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

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

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

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

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

                                                                          while iter.peek().is_some() {
                                                                             println!("{}", iter.next());
                                                                          }
                                                                          
                                                                    0
                                                                    Ну насчёт 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() его бы уничтожил.
                                                                      0
                                                                      Это я понимаю. Я не понимаю, почему нельзя проверить, что невалидная ссылка _действительно_ используется? И ругаться только в этом случае? В результате, следующий говнокод без проверок и прочего не компилируется: pastebin.com/DjjVcHB5 Я попробую его причесать и прислать позже (вечером). Но думаю, основная идея понятна.
                                                                      +2
                                                                      Я дополню предыдущих комментаторов.

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

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

                                                                    Only users with full accounts can post comments. Log in, please.