Каждый ищет в языках программирования что-то своё: кому-то важна функциональная сторона, кому-то богатство библиотек, а другой сразу обратит внимание на длину ключевых слов. В данной статье я хотел бы рассказать, что особенно важно для меня в 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.

Комментарии (86)


  1. AMDmi3
    02.06.2015 05:08
    +5

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

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


    1. kvark Автор
      02.06.2015 05:21
      +7

      Я понимаю, о чём Вы, но не совсем разделяю. Допустим, у вас есть такой код внутри функции:

      foo(x);
      bar(y);
      

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

      Чтобы понять, что произойдёт в действительности, программисту нужно знать, какие исключения и при каких условиях бросают foo и bar, что уже совсем не очевидно.


      1. RPG18
        02.06.2015 09:53

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


      1. TimReset
        02.06.2015 09:58

        На эту тему в одной из статей на хабре про Go было обсуждение — плюсы и минусы исключений и подхода Go рассматривали.
        Конкретно по Вашему примеры функция bar(x) и не должна вызваться, т.к. до этого функция foo(x) не отработала. Если не так, то плучим неопределённое поведение. Ну плюс исключений, если нужно вот так подряд методы вызывать, не нужно каждый раз проверять, что метод вызвался корректно и если нет, то прервать вызов методов. А интересно, как в Rust такой, на мой взгляд, типичный use case реализуется — нужно вызывать подряд несколько методов, например по работе с с СУБД, и если была ошибка, то сделать rollback, а если после всех этих вызовов небыло ошибки, то commit?
        А как в Rust с управлением памятью? Есть GC?
        Вообще фишки языка мне понравились, спасало за обзор!


        1. mickey99
          02.06.2015 11:02
          +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. alexeiz
            03.06.2015 07:32
            +1

            Вот что значит отсутсвие исключений. Практически каждая строчка кода теперь должна оборачиваться в try.


            1. stack_trace
              03.06.2015 09:33

              И в чём проблема? Длинна строчки увеличится на 6 символов? Зато явно видно, какие операции могут вернуть ошибку, а какие — нет.


              1. PsyHaSTe
                03.06.2015 10:23
                +2

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


                1. ilammy
                  03.06.2015 12:30
                  +1

                  Если это настолько раздражает и делает работу невыносимой, то можно написать плагин к компилятору, который введёт какой-нибудь макрос try!!, который анализирует вложенные в него выражения и оборачивает все вызовы, возвращающие Result, в match + return.


                  1. kvark Автор
                    03.06.2015 16:33

                    Думаю, можно и без плагина обойтись. Просто try_block! как новый макрос кажется вполне возможным.


                    1. avsej
                      07.06.2015 15:22

                      а к нему ещё и catch! чтобы подключить логику очистки и восстановления после ошибки


                1. stack_trace
                  03.06.2015 21:24

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


                1. StreetStrider
                  07.06.2015 14:32

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

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


                  1. PsyHaSTe
                    08.06.2015 10:25
                    -1

                    Обычно это решается обычным полиморфизмом, ибо ошибки (я работаю с .Net, поэтому в качестве примера привожу его) просто наследуются, и например при работе с базой достаточно ловить просто какой-нибудь DbException. Ну и все ошибки отличаются по большому счету только текстом, поэтому вся обработка заключается в написании подобных блоков

                    try
                    {
                     ...
                    }
                    catch (Exception e)
                    {
                       Logger.Log(e.GetType().Name, e.Message, e.StackTrace);
                    }
                    


            1. mickey99
              03.06.2015 12:08
              +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"));
                }
              


              1. PsyHaSTe
                03.06.2015 12:35

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


                1. stack_trace
                  03.06.2015 21:21
                  +2

                  В плюсах тоже есть набор стандартных исключений.


                  1. PsyHaSTe
                    04.06.2015 11:24

                    Стандартных, потому что они просто есть в бусте каком-нибудь, или они прописаны прямо в стандарте? Потому что в той же CLR есть например параграф I.10.5 Exceptions, где черным по белому написано, что, где и почем. У плюсов я такого не видел, с радостью увижу выдержки из C++11 или C++14 стандартов.


                    1. ilmirus
                      04.06.2015 11:38
                      +1

                      Например,
                      18.8 Exception handling
                      The header [exception] defines several types and functions related to the handling of exceptions in a C++ program.


                      1. ilmirus
                        04.06.2015 11:45

                        И это я не говорю о всяких bad_* классах, которые тоже исключения и их определения раскиданы по стандарту.


                      1. PsyHaSTe
                        04.06.2015 12:06

                        Хм, спасибо за информацию.


                    1. stack_trace
                      04.06.2015 22:05
                      +2

                      Ну, собственно, вот, чтобы не быть голословным. Часть стандартной библиотеки, а значит — описаны в стандарте. Если нужна именно ссылка на стандарт, то вот здесь, параграф 18.8, страницы 461-466.


        1. Googolplex
          02.06.2015 11:10
          +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 нету, фактически, только потому, что никто пока что не потрудился его сделать. Как выяснилось на практике, почти всегда без сборки мусора можно обойтись и совершенно не потерять в удобстве.


        1. CONSTantius
          02.06.2015 18:05

          Часто любят в функционально-монадическом стиле писать что-то вроде (привет, 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()`.


      1. PsyHaSTe
        02.06.2015 18:02
        +4

        На эту тему немало копий сломано, но если от кодов ошибок ушли к исключениям, то наверное это нужно? Я просто когда смотрю километровые стектрейсы из 30-40 методов в каком-нибудь IIS не представляю, СКОЛЬКО кода добавилось бы при пробросе каждой из этих ошибок выше, тем более, что по стектрейсу можно определить, что где отвалилось, а с кодами ошибок… Мартин писал про это давным-давно, и я с каждым годом убеждаюсь в его правоте. Единственное преимущество кодов ошибок — они более быстрые, потому что разворачивание стектрейса и всё прочее с последующим захватом требуют некоторых ресурсов, не сильно затратных в случае .Net или Java, но которых желательно избежать в системном языке. Но говорить о том, что коды ошибок удобнее — это извините меня, называется.
        image


        1. TimReset
          02.06.2015 18:06
          +1

          Вот это пруфы так пруфы!
          P.S. Полностью согласен — сам работал с проектом, где использовались коды вместо Exception. Очень не удобно обрабатывать, много лишних проверок.


          1. PsyHaSTe
            02.06.2015 18:10
            +2

            В данном случае это «Чистый код» Мартина, но и у Макконнелли уверен есть что-то в этом духе. Мне очень нравится раст, но с решением убрать исключения из языка я категорически не согласен.


        1. kvark Автор
          02.06.2015 18:23
          +5

          Программа либо устанавливала флаг ошибки, либо возвращала код, который проверялся вызывающей стороной.

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

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

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

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

          Сомнительное повышение качества. Хорошо разделять системы, работающие независимо друг от друга. Здесь же — обработка ошибок непосредственно влияет на алгоритм (прерывая его), а алгоритм — на обработку ошибок (требование сохранять инварианты).


          1. PsyHaSTe
            02.06.2015 18:50
            +2

            АОП было придумано затем, чтобы вообще вынести обработку ошибок чуть ли не в другой файл. Наверное, в этом есть какой-то смысл, а не деградация, не правда ли?

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

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

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

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


            1. PsyHaSTe
              02.06.2015 18:58

              Набросал пример на шарпе. Как это будет выглядеть на расте?

              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(), остальное отправляется выше.

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


              1. CONSTantius
                02.06.2015 19:31
                +9

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

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


                1. PsyHaSTe
                  02.06.2015 20:26
                  +1

                  Просто мне кажется, вы про каких-то идеальных программистов пишете. Со вторым пунктом я согласен, я это сразу написал, а насчет первого — во-первых вы не ответили на вопрос выше, что делать, если ошибка обработана корректно может быть только на верхнем уровне абстракции — эмулировать тот же проброс исключений? Второе, по вашему первому пункту, да, крутые бородатые программисты будут писать один раз и правильно, но большинство моих знакомых обычно не покрывают каждое возможное исключение каждой функции. То есть я понимаю, что в софте для каких-нибудь самолетов или АЭС это выполнено, но для какого-нибудь более приземленного программирования вроде какой-нибудь змейки или тетриса обычно этого не делают.

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


                  1. CONSTantius
                    02.06.2015 20:54
                    +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 этот компромисс вписывается хорошо (хотя вы вольны не согласиться).


                  1. ilammy
                    02.06.2015 22:37
                    +3

                    (Не хочу разводить сра полемику, но выскажусь.)

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

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


              1. rafuck
                03.06.2015 06:09

                А потом через пару лет вы в функции C() запускаете несколько копий D() в разных потоках…


              1. mickey99
                03.06.2015 12:43

                Достаточно просто и предсказуемо.

                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. CONSTantius
                  03.06.2015 12:50
                  +1

                  Поиграю в адвоката дьявола.

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

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

                  Другое дело, что такой код на Rust вряд ли кто-то станет писать.


                  1. mickey99
                    03.06.2015 13:02

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


                    1. CONSTantius
                      03.06.2015 13:13

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

                      Если человек не хочет обрабатывать ошибки, он способ найдёт :)


        1. CONSTantius
          02.06.2015 18:34

          На эту тему немало копий сломано, но если от кодов ошибок ушли к исключениям, то наверное это нужно?

          Ну вы же понимаете, что такой аргумент транзитивно применяется в любой ситуации по мере их возникновения. Получается, всё, что старое — хуже нового :)
          На эту тему немало копий сломано, но если от Python ушли к Node.js, то наверное это нужно?


        1. NeoCode
          02.06.2015 20:28
          +1

          Интересно, а можно ли ввести исключения опционально?
          Например в С++ исключения по умолчанию включены, но можно объявить функцию со спецификатором noexcept или throw(), который указывает что фунцкия не выбрасывает исключений. Можно ли придумать обратную схему — когда для функции нужно явно указывать что она выбрасывает исключения, иначе код не компилируется?


          1. ilmirus
            02.06.2015 21:30
            +1

            Уже было в Java. Называется checked exceptions.


            1. NeoCode
              02.06.2015 21:57

              Спасибо! На первый взгляд это вроде-бы неплохо выглядит — и исключения есть, и все явно прописано в описании функций (т.е. никаких неожиданностей).
              Поскольку не писал на Java — интересно, как к этой возможности относятся практикующие java-программисты? Это хорошо или плохо?


              1. PsyHaSTe
                02.06.2015 22:26

                В основном стонут, потому что, утрируя, в методе-точке входа нужно прописать все исключения, которые теоретически могут возникнуть в программе, от IO и коннекшнов к базе, и заканчивая веб-сокетами и кастомными исключениями.

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


          1. ilammy
            02.06.2015 22:42
            +1

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

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


          1. CONSTantius
            03.06.2015 02:38
            +1

            А как вы себе это представляете?

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

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


        1. kstep
          08.06.2015 17:41
          +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.
          Всё явно, в самом типе указана как возможность ошибки, так и типы ошибок.


          1. kstep
            08.06.2015 18:36
            +3

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

            Я это к чему: возвращаемые из метода исключения — это по сути часть интерфейса метода, и должно кодироваться в типе, как это сделано в расте, а в джаве изначальное отсутствие дженериков это сделать не позволяло, вот и родилось полурешение с checked exceptions + throws.


  1. bolk
    02.06.2015 07:13

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


    1. ababo
      02.06.2015 09:45
      +1

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


      1. bolk
        02.06.2015 09:48

        Вы бы контекст цитаты посмотрели в тексте.


      1. bolk
        02.06.2015 09:53

        Кстати, на сигналы не похоже вообще.


    1. kvark Автор
      02.06.2015 18:56
      +2

      Пусть так, но моё утверждение от этого слабее не становится. Читаем обработку ошибок в Go, видим IO методы, возвращающие пару (результат, ошибка):

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

      Насколько я вижу, ничто не запрещает использовать «f» несмотря на возникшую ошибку.


  1. guai
    02.06.2015 13:20
    +2

    А как обрабатываются ситуации типа OutOfMemoryException или InterruptedException?


    1. Googolplex
      02.06.2015 16:58
      +1

      InterruptedException — такого понятия в нативных потоках нет в принципе, поэтому если оно вам нужно, то придётся делать вручную.
      OutOfMemoryException — обработка этой ошибки — это прерогатива аллокатора. Дефолтный аллокатор (тот, что в libstd) сейчас паникует (или завершает программу, не помню точно — в любом случае, обработать его нельзя). В 99% случаев в прикладных программах это удовлетворительное поведение. Если вы пишите что-то близкое к железу, то там вы можете реализовать свои способы аллокации памяти (отключив libstd) и самостоятельно обрабатывать такие ошибки.


      1. CONSTantius
        02.06.2015 18:08
        +1

        Дефолтный аллокатор (тот, что в libstd) сейчас паникует (или завершает программу, не помню точно — в любом случае, обработать его нельзя).

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

        Я бы сказал, не «можете», а «должны». Вы где-нибудь видели ОС или программы для голого железа, пользующиеся при этом стандартной библиотекой языка? :)


  1. andyN
    02.06.2015 13:24
    +1

    Rust оказывался в 5-7 раз медленнее cPython в некоторых случаях (у нас был JSON и 1-2 глобальных переменных). Не помню в чем там дело, на форумах объясняли, но в итоге мы его (Rust) использовать не стали — сыроват, очень вероятны серьезные проблемы с производительностью, возможно даже нерешаемые.


    1. kvark Автор
      02.06.2015 13:53
      +6

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


      1. deniskreshikhin
        02.06.2015 15:00
        +1

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

        Я исследовал способность http-библиотек обрабатывать запросы, и вот раст дал худший результат где-то 100 запросов в секунду (rust + nickel).

        Задача трививальная — отдать html-страницу приветсвия nginx по GET запросу.
        У меня получился такой код gist.github.com/kreshikhin/e3c0764dcbe42dd9120b

        Для других языков: node.js обработал 1500 запросов в секунду, go около 5000, на чистом си получилось около 2500 запросов в секунду (в один thread).

        Т.е. на rust+nickel получился результат в 10-50 раз худший чем на других языках.

        Может это библиотека nickel кривая, но это вроде пока что единственное что есть для создания web-серверов.


        1. kvark Автор
          02.06.2015 15:26
          +1

          Спасибо за информацию! На всякий случай уточните, собирали ли Вы с включённой оптимизацией.

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

          Nickel — не единственный вариант. Вот тут можно увидеть альтернативы — arewewebyet.com


          1. deniskreshikhin
            02.06.2015 20:54
            +2

            Попробовал воспроизвести тест, но столкнулся с тем что за год и rust, и библиотека nickel сильно изменились.
            Так что воспроизвести увы тот результат не получилось.

            На данный момент rust в релизе действительно показывает замечательные результаты (12.5K запросов в секунду), наравне с go (11.1K). Который тоже стал работать лучше, т.к. зимой вышла версия 1.4.

            Тест я выложил для вас на github: github.com/kreshikhin/htest
            Там же можно посмотреть таблицу с результатом, и конфигурацию железа.


            1. kvark Автор
              02.06.2015 22:06
              +1

              Спасибо! Надеюсь, это развеит сомнения по поводу скорости в Rust.

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


              1. PsyHaSTe
                02.06.2015 22:31

                Мне попадалось другое исследование, где раст оказывался в среднем в ~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


                1. kvark Автор
                  02.06.2015 22:59
                  +4

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

                  Единственный пример, до которого «добрались» — k-nucleotide — почему-то рвёт GCC и по скорости, и по памяти, и по размеру кода. Так что я не вижу в этих цифрах проблемы, всё образуется ;)


                  1. CONSTantius
                    03.06.2015 13:39

                    Я бы не упоминал этот пример. Добраться-то до него добрались, но если посмотреть на код победителя (C++), то мы опять сравниваем людоеда с бегемотом: в Rust намного больше кода и там реализована своя хэш-таблица (зачем? уж не знаю).


                1. CONSTantius
                  03.06.2015 02:39
                  +2

                  Давайте мы не будем ссылаться на benchmarks game, м? :)


                  1. PsyHaSTe
                    03.06.2015 10:22

                    Я бы рад, но в чем причина? По большинству запросов по сравнению производительности языков он первый в списке гугла, значит по-видимому достаточно авторитетный источник.



                    1. CONSTantius
                      03.06.2015 13:46

                      Например, в том, что часто сравниваемые программы реализуют разные алгоритмы.


                      1. PsyHaSTe
                        03.06.2015 16:07

                        Тогда какой сторонний источник сравнения языков существует? Или ответ просто «Надо — пиши свой бенчмарк, никто такой ерундой не занимается»?..


                        1. CONSTantius
                          03.06.2015 16:19

                          Мне хорошие сравнения неизвестны.


        1. CONSTantius
          02.06.2015 18:18
          +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 запрос в секунду.

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

          С цитированием тестов производительности надо быть осторожным. Нужно много контекста, чтобы делать выводы, более точные, чем «ну это цифры примерно одного порядка» — железо, остальная часть ПО, методология — всё сильно влияет.


          1. deniskreshikhin
            02.06.2015 21:09

            Спасибо, действительно круто.

            Но вот трудно их сравнить — один запускал на маке, а там может быть и i7 на 6 ядер и 12 потоков, а другой на EC2 xlarge где может быть до 36 потоков)

            Хотя вывод конечно очевиден, решения на mainstream языках плетутся где-то в хвосте.


          1. mx2000
            08.06.2015 11:06
            +1

            github.com/reem/rust-event

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

            Понятно, что это дичайшая синтетика, но потенциал очевиден.


            1. CONSTantius
              08.06.2015 14:36

              Там написано, что это «event loop».

              Что такое «event loop» и чем это отличается от веб-сервера? От веб-фреймворка?


    1. CONSTantius
      02.06.2015 18:11
      +1

      Сомневаюсь насчёт «нерешаемых» проблем.

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


  1. ilmirus
    02.06.2015 14:58
    +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.


    1. kvark Автор
      02.06.2015 15:58

      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. ilmirus
        02.06.2015 16:19

        1) Про это я в курсе. И про то, что extend быстрее.

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

        3) Спасибо за ссылку!


        1. kvark Автор
          02.06.2015 16:37

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

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

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

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

          while let Ok(msg) = receiver.try_recv() {
             println!("{}", msg);
          }
          


          1. ilmirus
            02.06.2015 16:43
            +1

            Прошу прощения, там должен быть iter.next().

            Не-не, scanner в смысле lexer. Например, stackoverflow.com/questions/23969191/using-the-same-iterator-multiple-times-in-rust


            1. kvark Автор
              02.06.2015 16:57
              +1

              То есть ссылку от peek() Вы не используете? В таком случае, можно было и так:

              while iter.peek().is_some() {
                 println!("{}", iter.next());
              }
              


    1. Googolplex
      02.06.2015 17:05

      Ну насчёт 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() его бы уничтожил.


      1. ilmirus
        02.06.2015 17:54

        Это я понимаю. Я не понимаю, почему нельзя проверить, что невалидная ссылка _действительно_ используется? И ругаться только в этом случае? В результате, следующий говнокод без проверок и прочего не компилируется: pastebin.com/DjjVcHB5 Я попробую его причесать и прислать позже (вечером). Но думаю, основная идея понятна.


    1. CONSTantius
      02.06.2015 18:23
      +2

      Я дополню предыдущих комментаторов.

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

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


      1. kstep
        08.06.2015 18:38

        Тут мне вспоминается питон с его from __future__ import feature =) Только в расте к таким вещам компилятор намного более строг.