Pull to refresh

Comments 74

Таки-меня коробит, что для такой важной и распространенной вещи как обработка ошибок по всему коду используется макрос, который еще и меняет поток выполнения. Не то что бы я видел в этом какой-то вот прямо практический вред, нооо как-то криво это. Ожидаю рано или поздно какого-то обобщенного решения интегрированного в сам язык, вроде же были какие-то rfc.
RFC есть (https://github.com/rust-lang/rfcs/pull/243), но оно не движется.
Автор не хотел вставлять неявный вызов `Into::into` в `?`, человек из core team с ним не согласился, так всё пока и заглохло, потому что особо и не горит. Но рано или поздно будет, можно их попинать в комментах для ускорения.
Коробит, потому что непривычно? Макросы в Rust реализованы достаточно неплохо для того, чтобы быть хорошим инструментом для решения более-менее повседневных задач, не вызывая при этом боли, которая может возникнуть например в C++. Тем более, обработка ошибок через try! получается гораздо короче, чем использование try-catch блока в тех же сценариях.

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

try! спасает только то, что он всего один, достаточно прост и быстро узнается в процессе изучения языка.
Я думаю, именно потому, что вызовы макроса могут вести себя не так тривиально, как вызовы функции, они обязательно требуют специального обозначения вроде foo!. Это что-то вроде «будь внимателен, здесь происходит что-то необычное».

К счастью, макросы, которые регулярно используются в коде можно пересчитать на пальцах:

try!, println!, format!, vec!, assert! и assert_eq!

Наверное можно было бы расширить синтаксис языка специльно для исключений, но такое расширение все-равно будет следовать логике макроса try. Например, что-то вроде foo(try bar()). Для остальных случаев есть более универсальная конструкция match, которая уже и так умеет все то, что в обычных языках умеет try-catch блок. Единственное отличие – match работает для каждого конкретного вызова, в то время как try-catch может оборачивать целый блок кода. Но я бы поспорил о том, что это недостаток для языка программирования, который делает ставку на надежность.
Ну как, я относительно давно на ржавчине пытаюсь писать, так что дело не просто в непривычности.

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

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

Повторюсь, каких-то жутких практических недостатков в данном конкретном случае я не вижу — согласен с Михаилом, что ситуацию спасает одиночество `try!`. Но мне было бы комфортнее, если бы вышеуказанный RFC довели до ума и реализовали, пускай оно даже один в один как `try!` работало бы. Или даже что бы таки реализовали HKT и ввели в язык do-нотацию :).
А как код выглядел бы с do-нотацией? Не получится ли слишком громоздко?
Полагаю как-то так: ideone.com/DlGYHp со строки 32.
Всё, что выше — просто чтоб компилировалось.
В общем-то да, примерно то же самое.
> А как код выглядел бы с do-нотацией?

Сейчас есть накостыленый на макросах mdo! — https://github.com/TeXitoi/rust-mdo — лучше без HTK особо не сделаешь. С ним как-то так выходит:

fn write_to_file_using_try() -> io::Result<()> {
    let mut file = try!(File::create("my_best_friends.txt"));
    try!(file.write_all(b"This is a list of my best friends."));
    println!("I wrote to the file");
    Ok(())
}

fn write_to_file_using_mdo() -> io::Result<()> {
    mdo! {
        mut file =<< File::create("my_best_friends.txt");
        ign file.write_all(b"This is a list of my best friends.");
        ign Ok(println!("I wrote to the file"));
        ret Ok(())
    }
}


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

> Не получится ли слишком громоздко?

Может, от конкретного кода зависит. Функции, возвращающие чего-то левое (как println! в примере), которые не выходит вынести за рамки do, конечно, придется «втягивать».

Если что, я на знатока функциональных буррито не претендую, но идея кажется заслуживающей проработки)
Я Все хотел задать вопрос, Вот Rust И GO, высоконагруженные платформы, А есть ли succes story от их использования,
Я например хочу написать веб приложение, что лучше использовать ??(Best practices)? у Го и Rust есть предпосылки для роста, это не NodeJs со своей лапшей (Callbacki)
New York Times вроде как переписали кучу всего на Go
https://www.youtube.com/watch?v=bAQ9ShmXYLY
Можно здесь подсмотреть github.com/golang/go/wiki/GoUsers
То есть, Go используется дофига где. Насчёт применения Rust в продакшене пока рано говорить.
Кроме OpenDNS чего не слышал. И то обвязочка для сишной либы, вы серьёзно называете это продакшеном?
Логика на уровне «я не слышал, значит этого нет». А я слышал про Skylight и MaidSafe, и чего, кто теперь прав?

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

У MaidSafe всё ядро распределённой сети на Rust (14000 строк).

Redox — это активно разрабатываемая ОС на Rust. 40000 строк кода на Rust. И пока вы не возразили, что этим «никто не пользуется» или «вы про это не слышали» — вспомните, как начинался Linux.
Логика на уровне, что в списке про Go присутствует индустрия. А ваш список — карта песочниц в округе.

Его размер здесь не важен, но если хотите, то вот цифры (это честные SLOC, без пробелов и комментариев)
Вообще говоря важен, особенно не в абсолютных цифрах, а в степени вовлечённости в архитектуре. MaidSafe ещё не релизнулся, вы не компетентны если называете opens-source поделку до первого RC «продакшеном». 14000 LOC это смешно, у меня хобби-проект на Go за полтора месяца вышел на 8000. 14000 с учётом специфики задач это уровень концепта.

Redox — это активно разрабатываемая ОС
Я не буду вас ловить и проверять, насколько активно и какие там люди (больше, чем полтора ботана, надеюсь). По моему опыту разработки модулей и драйверов Linux — раст там вообще ничем не поможет. А до продакшена этой поделке лет 5 в лучшем случае, 20 в обычном.

Вы поймите, продакшен это коммерчески успешные проекты, когда люди за свои ошибки отвечают жопами, а код прибывает по 10к в неделю минимум. Когда можно набрать 5 человек с улицы и вовлечь в проект меньше, чем за полгода. Ваш список это хороший индикатор, но вы оффтопите в этом треде про продкшен.
На Skylight-то посмотрите, прежде чем обвинять в некомпетентности.
А что там? Главный разработчик растовых хаков для Skylight — Yehuda Katz, который и написал эти две статьи в блоге про то, какой Rust офигенный, он же входит в rust core team. Думаете, я случайно про «полтора ботана» пошутил?
Деньги зарабатывают? Зарабатывают. Нормально для языка, который стабилизовался вот ещё меньше, чем полгода назад.
больше, чем полтора ботана, надеюсь
Линус вон вообще один начинал, в чём проблема-то?

Да и, что меня ловить-то, я и сам поймаюсь.

Redox SLOCs


А code churn на уровне 200 000 — 2 000 000 строк это уже извините меня не «поделка», как ни крути.

А до продакшена этой поделке лет 5 в лучшем случае, 20 в обычном.
Сколько лет пройдёт до того, как махровая индустрия возьмёт на вооружение Rust обсуждать есть смысл только с учётом того, что Go появился на 3 года раньше Rust. Прямые сравнения неуместны.
Ребят, ну чего вы как индусы числом строчек меритесь? :-)
продакшен это коммерчески успешные проекты, когда люди за свои ошибки отвечают жопами, а код прибывает по 10к в неделю минимум.


Можно узнать источник этого определения?
То есть, к определению
боевой код, который работает в реальных условиях — это production
товарища mkpankov у вас вопросов нет? Или вы согласны, что он неадекват и интересуетесь именно этим определением? Что-ж почитайте вот jdevelop.blogspot.ru/2013/03/blog-post.html
Нехорошо людей за глаза неадекватами называть.
Практически нет вопросов. Можно уточнять что такое «боевой», что такое «реальные условия», но в целом вопросов нет.
Я например хочу написать веб приложение, что лучше использовать
Ruby on Rails, потому что быстрее и проще, а узкие места переписать на Go всегда успеете. Всё равно их переписывать придётся.
Best practices для Rust пока нет.

История успеха — это Servo. Он рисует страницы в 2 раза быстрее Gecko в одном потоке и ещё быстрее в многопоточном режиме.

А из коммерческих пользователей есть как минимум Maidsafe, Skylight, OpenDNS. У них в разных местах есть посты о том, почему они выбрали Раст.
История успеха — это Servo.

Заинтересовал, погуглил. Наткнулся многократно на то, что его пока не собираются использовать в Firefox. Задумался ещё больше. Ведь разработка такой большой вещи занятие весьма эпохальное (это работа не для 5 программистов). С какой целью тогда они его делают?
Не совсем так.

Вот план реализации Servo: github.com/servo/servo/wiki/Roadmap. Моё понимание таково: Servo пока просто не готов к боевому применению. Хотя некоторые сайты уже отрисовываются так же хорошо, как и в Gecko.

Насколько я помню, в целом Mozilla действует так: постепенно заменяют компоненты в Firefox на те, что используется в Servo, так что в конце они будут использовать одно и то же, а затем можно и движок заменить.
В нём уже все спецификации реализовали или только те, что можно быстро нарисовать? :-)
это не NodeJs со своей лапшей (Callbacki)

Лапша у вас на ушах, уважаемый. Вы внимательно погуглите магические 'javascript promise', 'javascript async/await', 'javascript es2015', чтобы глупостей не говорить.
Это конечно все хорошо, просто отладка осложняется, и код не очевиден, Я хотел уже осваивать Node.JS но потом увидел что на код надо смотреть не линейно а сначала код и заглушка(Callback) которая после нее. Конечно я понимаю таково преимущество Асинхронности, может быть проблема старого поколения что на код они смотрят линейно ???
javascript es2015

Его V8 уже поддерживает стабильно?
К сожалению очень сложно представить выгоду от всех этих сложностей языка в реальных проектах. Там где нужен жесткий real time или крайне стабильный драйвера Rust наверно и крут. Но вот касательно всех других проектов — думаю для массового рынка он будет слишком невыгоден скоростью разработки.
Почему-то все очень любят оценивать только скорость разработки, забывая про скорость отладки. И проблема в том, что эти две величины связаны между собой не так просто, как хотелось бы: почти любой многопоточный алгоритм значительно сложнее отлаживать, чем идентичный ему однопоточный, при этом оба могут быть записаны в одну строку (а-ля records.map(processFn) и records.parallelMap(processFn)). В языках без GC к этому добавляются проблемы использования объектов после разрушения и т.п.

Баги в программе — это ещё и удар по репутации. Уж лучше потратить в два раза больше времени на написание кода, чем написать код в два раза быстрее, а потом столько же его отлаживать (ну, это как повезёт), вдобавок теряя клиентов и получая негативные отзывы.
Уж лучше потратить в два раза больше времени на написание кода, чем написать код в два раза быстрее, а потом столько же его отлаживать (ну, это как повезёт), вдобавок теряя клиентов и получая негативные отзывы.


Нельзя сказать однозначно. Как минимум, отлаживать нужно не всегда.
Вам никто ведь не мешает делать unwrap везде, а затем постепенно рефакторить. Стабильность теряется разумеется.
Распространенность в стандартной библиотеке возвратов структуры Result вместе с рекомендованным способом его обработки через макрос try! (который по сути пробрасывает ошибку вверх по стеку вызовов) весьма напоминает эмуляцию throw exception из Java и подобных языков.
Нечто вроде эмуляции. Эксепшены более тяжеловесны, так как в них (как минимум) сохраняется весь stack trace. (Про другие причины их тяжеловесности в разных языках я когда-то читал, но уже многое подзабыл, и ссылку на статью найти не могу).
Похожая эмуляция исключений, кстати, недавно появилась в swift 2.0, и меня это очень сильно порадовало. Все же не очень удобно и читабельно плодить кучу if-ов для проверки ошибок после вызова каждой функции. Но решение в Rust мне видится более гибким, так как можно делать всякие функциональные чудеса с помощью map, map_err и проч., полностью отказавшись от try! при необходимости.
Порадовало отсутствие стектрейса? Вы таки мазохист?
Можно автору статьи несколько вопросов?

Какой размер имеет в Rust экземпляр типа i32? В Си i32 – элементарный тип. Скорее всего в Rust i32 тоже элементарный, т.е. не класс. Поэтому его размер, скорее всего, 4 байта. И какой размер имеют экземпляры типов Option и Result<i32, E>? И ещё вопрос – указатель в Rust является элементарным типом или нет? И каков его размер?

Как обработать в Rust возникающие при сложении переполнения, т.е. ошибки?
Автор статьи вам скорее всего не ответит, поскольку не читает Хабр. Отвечу я как переводчик.

i32 – элеменентарный тип, его размер 4 байта. В Rust вообще нет понятия класса.
Размер Option<T> равен размеру T + дискриминант (обычно 1 байт) + выравнивание.
Размер Option<&T> равен размеру указателя для вашей архитектуры. Дискриминант не нужен, поскольку значение None кодируется как нулевой указатель. В Rust нет нулевых указателей, так что сам &T никогда не будет равен null.
Размер Result<T, E> равен размеру дискриминанта (1 байт) + размеру наибольшего возможного значения (или размер T, или размер E) + выравнивание.

Арифметические операции по умолчанию игнорируют переполнение, но если вам нужно их обрабатывать, можно воспользоваться методами checked_add, checked_mul и т.д. В случае с i32 они возвращают результат Option<i32>.
Простите, я вас обманул по-поводу переполнения:

Обычное сложение 150u8 + 150u8 паникует в случае переполнения (и корректно завершает поток).
Метод 150u8.checked_add(150u8) возвращает Option<T> со значением None.
Метод 150u8.saturating_add(150u8) возвращает граничное значение (255).
Метод 150u8.wrapping_add(150u8) игнорирует переполнение (44).
На данный момент, если не ошибаюсь, паникует только в отладочной сборке. Это может быть важно)
Да, точно, для release оператор + ведет себя аналогично wrapping_add.
Если Option имеет дискриминант (или дескриптор, т.е. описание? В языке Алгол-68 применялся термин «паспорт», этот паспорт дополнительно описывал объекты), то из функции нельзя вернуть Option в 32-разрядном регистре. Некоторая потеря эффективности в пользу надёжности.
Установил Rust себе на Windows XP – не работает. Потом узнал, что эта платформа не поддерживается. Тогда поставил на 32-разрядную Windows 7. Компилятор сообщает о синтаксической ошибке на «hello world», взятый из учебника. Наверно, нужен Linux… Компилятор Rust выдаёт не исполняемый код, а код в LLVM IR. А под Windows единственный инструмент, который превращает код LLVM IR в исполняемый – Visual C++. Как-то не по фэншую – сочетание свободного ПО и проприетарного… Или я неправильно информирован?
> Установил Rust себе на Windows X
Но сегодня 2015 год же!

> А под Windows единственный инструмент, который превращает код LLVM IR в исполняемый – Visual C++. Как-то не по фэншую – сочетание свободного ПО и проприетарного… Или я неправильно информирован?
Если бы вы реально хотели посмотреть, то увидели, что у раста два варианта под windows. Через MSVC и Mini-GW.

> Компилятор сообщает о синтаксической ошибке на «hello world», взятый из учебника.
Может програмирование это не для вас?
Ни к чему нападать на человека, который заинтересовался языком, задаёт нормальные вопросы и пытается разобраться.
Человек не задает нормальные вопросы. Человек язвит и не совсем понимает, что же такое этот LLVM.

Задавал бы он нормальные выпросы, он бы показал код и ошибку. И точно бы не требовал, чтобы компилятор работает на 32 разрядной ХР.
Некоторая потеря эффективности
Я не могу понять, в чем потеря эффективности? Не сущесвует никакой возможности впихнуть тип «i32 или ничего» в 4 байта. По-любому нужен еще хотя-бы один бит, чтобы кодировать это «ничего». Как раз для этого необходим дискриминант. Никто вам не мешает возвращать из функции просто i32, размером в 4 байта.

На правах безумной идеи: тегированная память, как в Эльбрусах, была бы весьма кстати.
Я не могу понять, в чем потеря эффективности? Не сущесвует никакой возможности впихнуть тип «i32 или ничего» в 4 байта.

В Си эта проблема решалась значениями-исключениями
hFile = CreateFile(...);
if (hFile == INVALID_NAHDLE_VALUE) ...


Никто вам не мешает возвращать из функции просто i32, размером в 4 байта.

и потерять всю эту безопасность, паттерн-матчинг и вернуться на ступень назад
> В Си эта проблема решалась значениями-исключениями

И это печально.
Не понимаю зацикленной логики. Да, в Си это часто решалось значениями-исключениями. Да, это было быстрее. Но это менее безопасно. Сделали более безопасно. Нам не нравится что это стало медленнее. Так что же мы изначально хотели, быстро или безопасно?
Мы хотели бескомпромиссно, в этом идеология си/си++
Если для этого нужно усложнять концепцию, вставив в описание Option ручное указание значения-исключения для NONE, такое усложение стоит выигрыша в скорости.
Это не «бескомпромиссно», это небезопасно.

Если для этого нужно усложнять концепцию, вставив в описание Option ручное указание значения-исключения для NONE, такое усложение стоит выигрыша в скорости.
Да, а вы как себе представляете решение этой проблемы без этого, но чтобы нельзя было забыть обработать ошибку?

Позвольте вопрос: вы на Си пишете или так, сопереживаете?
Да, а вы как себе представляете решение этой проблемы без этого, но чтобы нельзя было забыть обработать ошибку?

Наверное плохо объяснил. Всё остаётся по-прежнему, и возвращается Option, но компилятору даётся хинт, что значение INVALID_HANDLE_VALUE трактуется как None, таким образом результат помещается в int32
Такой хинт уже реализован в Rust для типа Option&lt&T>. Такой тип занимает 4 байта (для 32-битной архитектуры), и значение 0 трактуется как None.
Да, это интересная идея. Я думаю, что сейчас это требует поддержки в LLVM, поэтому сложно делать это для любых типов.
Разве тут что-то требуется от LLVM? Сейчас для реализации хака с Option<&T> достаточно обернуть данные в обертку NonZero. Можно себе представить, что нечто подобное можно сделать для произвользного диапазона значений. Но это все начинает напоминать оптимизации в стиле «давайте хранить 3 разных значения в двух байтах». Можно, но стоит ли?
Я попробовал посмотреть на разницу в LLVM IR для ссылки и указателя: is.gd/EyTrrF

Как видим, ссылка явно dereferenceable. Я думаю, что для LLVM указатели с возможностью обнуления — особый случай, и они специально поддержаны. Обобщённого же механизма нет. Да и в core только NonZero, а общего типа с возможностью указать, какие значения особые, нет.
А давайте поговорим о том, как приведенный вами пример позволяет узнать причину ошибки?
В качестве примера, в glibc есть костыль с глобальной errno. Про многопоточность естественно тут приходится забыть.
Сам я за подход rust. Обычно накладные расходы не столь критичны. А если уж очень надо — можно как в C сделать, кто мешает то…
Про многопоточность естественно тут приходится забыть.

errno давно уже объявляется как __thread (для MSVC __declspec(thread))
у каждого потока своя переменная errno.
Под многопоточностью я подразумеваю возможность прокинуть прозрачно ошибку в другой поток и обрабатывать ее там.
В rust, очевидно, можно передать Option и Result<T, E> из одного потока в другой совершенно прозрачно.

P.S. если уж говорить про errno, то много где(если не везде) это не переменная.
как приведенный вами пример позволяет узнать причину ошибки?

пример скорее был для Option, а не Result, а с Result можно сделать хинты, мапящие разные ошибки на константы, которые не может вернуть ф-ция (например, если возвращается pointer таких недопустимых результатов очень много)
Насколько я знаю, единственным «специальным» значением для pointer является 0 (т.е. null). Все остальные значения могут быть валидными указателями. А вот специально для null в компиляторе Rust уже реализован хак (ответил вам выше), оптимизирующий размер Option<&T> до размера указателя.
Компилятор сообщает о синтаксической ошибке на «hello world», взятый из учебника.
Какая ошибка у вас?
Компилятор Rust выдаёт не исполняемый код, а код в LLVM IR.
Это не так, компилятор Rust как исполняемый файл rustc выдаёт исполняемый код. LLVM IR используется внутри и скрыто от пользователя. Visual C++ используется не здесь, а при компоновке с системными библиотеками и нужен если вам нужно MSVC ABI — из всего Visual C++ нужен лишь link.exe.

Вместо макроса try! можно использовать оператор ? для автоматического возврата ошибок

use std::fs::File;
use std::io::{self, Read};
use std::path::Path;

#[derive(Debug)]
enum CliError {
    IoError(io::Error),
    ParseError(std::num::ParseIntError),
}

impl From<io::Error> for CliError {
    fn from(err: io::Error) -> CliError {
        CliError::IoError(err)
    }
}

impl From<std::num::ParseIntError> for CliError {
    fn from(err: std::num::ParseIntError) -> CliError {
        CliError::ParseError(err)
    }
}


//fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> 
fn file_double(file_path: AsRef<Path>) -> Result<i32, CliError>{
    let mut file = File::open(file_path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    let n: i32 = contents.trim().parse()?;
    Ok(2 * n)
}

fn main() {
    match file_double("path/to/your/file.txt") {
        Ok(result) => println!("Result: {}", result),
        Err(e) => eprintln!("Error: {:?}", e),
    }
}

Или даже так

fn file_double(file_path: AsRef<Path>) -> Result<i32, CliError>
    File::open(file_path)?
        .read_to_string(&mut String::new())?
        .trim()
        .parse::<i32>()
        .map(|n| 2 * n)

Sign up to leave a comment.

Articles