Чтение кода на Rust навевает шутки о том, как «друзья не позволяют друзьям пропускать день ног» и вызывает в голове комические образы мужчин с халкообразным торсом, балансирующим на тощих ногах. Rust ставит во главу угла безопасность и ювелирное обращение с памятью. В действительности, это довольно редко является настоящий проблемой, и такой подход превращает процесс мышления и написания кода в монотонный и скучный процесс.
После нескольких встреч с Андреем, увидев некоторые из его выступлений, я убедился, что он любит подшучивать. Тем не менее, давайте проглотим наживку. Эта шутка смешная только потому, что она выглядит смешной, или может быть потому, что в ней только доля шутки?
Парадокс Блаба
Всякий раз, размышляя о пользе тех или иных возможностей языков программирования, я возвращаюсь к эссе Пола Грэма «Побеждая посредственность». В нем повествуется об интересном явлении среди программистов, которое он называет «Парадокс Блаба». Для тех, кто не в курсе, парадокс звучит примерно так: Допустим есть программист, который использует некий язык Блаб. С точки зрения своей выразительности, Блаб находится где-то посередине континуума абстрактности среди всех языков программирования. Это не самый примитивный, но и не самый мощный язык программирования.
Когда наш Блаб-программист смотрит на «нижнюю» часть спектра языков программирования, он с легкостью замечает, что эти языки являются менее выразительными, чем его любимый Блаб. Но когда наш гипотетический программист смотрит на «верхнюю» часть спектра, обычно он не осознает, что в действительности смотрит вверх. Вот как это описывает Пол:
Все что он видит, это просто «странные» языки. Возможно, он воспринимает их как равносильные Блабу, только в них еще куча стремной и непонятной фигни. Блаба для нашего программиста вполне достаточно, поскольку он сам думает на Блабе.
Помню, когда я впервые прочитал это, я подумал: «воу, это довольно проницательно». Кто бы мог подумать, что годы спустя эта концепция прочно укоренится в моем образе мышления, когда я начал пытаться учить людей программированию.
Будучи руководителем проектов по языкам в Microsoft, я работаю над TypeScript – типизированной версией Javascript. В обязательном порядке, когда я выступаю перед аудиторией преимущественно JavaScript разработчиков и пытаюсь донести мысль о том, как здорово было бы попробовать добавить немного строгой типизации в Javascript, на меня смотрят хмурые лица. Всякий раз. Даже если она не обязательна. Даже после того как я опишу полдюжины преимуществ. Как и говорил Пол, это выглядит просто «странно». Для JavaScript-программистов TypeScript выглядит в основном тем же что и JavaScript, плюс куча стремной и непонятной фигни.
Пообщавшись с командами других языков программирования, а также наблюдая за все большим количеством людей на конференциях, я осознал, что наблюдение Пола является не только метким, но еще и на удивление универсальным. Большинство программистов готовы отбиваться изо всех сил, увидев новый язык программирования, который они никогда не использовали. Новые, чуждые им особенности вызывают у них аллергическую реакцию. Только поработав с новыми возможностями достаточно долгое время, они начинают понимать, что все это не просто бесполезные приблудины.
Короче говоря, парадокс Блаба – это нечто, с чем мы как программисты должны считаться, во что мы имеем свойство впадать, и из чего нам стоит выбираться, прикладывая все усилия.
Давайте просто сделаем это. Давайте рассмотрим несколько самых странных и бесполезных особенностей Rust. А затем посмотрим, сможем ли мы провернуть деблабизацию.
Странная фигня №1. Полиморфизм в стиле Rust
Давайте напишем программу на Rust, которая использует немного полиморфизма для того, чтобы напечатать две разные структуры. Сначала я покажу вам код, а затем мы рассмотрим его в деталях.
use std::fmt;
struct Foo {
x: i32
}
impl fmt::Display for Foo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "(x: {})", self.x)
}
}
struct Bar {
x: i32,
y: i32
}
impl fmt::Display for Bar {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "(x: {}, y: {})", self.x, self.y)
}
}
fn print_me<T: fmt::Display>(obj : T) {
println!("Value: {}", obj);
}
fn main() {
let foo = Foo {x: 7};
let bar = Bar {x: 5, y: 10};
print_me(foo);
print_me(bar);
}
Какое же оно вырвиглазное! Да, тут есть полиморфизм, но это и близко не похоже на ООП. Этот код использует обобщения, и не только обобщения, но в таком подходе есть куча ограничений. И что это за
impl
?Давайте по частям. Я создаю две структуры для хранения наших значений. Следующим шагом я реализую для них нечто, называемое
fmt::Display
. В C++ мы бы перегрузили оператор <<
для ostream
. Результат был бы аналогичным. Теперь я могу вызывать функцию печати, передавая свои структуры напрямую.Это уже половина истории.
Дальше у нас появляется функция
print_me
. Эта функция обобщенная и принимает все что угодно, если оно умеет fmt::Display
. К счастью, мы только что убедились, что наши структуры так умеют.Все остальное просто. Мы создаем несколько экземпляров структур и передаем их на печать в
print_me
.Фух… пришлось потрудиться. Так делается полиморфизм в Rust. Вся суть в обобщениях.
Теперь давайте на минуту переключимся на C++. Многие, особенно новички, могли не сразу додуматься до использования шаблонов, и пошли бы по пути объектно-ориентированного полиморфизма:
#include <iostream>
class Foo {
public:
int x;
virtual void print();
};
class Bar: public Foo {
public:
int y;
virtual void print();
};
void Foo::print() {
std::cout << "x: " << this->x << '\n';
}
void Bar::print() {
std::cout << "x: " << this->x << " y: " << this->y << '\n';
}
void print(Foo foo) {
foo.print();
}
void print2(Foo &foo) {
foo.print();
}
void print3(Foo *foo) {
foo->print();
}
int main() {
Bar bar;
bar.x = 5;
bar.y = 10;
print(bar);
print2(bar);
print3(&bar);
}
Довольно просто, не так ли? Окей, вот вам небольшая викторина: что именно напечатает код на C++?
Если вы не угадали, не расстраивайтесь. Вы находитесь в хорошей компании.
Если угадали – мои поздравления! Теперь задумайтесь на минуту, сколько всего вы должны знать о С++, чтобы дать правильный ответ. Из того что я вижу, вы должны понимать принципы работы стека, как объекты копируются, когда они копируются, как работают указатели, как работают ссылки, как устроены виртуальные таблицы и что такое динамическая диспетчеризация. Просто чтобы написать несколько простых строк в стиле ООП.
Когда я начинал изучать C++, этот подъем оказался слишком крутым для меня. К счастью, мой двоюродный брат оказался экспертом по C++, и, взяв меня под свое крыло, он показал мне несколько проторенных дорожек. Тем не менее, я успел натворить тонны детских ошибок, вроде этого примера. Почему? Одной из причин неприступности С++ является высокая когнитивная нагрузка при его освоении.
Часть когнитивной нагрузки приходится на вещи, которые присущи программированию по своей сути. Вы должны понимать стек. Вы должны знать как работают указатели. Но С++ повышает степень нагрузки, требуя понимания того, в каких случаях значение будет скопировано не полностью, и когда виртуальная диспетчеризация используется, а когда не используется – и все это без каких-либо предупреждений от компилятора, если разработчик делает что-то, что «скорее всего является плохой идеей™».
Это не попытка пойти войной против С++. Многие вещи в Rust реализованы с мыслью сохранить философию низкоуровневых и эффективных абстракций, взятой из C++. Вы даже можете написать код, который будет очень похож на пример Rust.
Что Rust действительно делает – так это отделяет наследование от полиморфизма, подталкивая вас мыслить в направлении создания обобщений с самого начала. Таким образом вы начинаете думать обобщенно с первого дня. Тем не менее, отделение наследования от полиморфизма может показаться странной идеей, особенно если вы привыкли всегда использовать их вместе.
Такое разделение может вызвать одно из первых проявлений Блаб-эффекта: в чем вообще преимущество разделять наследование и полиморфизм? И кстати, в Rust вообще есть наследование?
Хотите верьте, хотите – нет, но по крайней мере в Rust 1.6 нет вообще никаких специальных инструментов для наследования структур. Вместо этого их функциональность наращивается за пределами самих структур, с помощью особенной концепции языка – «типажей». Типажи позволяют добавлять методы, требовать реализации методов, и всячески дооснащать структуры данных в уже существующих системах. Также типажи поддерживают наследование: один типаж может расширять другой.
Если хорошенько покопаться, можно заметить еще кое-что. В Rust нет всех тех проблем, о которых нам пришлось беспокоиться на С++. Мы можем больше не думать о том, как что-то теряется, когда функция вызывается каким-то образом, и какое влияние оказывает виртуальная диспетчеризация на наш код. В Rust все работает в едином стиле, независимо от типа. Таким образом целый класс детских ошибок просто исчезает.
(Прим. пер. – Подробнее о типажах можно почитать в русскоязычном переводе книги «Язык программирования Rust».)
Странная фигня №2. В смысле, нет исключений?
Раз уж мы заговорили о вещах, которых в Rust нет, следующей странной фигней будет отсутствие исключений. Разве это не шаг назад? Как нам поступать с ошибками? Можем ли мы пробрасывать их наверх, чтобы обрабатывать все сразу в одном месте?
Что ж, пришло время познакомиться с монадами.
Хотя… ладно, шучу, на этот раз можно обойтись без них. В Rust обработка ошибок гораздо более прямолинейна. Вот пример того, как это выглядит на практике. Для начала, примеры того, как будет выглядеть объявление функций:
impl SystemTime {
/// Возвращает текущее системное время
pub fn now() -> SystemTime;
/// Возвращает ошибку, если переданное "раньше" окажется позже
pub fn duration_from_earlier(&self, earlier: SystemTime) -> Result<Duration, SystemTimeError>;
}
Обратите внимание, что функция
now
просто возвращает SystemTime
и не имеет каких-либо исключительных ситуаций, в то время как duration_from_earlier
возвращает тип Rеsult
, который может принимать значения как Duration
, так и SystemTimeError
. Таким образом, вы сразу видите все возможные исходы выполнения функций, как успешные, так и не успешные.Но все эти исключительные ситуации создают кашу в возвращаемых значениях. Кто захочет видеть такое в своем коде? Здорово, конечно, всегда делать проверки на ошибки, но смысл исключений заключается как раз в том, что они позволяют обрабатывать ошибки не только локально, но и пробрасывать их наверх, выполняя обработку в одном месте.
И Rust позволяет вам сделать тоже самое.
fn load_header(file: &mut File) -> Result<Header, io::Error> {
Ok(Header { header_block: try!(file.read_u32()) })
}
fn load_metadata(file: &mut File) -> Result<Metadata, io::Error> {
Ok(Metadata { metadata_block: try!(file.read_u32()) })
}
fn load_audio(file: &mut File) -> Result<Audio, io::Error> {
let header = try!(load_header(file));
let metadata = try!(load_metadata(file));
Ok(Audio { header: header, metadata: metadata })
}
Хотя это не совсем очевидно, этот код использует пробрасывание исключений. Вся фишка в макросе
try!
. Он делает достаточно простую вещь. Он вызывает функцию. Если она завершится успешно, он вручит результат вычислений вам. Если вместо этого случится ошибка, try!
пробросит эту ошибку, завершив выполнение текущей функции.Это означает, что если у
load_header
будут какие-либо проблемы при вызове file.read_u32
, то функция вернет io::Error
. Далее, то же произойдет в load_audio
, и из нее будет возвращена та же ошибка. И так далее до тех пор, пока вызывающая функция наконец не обработает ошибку.(Прим. пер. – Подробнее об обработке ошибок можно почитать в статье на Хабре «Обработка ошибок в Rust».)
Странная фигня №3. Борроу-чекер
Вы знаете, это забавно. Первое, что упоминают многие люди, говоря о Rust – это borrow checker. Более того, его часто преподносят как основную особенность Rust, выделяющую его среди других языков программирования. Например, для Андрея, borrow checker – это «халкообразный торс» Rust. Для меня же borrow checker – это просто еще одна проверка компилятора. Так же, как проверка на соответствие типов, borrow сhecker позволяет отловить большинство багов до того, как они произойдут во время выполнения. Вот и все. Конечно, по началу он может показаться монструозной штуковиной, но я посмею утверждать, что дело тут не в том, что Rust заставляет вас изучать какую-то новую непонятную систему типов, а в том, что умение работать с ним наращивает новые мускулы у вас как программиста.
Так какие ошибки отлавливает borrow checker, спросите вы?
Использование указателей после освобождения памяти
О да, классическая ситуация, сначала вы освобождаете память, а затем снова ее используете. В большинстве случаев это именно та причина, по которой программы падают с пугающими «null pointer exception».
Есть целая куча «хороших практик» C++, которые позволяют избежать use-after-free: использование RAII, использование ссылок или умных указателей вместо сырых указателей, документирование отношений владения и заимствования в вашем API и так далее. Все то, что по мнению Андрея «превращает процесс мышления и написания кода в монотонный и скучный процесс». Команда хорошо натренированных С++ программистов в состоянии избежать большинство use-after-free ошибок, занимаясь монотонной и скучной работой, потому что такова цена – соблюдение всех «хороших практик», никогда не читерить и пополнять команду только высококвалифицированными экспертами C++.
Невалидные итераторы
Вам никогда не приходилось модифицировать контейнер, по которому вы итерировались в C++, и получать из-за этого внезапные падения когда-нибудь в будущем? Мне приходилось. Если вы добавили или удалили из контейнера хотя бы один элемент, этого достаточно, чтобы потребовалось провести реаллокацию контейнера и сделать ваш итератор невалидным.
Я не часто наступаю на эти грабли, но это все еще происходит время от времени.
Состояния гонки данных
В Rust данные либо общие, либо изменяемые. Если данные можно изменять, их нельзя разделять между несколькими потоками, так что нет никакой возможности начать менять их в двух потоках одновременно, вызвав тем самым состояние гонки. Если же данные общие, их нельзя модифицировать, так что вы можете читать их сколько вам заблагорассудится из любого количества потоков.
Если вы пришли из мира С++ или любого другого языка с множеством хороших параллельных библиотек, такие ограничения могут показаться вам слишком строгими. К счастью, это далеко не вся история, но это основа, дающая вам набор простых правил для создания более сложных абстракций. Остальная часть истории пишется прямо сейчас. В экосистеме Rust появляется все большее число библиотек, ориентированных на параллелизм. Если вам интересно узнать больше, вы можете изучить принципы их работы.
Отслеживание владения
Эта концепция может показаться несколько избыточной, но на самом деле это именно то, с чем постоянно воюет C++. Ранее я упоминал об одной из хороших практик «документировать отношения владения и заимствования в вашем API». Проблема в том, что эта информация хранится в комментариях, вместо того, что находится непосредственно в коде.
Вот вам сценарий: вы пишете на С++ и вам необходимо вызвать библиотеку, которую написал кто-то другой. Допустим, это библиотека на C и она принимает в качестве аргументов сырые указатели. Должны ли вы позаботиться удалить впоследствии то, что передали в эту библиотеку? Или она возьмет на себя эту ответственность, сохранив полученные данные в одной из своих структур? Может быть вы вызываете скриптовый движок вроде Ruby? Кто в таком случае владеет данными?
Вместо того, чтобы вчитываться в документацию, Rust позволяет быть уверенным в ваших ожиданиях, все время проверяя правильность использования API библиотеки с помощью borrow checker.
И многое другое
Borrow checker помогает избежать множество других ошибок. Например, он позволяет всегда рассчитывать на то, что любые изменяемые данные, которые вы принимаете в написанную вами функцию не влияют на какое-либо внешнее состояние, и вы можете смело изменять их так, как посчитаете нужным.
Это, кстати, открывает широкие возможности для дополнительных оптимизаций, которые трудно произвести в С-подобных языках программирования, поскольку компилятор гарантирует, что любое значение, которое имеет несколько псевдонимов, не может быть изменяемым, и наоборот – изменяемое значение всегда имеет только одно имя.
(Прим. пер. – Подробнее о концепции владения и заимствования можно почитать в русскоязычном переводе книги «Язык программирования Rust».)
Странная фигня №4. Правила нужны для того, чтобы их нарушить
Я считаю, что одной из самых сильных сторон Rust является его прагматичность. Большинство строгих ограничений можно обойти с помощью таких возможностей, как
unsafe
и mem::transmute
. Borrow checker не подходит для решения ваших задач? Не проблема, просто отключите его.(Прим. пер. – Строго говоря, это не правда: в Rust нет никакого простого способа отключить borrow checker. Даже внутри блоков unsafe он работает на полную мощность. Но borrow checker проверяет правила заимствования только для ссылок
&T
и &mut T
, в то время как в unsafe
-блоках у вас также появляется возможность использовать сырые указатели *const T
и *mut T
, которые работают практически аналогично указателям из C. Их использование никак не ограничено правилами заимствования. Подробнее об этом можно почитать в книге «The Rustonomicon: The Dark Arts of Advanced and Unsafe Rust Programming».)Это позволяет вам делать все, что вы привыкли делать на C-подобных системных языках программирования. Преимущество Rust заключается в том, что гораздо проще писать код, который с самого начала безопасный по-умолчанию, и затем добавлять небезопасные участки по мере их необходимости. Гораздо труднее писать безопасный код, основываясь на том, что изначально небезопасно.
Хотя Rust и дает возможность выбора, он подталкивает вас не стрелять себе в ногу.
Так что там с ногами?
Возвращаясь к ногам, пропускал ли Rust свои тренировки? Получился ли он однобоким? Оказался ли он сосредоточен на неправильных вещах?
Rust крепнет с каждым днем, и, к счастью, хорошо осведомлен, как выполнять присед, не прогибая при этом спину. Этот момент трудно переоценить. Философия Rust имеет прочный фундамент, а значит язык будет расти и развиваться.
От переводчика: Как всегда, выражаю благодарность русскоязычному сообществу Rust за помощь в переводе и ценные замечания.
Комментарии (132)
erlyvideo
25.01.2016 12:05+8Эээммм?
Управление памятью — редкая проблема? Мне казалось, что это стало чуть ли не решающим фактором, позволившим джаве втоптать C++ в землю и забрать себе всю энтерпрайз разработку.Monnoroch
25.01.2016 12:34+4Это заблуждение из девяностых. В современном С++ не все идеально, но при соблюдении нескольких простых правил никаких отстрелов ног не происходит, об этом что Саттер, что Страуструп, да и другие популяризаторы современного С++ на каждой конференции говорят.
defuz
25.01.2016 12:57+6Мне кажется, в статье как раз довольно тонко обыгрываются эти самые «нескольких простых правил», в разделе «Использование указателей после освобождения памяти». ;)
Я сильно сомневаюсь, что все прямо так радужно. Особенно если речь идет о многопоточных системах, на которые мы должны ориентироваться в первую очередь в 2016 году. Каким образом C++ позволяет убедиться, что можно передавать между потоками, а что нельзя? Какие данные должны быть доступны одновременно из нескольких потоков, а какие нет? Только на чтение, или можно изменять? А когда? Все это сложная, «монотонна и скучная» работа. И это только вопрос времени, когда и где именно вы ошибетесь. И какие у этого будут последствия.soniq
25.01.2016 17:49-1На самом деле, можно рассматривать языки программирования не в вакууме, а вместе с командой, которая что-то программирует. В этой ситуации С++ дает техлиду свободу создать любую парадигму, какая лучше всего подходит именно для его задач. А Rust навязывает ту парадигму, которую в него заложили создатели. Другими словами, компилятор С++ программисту пытается всячески помочь, а от ошибок ограждает код-ревью и опыт, а в Rust компилятор пытается сам, в меру своего разумения, защитить программиста от глупых ошибок.
defuz
25.01.2016 18:17+8Я никак не могу с вами согласился. Да, есть базовые правила, вроде «никогда не может быть больше одной мутабельной ссылки». Но обычные ссылки – это далеко не все, что предоставляет вам Rust. Это только основа. Дальше у вас есть выбор из кучи инструметов, и возможность выбрать именно ту модель, которая наиболее приемлема для вас и вашей задачи.
Хотите – используйте unsafe. Хотите – используйте подсчет ссылок Rc/Arc. В ближайшем будущем у вас появится возможность еще и подключить сборку мусора, если захотите. И не просто подключить, а выбрать ту его реализацию, которая вам больше всего подойдет.
Если вам интересна эта тема, почитайте главу «Выбор гарантий» из официального руководства Rust. Она дает лишь основное представление о том, сколько различных инструментов предлагает Rust прямо из коробки для решения одних и тех же задач, давая при этом разную степерь удобства и разные гарантии по эфективности и безопасности.
ozkriff
25.01.2016 12:35> Управление памятью — редкая проблема
Я не вижу, где бы такое утверждалось. Речь как раз о том, что «безопасность и _ювелирное_ обращение с памятью» не так уж и сильно нужны большинству программистов, по крайней мере по их представлениям.
valexey
25.01.2016 15:37+3Странная фигня №1. Полиморфизм в стиле Rust
Погодите, но ведь это же статический полиморфизм (то есть полиморфизм времени компиляции) продемонстрирован, разве нет? Это как type class в Haskell если без расширизмов от ghc (без existential types), ну и как это будет в C++ когда введут наконец концепты в стандарт.
Как на Rust будет динамический полиморфизм (времени исполнения)? И какой при этом будет оверхед?defuz
25.01.2016 15:45+2Динамический полиморфизм делается двумя способами:
1. Первый способ тупой: использовать тип-суммы. Но такой подход очевидно не расширяемый (нельзя добавить новые варианты enum). Оверхед – постоянное выполнения match.
2. Второй способ: типажи-объекты. К ним можно привести данные любого типа, которые реализуют определенный трейт. Представляют собой, собственно, данные и виртуальную таблицу. Вот на эту виртуальную таблицу и появляется оверхед. Где-то тоже самое, что приведение к абстрактному классу в C++.valexey
25.01.2016 15:52Ага, спасибо. Посмотрю.
Я правильно понимаю, что тип-суммы, это то, что в других ЯП называется алгебраическими типами данных?
Оверхед от типажей-объктов будет при каждом вызове функции, или только в тех случаях когда компилятор действительно не может определить на этапе компиляции какую функцию нужно дернуть (в С++ компиляторах эта оптимизация делается всегда когда возможно).defuz
25.01.2016 15:55Тип-сумма – один из алгебраических типов данных. Вообще есть еще тип-произведение, или проще говоря кортеж. Но вообще да, в Rust используются алгебраические типы данных.
defuz
25.01.2016 16:04Оверхед от типажей-объектов будет всегда, потому что это по-определению «данные какого-то неизвесного типа, реазующего заданный типаж». В Rust вы всегда явно контролируете то что используете – static dispatch или dynamic dispatch. Объект нужно явно привести к типаж-объекту перед использованием – это отдельный тип.
Но на практика довольно редко приходится использовать типаж-объекты. Почти все задачи успешно решаются через статический полиморфизм с последующей мономорфизацией. А для того, чтобы обобщенные определения не превращались в ад, в Rust помимо типов-параметров есть еще ассоциированные типы.valexey
25.01.2016 16:11Но на практика довольно редко приходится использовать типаж-объекты.
Хм. Интересно. А как в Rust статическим полиморфизмом обходится например такая задача, как написание GUI? Это вроде бы та область, где активно используется динамическая диспетчеризация/полиморфизм.
Примеры можно?defuz
25.01.2016 16:20Если у нас есть контейнер, который содержит в себе виджеты одинакового типа (пример: абстрактный тип Tree и типаж TreeElement), то используется обобщенный тип данных:
struct Tree<T> { items: Vec<T> } // для любого типа T реализующего типаж TreeElement реализовать типаж Widget для Tree<T> impl<T> Widget for Tree<T> where T: TreeElement { ... }
Если у вас виджет, который может содержать множество любых виджетов (пример – какой-нибудь layout), то это как раз тот самый случай, когда нужно использовать типаж-объекты:
struct Layout { items: Vec<Box<Widget>> } fn main() { let w1 = Widget1::new(); let w2 = Widget2::new(); let layout = Layout { items: vec![Box::new(w1) as Box<Widget>, Box::new(w2) as Box<Widget>]; } }
Обратите внимание, что мы явно приводим наши объекты к Box<Widget>. Это и есть типаж-объект. В данном случае Widget – это имя типажа, а Box – это нечто, что хранится в памяти, и чем мы владеем (тип-обертка). Таким образом, каждый элемент структуры Layout будет хранить в себе данные об объекте и виртуальную таблицу для типажа Widget.valexey
25.01.2016 16:27Спасибо. С т.з. типов более-менее понятно.
А можно пояснить что в последнем примере будет в плане размещения переменных/объектов в памяти? w1 и w2 будут на стеке? При приведении вида Box::new(w1) as Box значение w1 будет скопировано в кучу, таким образом, физически у нас станет на какой-то момент два w1?
Обычно в гуях какой-нибудь Layout держит в себе лишь указатели/ссылки/смартпоинтеры (в зависимости от ЯП) на виджеты, но не сами эти объекты.defuz
25.01.2016 16:44+2В данном случае Box<Widget> – это и есть смарт-поинтер на кучу. Вообще в Rust часто используются умные указатели, например &str – это указатель на строковый слайс, представляющий собой ссылку на начало строки и к-во байт.
В данном случае в векторе layout.items будут хранится пары ссылок: ссылка на данные в куче и ссылка на виртуальную таблицу.
Теперь конкретно по-поводу того, что происходит в main:
Cначала w1 и w2 создаются на стеке.
Затем они перемещаются в кучу, при передаче их в конструктор Box::new. Это тоже самое что копирование, с той лишь разницей, что после перемещения изначальное значение будет не доступно. Так что у вас не будет такого момента, когда существуют одновременно две копии объекта (технически они будут, но переменные w1 и w2 больше не доступны для использования, поскольку их значения были перемещены).
Далее мы преобразовываем упаковку конкретного типа в упаковку типаж-объекта. По-сути это операция, которая просто к ссылке на объект добавляет еще ссылку на конкретную виртуальную таблицу, так что тут каких либо перемещений не происходит.
Компилятор может оптимизировать процес перемещенний и создавать объект сразу на месте выделенной под него памяти в куче. Я описал процес так, как он происходит по своей сути, шаг за шагом.valexey
25.01.2016 16:48А как это всё будет выглядеть если у меня два layout'a (или других подобных компонента) и в обоих нужен w1 (точнее ссылка на него)? В обоих местах естественно нужна возможность w1 модифицировать.
Просто так поперемещать, насколько я понимаю, уже не выйдет.Revertis
25.01.2016 17:03+3Интересно, что это за интерфейс, в котором на одном экране один и тот же виджет используется несколько раз? Какой это мог бы быть виджет вообще?
valexey
25.01.2016 17:08В гуйне обычно есть несколько сущностей которые хранят в себе ссылки на гуй-компонентины. Простой пример — layout этот + диспетчер клавиатуры, которому нужно знать кому посылать сообщение в случае если пользователь клавишу вдавил. Подобных штук там довольно много разных.
Один и тот же виджет входит сразу в несколько иерархий и списков виджетов.Revertis
25.01.2016 17:14Никогда такого не видел. С гуйнёй работаю последние 9-10 лет.
valexey
25.01.2016 17:19Я гуйней тоже занимался. Писал гуйно-фреймворк местный для Scada.
Да и судя по архитектуре Swing — там подобное тоже имеется.splav_asv
25.01.2016 20:29+1Можно использовать Mutex, т.е. вроде бы должно получится, но не пробовал:
items: vec![Mutex::new(Box::new(w1) as Box), Mutex::new(Box::new(w2) as Box)];
достать — items[0].lock()
defuz
25.01.2016 17:14+2Чтобы в полной мере ответить вам на ваш вопрос, мне пришлось бы рассказать вам о концепции лайфтаймов, но боюсь, это был бы слишком длинный комментарий.
Если коротко, то у вас может быть много вариантов ссылок на типаж-объект. Box – это владеющая ссылка на кучу. В то же время, вы можете хранить ссылку на типаж-объект как &'a Widget. Это будет ссылка-заимствование. В данном случае 'a – это метка, указывающая на время жизни этого виджета. Таких ссылок может быть несколько.
Сразу скажу, что у вас принципиально не получится сделать несколько изменяемых ссылок на один и тот же объект (&'a mut Widget). Это фундаментальное ограничение Rust.
Варианта решения три:
Либо изменить архитектуру таким образом, чтобы вам было достаточно только одной одновременно существующей ссылки. У такого решения есть множество плюсов: у вас все еще нет вообще никаких накладных расходов и управление памятью происходит максимально эфективно. Более того, ваш код одинаково хорошо будет работать как в однопоточном, так и в многопоточном режиме. Т.е. он будет потокобезопасным по определению
Второй метод – это использовать умные указатели: Rc (reference counter, для однопоточного приложения) или Arc (atomic reference counter, для многопоточного приложения). Таким образом у вас появится возможность получать мутабельную ссылку на объект, но у вас возникнут накладные расходы во время выполнения, связанные с необходимостью подсчета ссылок. Также у вас появится возможность устроить утечку памяти, если виджеты будут циклически владеть ссылками друг на друга, как и всегда при использовании механизма подсчета ссылок.
Есть еще третий путь – interior mutability. Этот метод подходит для случаев, когда данные хоть и изменяются технически, но такие изменения не отражаются на их состоянии. Пример: мемоизация/кеширование выполнения функции.
defuz
25.01.2016 16:11На счет «всегда» я возможно погорячился, потому что на самом деле нет никаких преград для LLVM заинлайнить вызов функции по ссылке. Но нужно понимать, что если существует возможность избавится от виртуальной таблицы на этапе компиляции, то вам вообще не нужен типаж-объект в данном конкретном случае.
Гораздо больший оверхед от типаж-объектов заключается в том, что поскольку это unsized type (неизвестно какого размера могут быть данные), то они всегда живут только в куче, и передаются/принимаются только по ссылке на кучу. Обычные объекты в Rust в большинстве случаев живут прямо в стеке и вы либо передаете ссылку на стек, либо перемещаете их по значению (move-semantic).Googolplex
25.01.2016 18:14Трейт-объекты, конечно же, могут жить на стеке:
use std::io::Write; let buf: Vec<u8> = Vec::new(); let w: &mut Write = &mut buf;
valexey
25.01.2016 15:41Странная фигня №2. В смысле, нет исключений?
По моему, в Rust получилось нечто, что очень сильно напоминает checked exceptions в java со всеми их плюсами и минусами. А если учесть, что checked exceptions были признаны ошибкой…defuz
25.01.2016 15:52+8Похожего у них только то, что в сигнатуре виден тип возращаемой ошибки. Принцип реализации совершенно другой. В Rust есть «ошибки», и есть «паники». Полученное значение Result можно всегда преобразовать в панику потока, просто вызвав метод `unwrap`. Паника гарантировано корректно завершит текущий поток, по-сути это unhandled exception.
Я не очень понимаю, кем именно и почему checked excpetions признаны ошибкой, так что если вам интересно разобраться, задавайте конкретные вопросы.valexey
25.01.2016 16:08Похожего у них только то, что в сигнатуре виден тип возращаемой ошибки.
Ну, это и есть основное отличие checked от unchecked exceptions. И именно это вызывает в т.ч. кучу проблем в той же java.
Заабортить поток при исключении/ошибке можно и в жабах, да и вообще в общем то в любом ЯП. Это не проблема как раз.
Я прекрасно понимаю, что в Rust работа с ошибками в плане механизмов больше похожа на соответствующую монаду в Haskell нежели на исключения. Но расматриваем то, так сказать пользовательские характеристики, а не потроха. Кстати, какова эффективность (по сравнению с исключениями на современных архитектурах и компиляторов) у такого подхода? Накладных расходов на каждый успешных вызов функции нет?
Про checked exceptions пишут следующее:
«Checked exceptions are bad because programmers just abuse them by always catching them and dismissing them which leads to problems being hidden and ignored that would otherwise be presented to the user»
Кроме того, представьте себе, что вам например нужно добавить в самую общую, всеми используемую библиотеку еще один тип ошибок который она должна выбрасывать (возможно ранее какие-то функции вообще не выбрасывали ошибок, а теперь должны). Теперь в Rust и в java вам придется пройтись по ВСЕМУ коду (стандартному, стороннему, своему) и везде изменить сигнатуры функций. Ну и до кучи все перекомпилировать конечно.
Это же Ад ломающий обратную совместимость.Googolplex
25.01.2016 18:24+3Кстати, какова эффективность (по сравнению с исключениями на современных архитектурах и компиляторов) у такого подхода? Накладных расходов на каждый успешных вызов функции нет?
Если вы говорите про подход с обработкой ошибок с помощьюResult
, то накладные расходы здесь гораздо меньше, чем у исключений. Фактически, накладной расход — это дополнительное поле-дискриминатор enum'а в возвращаемом значении, и всё.
Про checked exceptions пишут следующее:
«Checked exceptions are bad because programmers just abuse them by always catching them and dismissing them which leads to problems being hidden and ignored that would otherwise be presented to the user»
В Rust невозможно проигнорировать ошибку а-ляcatch (Exception e)
в Java. Если функция, которую вы вызываете, может завершиться с ошибкой, то её возвращаемое значение будет типаResult<T, Error>
, из которого собственноT
можно достать только явно, через паттернматчинг (ну или через конструкции, к нему сводящиеся — монадические комбинаторы или макрос try!()). Да, некоторые операции типа записи в поток ввода-вывода могут ничего не возвращать, и в таком случае возможность случайно проигнорировать ошибку возрастает, но компилятор в таком случае выдаст предупреждение.
Кроме того, представьте себе, что вам например нужно добавить в самую общую, всеми используемую библиотеку еще один тип ошибок который она должна выбрасывать (возможно ранее какие-то функции вообще не выбрасывали ошибок, а теперь должны). Теперь в Rust и в java вам придется пройтись по ВСЕМУ коду (стандартному, стороннему, своему) и везде изменить сигнатуры функций. Ну и до кучи все перекомпилировать конечно.
Это же Ад ломающий обратную совместимость.
Как правило, библиотеки, которые предоставляют функции, которые могут завершиться с ошибкой, содержат специальный тип-enum, варианты которого соответствуют ошибкам. В этом случае при добавлении новых ошибок сигнатура ни одной функции не поменяется. Клиентский код может сломаться там, где делается паттернматчинг по этому енуму, но это делается далеко не всегда — очень часто ошибки просто выводятся пользователю через реализацию Display для ошибки, без точного анализа.
Да, если изменяются функции, которые раньше вернуть ошибку не могли, а теперь могут, то это ломает обратную совместимость. В этом случае автор библиотеки соответствующим образом изменит версию своего проекта согласно semver, и Cargo обеспечит, чтобы код, зависящий на старую версию библиотеки, не сломался.valexey
25.01.2016 20:21Если вы говорите про подход с обработкой ошибок с помощью Result, то накладные расходы здесь гораздо меньше, чем у исключений. Фактически, накладной расход — это дополнительное поле-дискриминатор enum'а в возвращаемом значении, и всё.
Гораздо меньше нуля? :-) Речь же шла про успешные вызовы функции. При успешном вызове исключения оверхеда не привносят.
Если я правильно понимаю, то в теле каждой функции будет ветвление которое будет выпоняться на всех уровнях при каждом успешном вызове функции и этим привносить оверхед в точности также как это в классической обработке ошибок в виде возвращаемых кодов ошибок. Это так?
В Rust невозможно проигнорировать ошибку а-ля catch (Exception e) в Java. Если функция, которую вы вызываете, может завершиться с ошибкой, то её возвращаемое значение будет типа Result<T, Error>, из которого собственно T можно достать только явно, через паттернматчинг
Проигнорировать ошибку можно всегда. А в языке с развитой системой макросов это можно сделать еще удобней, чем в языке без них.
Ну вот например даже без макросов:
use std::io; use std::fs::File; use std::io::prelude::*; fn write_to_file_errors_ignored() { let _ = || -> Result<(), io::Error> { 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 main() { write_to_file_errors_ignored(); }
Тут абсолютно все равно что за типы ошибок были внутри последовательности операторов.
Это четкий аналог java'вского:
void writeToFileIgnoreExceptions() { try { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("filename.txt"), "utf-8")); writer.write("This is a list of my best friends."); System.out.println("I wrote to the file"); } catch (IOException ex) {} }
Только в отличии от java в Rust добавилось мусора внутри последовательности statement'ов — добавились макросы try! ну и добавилось оверхеда в случае если ошбок не было.defuz
25.01.2016 20:31+3Если бы вы относительно хорошо знали Rust и просто хотели бы забить на обработку ошибок, скорее всего вы написали бы так:
fn write_to_file_errors_ignored() { let mut file = File::create("my_best_friends.txt").unwrap(); file.write_all(b"This is a list of my best friends.").unwrap(); println!("I wrote to the file"); }
Этот код написать гораздо проще, чем то, что предложили вы, и он работает по принципу «если что-то пошло не так – просто упади». На мой взгляд, такой подход куда лучше, чем замалчивание ошибок, которое происходит в Java у плохих программистов.
Я уж не говорю о том, что даже тупо сделать нормальный проброс ошибок проще, чем то что вы написали:
fn write_to_file_errors_ignored() -> Result<(), io::Error> { 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"); }
Если у кого-то прямо такая аллергия на тот факт, что функция может возвращать ошибку, то с этим ему компилятор никак не поможет. И отсутствие «checked exceptions» тем более.
И последнее, вы сами дали четкий сигнал компилятору, что хотите проигнорировать возвращаемое значение из замыкания, используя специальное имя переменной "_". Если бы вы использовали нормальное имя, компилятор выдал бы вам предупреждение.valexey
25.01.2016 20:35Я Rust вообще не знаю, и не скрываю этого :-)
А штука с unwarp совершенно не эквивалентна тому, что написал я, и тому, что на java — это не игнорирование ошибок, а паника в случае если ошибка возникла. Также unwarp придется вставлять в 100500 строчках внутри блока из изменять их все, если я таки решу ошибку как-то обработать.
Нормальный проброс ошибок не проще из за того, что в этом случае спецификация функции зависит от её раализации. Именно поэтому checked exceptions в java считаются не самой лучшей идеей.
Если вы читали доку на стандартную либу по Rust, то должны знать откуда я взял этот пример и как его изменил :-) Поэтому ваши примеры я уже видел :-)defuz
25.01.2016 20:52+7Не обижайтесь, но мне кажется, что вы сейчас наглядно демонстрируете проявление парадокса, о котором говорится в статье. :) Вам зачем-то нужно обязательно убедится, что в Rust что-то сделано не так. Для этого вы берете опыт, который у вас есть на Java, и проецируете его на Rust, которого вы толком не знаете, и тут же выдаете вердикт: фигня, так работать не будет.
То-то сотни разработчиков Rust, многие из которых MS и PhD по компиляторам такие тупые и не додумались предложить лучшего решения. :)
Я уже не знаю что вам отвечать, потому что вначале вы пишете, что checked исключения плохие потому, что Java-разработчики тупо забивают их обрабатывать, а когда я показываю вам, что обработать ошибку или пробросить ее наверх даже проще, что попытаться на нее «забить», вы говорите мне, что я написал «совершенно не эквивалентное тому, что написал я». Ну конечно не эквивалентное, я хотел показать, что никто так как вы на Rust писать не будет, разве что ему на самом деле нужно будет проигнорировать ошибку.
Нормальный проброс ошибок не проще из за того, что в этом случае спецификация функции зависит от её раализации. Именно поэтому checked exceptions в java считаются не самой лучшей идеей.
Во-первых, вы сами как считаете, в API фукции должны входить возвращаемые ошибки или нет? Во-вторых, не вижу ничего странного в том, что функции с побочными эфектами и без должны иметь разные сигнатуры, ведь они не взаимозаменяемы. И в третьих, если вас действительно тревожит этот вопрос, можно привести все ошибки к Box<Error>.
valexey
25.01.2016 21:26+2Не обижайтесь, но мне кажется, что вы сейчас наглядно демонстрируете проявление парадокса, о котором говорится в статье. :) Вам зачем-то нужно обязательно убедится, что в Rust что-то сделано не так. Для этого вы берете опыт, который у вас есть на Java, и проецируете его на Rust, которого вы толком не знаете, и тут же выдаете вердикт: фигня, так работать не будет.
Технически, всё как раз будет работать. А опыта на Java у меня совсем не много, так что мимо кассы :-) В работе я использую другие языки.
Java тут выбрана просто как каноничный пример похожих грабель в мире языкостроения. На раннем этапе всем тоже казалось, что checked exceptions это офигенная идея (читай — пока языком пользовались его создатели и те самые сотни разработчиков, то есть пока почти никто не пользовался языком). Но как java пошла в народ, это все вскрылось. И увы, checked exceptions оказались ниочень.
То-то сотни разработчиков Rust, многие из которых MS и PhD по компиляторам такие тупые и не додумались предложить лучшего решения. :)
Ну, во первых это попытка манипуляторства вида argument from authority. Во-вторых чтобы сделать приличный ЯП мало быть PhD по компиляторостроению — компиляторщик сделает язык удобный для компиляции и сделает прекрасный компилятор. Но этот язык вполне вероятно будет не удобен прикладнику.
В общем, в том то и проблема, что они — PhD, как думаю, и в случае Java :-) Тут неплохо бы еще быть PhD по психологии и юзабилити хотя бы.
Пользователи же популярного языка — ну вот совсем не PhD, им надо фигак-фигак и в продакшн. Поэтому они будут искать лазейки как им это сделать быстрее, и если быстрее будет в обход языкового механизма (checked exception), то будут ходит в обход. Один из очевидных путей обхода я показал. Уверен что можно сделать еще удобней обход.
К компилятору Rust'a у меня пожалуй никаких притензий пока нет. К языку некоторые есть. Притензии в плане обработки ошибок — это довольно попсовые притензии. У меня есть и более экзотические :-)
пробросить ее наверх даже проще
На java пробросить наверх ошибку еще проще! Там вообще ничего не надо писать кроме как изменить сигнатуру функции!
void writeToFileIgnoreExceptions() throws IOException { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("filename.txt"), "utf-8")); writer.write("This is a list of my best friends."); System.out.println("I wrote to the file"); }
И никаких try! в теле функции. Но это, очевидно, checked exceptions не спасает. Посмотрите от чего страдают жабисты, и найдите хоть один аргумент почему этого не будет в Rust. Пока я вижу все предпосылки для ровно того же.
То есть если я захочу игнорировать ошибку — я изменю только сигнатуру и добавлю try… catch, если я захочу обрабатывать ошибку, то я напишу что-то в catch, если я захочу паниковать при ошибке, я напишу это в блоке catch. Видите насколько тут меньше писать чем в Rust, при эквивалентных трансформациях кода? И всё равно это не достаточно удобно, люди не любят менять сигнатуры функции. Это не прижилось.
Во-первых, вы сами как считаете, в API фукции должны входить возвращаемые ошибки или нет? Во-вторых, не вижу ничего странного в том, что функции с побочными эфектами и без должны иметь разные сигнатуры, ведь они не взаимозаменяемы. И в третьих, если вас действительно тревожит этот вопрос, можно привести все ошибки к Box.
Я сам не знаю как лучше. Долго думал и о вопросе обработки ошибок, смотрел разные ЯП (haskell, c++, go, ada, SPARK) решения не нашел (пока?). Поэтому и интересуюсь и делюсь мнением, потому, что некоторые знания по этому вопросу имеются. А у вас какой background?
Кроме того, если уж в API функции начали вытаскивать такие детали реализации, как ошибки которые могут возникнуть при работе, то можно вытащить и побольше, например информацию о том, как зависит результат функции от входных параметров (и зависит ли) (SPARK), есть ли побочные эффекты у функции (Haskell), и вообще, много чего еще можно придумать (вплоть до того, какую память функция может потреблять, в каких колличествах, сколько времени может исполняться и какая трудоемкость алгоритмов — это всё бывает нужно).defuz
25.01.2016 21:40+4Это была не попытка аппелировать к безымянному авторитету. Я просто хотел подбросить вам мысль о том, что возможно мы далеко не первые, кто задумались над этим, и что врядли разработчики Rust не додумались учесть таких простых вещей. Наверное, они долго думали чтобы прийти именно к такому решению (поверьте, вопрос обработок ошибок в сообществе Rust обсуждается давно и безостановочно). Я с бОльшей вероятностью готов предположить, что мы с вами чего-то не до конца понимаем, чем то, что разработчики Rust «недоглядели».
Мой основной опыт приходится на Python и другие динамические языки программирования, где обработка ошибок вроде бы есть, и вроде бы даже рай для ваших Java-программистов, но с точки зрения попыток написания надежного кода это полный провал.
Наверное, если нужно «фигак-фигак и в продакшен», то checked exceptions и правда не лучшее решение. Но Rust создавался с целью писать надежный код, и поверьте, там где это действительно необходимо, такая мелочь, как необхомость явно пробрасывать исключение – это последнее, что меня будет волновать в моем коде. А вот невозможность понять, что именно у меня может выбросить функция, меня будет волновать очень даже.
Давайте сойдемся на тем, что checked exceptions – это хорошая идея, если у разработчика на одном из первых мест находится надежность выполнения программы. Если нужно по-быстрому и в продакшен, то наверное нужно брать динамические языки программирования.PsyHaSTe
29.01.2016 10:27Все же я не понимаю, почему нельзя было ввести ключевое слово try { }, которое просто автоматически расставило бы макросы для вызовов всех функций в скопе.
Плюс непонятно, что делать, когда ошибки могут быть принципиально разными. Например на C#, за примером ходить не надо, реализация метода Add стандартного класса List:
int System.Collections.IList.Add(Object item) { ThrowHelper.IfNullAndNullsAreIllegalThenThrow<T>(item, ExceptionArgument.item); try { Add((T) item); } catch (InvalidCastException) { ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T)); } return Count - 1; }
kstep
29.01.2016 12:03+2Если бы это было ключевое слово на уровне компилятора, то тип Result пришлось бы делать встроенным, прибивать гвоздями к компилятору. А сейчас обработка ошибок реализуется полностью на библиотечном уровне. Result и try! — не часть языка, а часть стандартной библиотеки. Можно спорить хорошо это или плохо, но это один из столпов языка: создать мощную обобщённую и достаточно компактную базу, которая позволит выразить на уровне библиотек большинство концепций, которые обычно захламляют и усложняют компилятор. И лично мне, как и многим приверженцам раста, это нравится.
По поводу разных ошибок из одной функции, опять же язык достаточно мощный, чтобы выразить это на уровне типов. Для этого есть два инструмента: типы-суммы (enum, a.k.a. tagged union в C++) и типажи. Обычно для библиотеки создаётся тип-сумма всех возможных ошибок, которые может вернуть библиотечная функция, и расширение списка ошибок достигается увеличением списка доступных в типе-сумме вариантов. Впрочем, про это уже была рассказано не раз, так что повторяться опять и опять смысла не вижу. А для избавления от boiler-plate кода при описании таких типов-сумм для ошибок, язык опять же предоставляет достаточную базу, чтобы реализовать решение на уровне библиотек.PsyHaSTe
29.01.2016 12:22Тогда просто сделать возможность макросам быть многострочными. Псевдокод примера выше:
fn write_to_file_errors_ignored() -> Result<(), io::Error> { try_block! { let mut file = File::create("my_best_friends.txt"); file.write_all(b"This is a list of my best friends."); println!("I wrote to the file"); } }
Тут конечно придется усложнить структуру макросов, дать им возможность инспектировать AST, чтобы оборачивать нужные методы, но зато конечному пользователю [языка] не придется писать 100500 try в каждой строчке, если у него при вызове любого метода может упасть ошибка. Метод, в котором 10 строк подряд могут упасть в любом месте не так уж редки. Плюс функционал многострочных макросов мог бы пригодиться где-нибудь еще.ozkriff
29.01.2016 12:26Макросы и так многострочные. Вышеописанное, наверное, или уже сейчас, или через какое-то время можно будет сделать при помощи плагинов компилятора. Но мне все это видится излишней магией.
Нужны просто HKT и чертовы монады с `do` :).ozkriff
29.01.2016 12:34> Макросы и так многострочные
Они же не зря в обязательном порядке со скобками вызываются :)
0xd34df00d
27.01.2016 00:35+1Поэтому и интересуюсь и делюсь мнением, потому, что некоторые знания по этому вопросу имеются.
Мой опыт и мнение сводятся к тому, что экзепшоны — костыль и гоуту двадцать первого века, их надо избегать, поэтому я себе даже в плюсах пишу Either, где слева boost::variant со списком возможных ошибок, по которому можно пройтись инлайн-визитором и радоваться жизни.
При этом, если вы начнёте комбинировать ошибки с другими монадами (новомодными future'ами, например), то прямая передача и обработка исключений превратится в ад и лапшу. Куда проще что-то вроде
const auto& future = MessageFetchThread_->Schedule (&AccountThreadWorker::SetReadStatus, read, ids, folder); Util::Sequence (this, future) >> [=] (const auto& result) { Util::Visit (result.AsVariant (), [=] (const QList<Message_ptr>& msgs) { HandleUpdatedMessages (msgs, folder); UpdateFolderCount (folder); }, [] (auto) {}); };
если мне надо проигнорировать ошибку, или, если не надо,
Util::Sequence (nullptr, account->SendMessage (message)) >> [safeThis = QPointer<ComposeMessageTab> { this }] (const auto& result) { Util::Visit (result.AsVariant (), [safeThis] (const boost::none_t&) { if (safeThis) safeThis->Remove (); }, [safeThis] (const auto& err) { Util::Visit (err, [safeThis] (const vmime::exceptions::authentication_error& err) { QMessageBox::critical (safeThis, "LeechCraft", tr ("Unable to send the message: authorization failure. Server reports: %1.") .arg ("<br/><em>" + QString::fromStdString (err.response ()) + "</em>")); }, [] (const vmime::exceptions::connection_error&) { const auto& notify = Util::MakeNotification ("Snails", tr ("Unable to send email: operation timed out.<br/><br/>" "Consider switching between SSL and TLS/STARTSSL." "Port 465 is typically used with SSL, while port 587 is used with TLS."), PCritical_); Core::Instance ().GetProxy ()->GetEntityManager ()->HandleEntity (notify); }, [] (const auto& err) { qWarning () << Q_FUNC_INFO << "caught exception:" << err.what (); const auto& notify = Util::MakeNotification ("Snails", tr ("Unable to send email: %1.") .arg (QString::fromUtf8 (err.what ())), PCritical_); Core::Instance ().GetProxy ()->GetEntityManager ()->HandleEntity (notify); }); }); };
0xd34df00d
27.01.2016 00:22Во всём согласен с вами, кроме одной мелочи: побочные эффекты к ошибкам не имеют никакого отношения.
Zelgadis
25.01.2016 23:41Растовский компилятор будет ругаться на игнорируемый разультат работы write_to_file_errors_ignored()
valexey
26.01.2016 00:06он не игнорируется :-) проверьте мой код. нет варнингов.
Zelgadis
26.01.2016 00:22А ведь и в правду… Но это же извращение какое-то, зачем так делать? Насколько я помню с checked exception работают примерно такой — проброс на вверх пока нельзя выполнить: план б или конвертацию в RuntimeException.
Если вы хотите проигнорировать ошибку из Result, мне кажется проще и нагляднее делать `if let`
А то, что нет warnings логично же, сами написали `let _` намекнув компилятору о том, что вам это значение не интересно.valexey
26.01.2016 00:37Я Rust не знаю. Это просто первое что пришло в голову на тему «как и тут проигнорировать checked exceptions, не пробрасывая и без варнингов». Если что, напомню, что выше товарищ утверждал, что в Rust в отличие от java, проигнорировать не выйдет ну вообще никак.
Если внезапно Rust станет популярным, то вот подобное извращение будет наименьшим из того, что народ на нем будет выделывать :-)
Проблема с checked exceptions ровно в том, что народ начал массово их игнорировать именно вот таким образом, потому, что сигнатуру менять по всей иерархии на каждый чих — не удобно, конвертировать тоже лениво. Ну, максимум что какую-то малополезную обработку вставляли. А, ну и еще там были проблемы с тем, что несмотря на checked exceptions всё равно были рантайм-паники, которые могут летать как хотят и которые вроде как не поймать. Поэтому доверия к нижележащему коду, даже если ты обвязал все обработками ошибок, всё равно нет.
Насколько я понимаю, в Rust примерно те же грабли.Zelgadis
26.01.2016 01:03+2Много в расте точно так же игнорируется. Просто компилятор более назойлевый чем у джава + checkstyle (который кстати будет ругаться на съеденое исключение).
У меня еще пока не было желания игнорировать Result в расте не было и все не такой как checked exception.
> Если внезапно Rust станет популярным, то вот подобное извращение будет наименьшим из того, что народ на нем будет выделывать :-)
Не думаю.
> Проблема с checked exceptions ровно в том, что народ начал массово их игнорировать именно вот таким образом, потому, что сигнатуру менять по всей иерархии на каждый чих — не удобно, конвертировать тоже лениво
А в Rust сигнатура такой какой была и остается.
https://github.com/mitsuhiko/redis-rs/blob/master/src/types.rs#L321 Вот типичный паттерн для ошибок в расте. Один единый тип ошибок на всю бибилиотеку. Тип ошибки — тип-суммы с имплементацией типажа Error или «толстая» структура которая хранит этот самый enum. Для всего удобства я себе crate сделал который избавляет от boilerplate кода.
defuz
26.01.2016 01:05+1Сложно назвать то что вы написали «игнорированием ошибки». Я вам подскажу:
file.write_all(b"This is a list of my best friends.").is_ok();
И никаких предупреждений.
Откуда вы вообще взяли все это массовое недовольство checked исключениеми? Такое ощущение, что больше всего недовольства они вызывают именно у вас. Если кто-то взялся писать на Java, а потом начал ныть, что ему «исключения мешают», так может ну его нахрен эту Java, и дело тут не в ней, а в том, что кто-то плохо подбирает инструменты для работы?Halt
26.01.2016 13:37Я думаю речь о том, что checked exceptions плохо подходят для модели «фигак и в продакшн», поскольку подобный код обычно пишется наспех, переделывается много и необдуманно, а в итоге вылезают все прелести связанного полиморфизма и наследования вида: «мы объявили сигнатуру как IOException а там, оказывается, еще надо ловить исключения из совсем другой иерархии…».
defuz
26.01.2016 01:34+3Я кажется понял, в чем заключается наше с вами непонимание. Смотрите, в пишете:
Проблема с checked exceptions ровно в том, что народ начал массово их игнорировать именно вот таким образом, потому, что сигнатуру менять по всей иерархии на каждый чих — не удобно, конвертировать тоже лениво.
Вот только в Rust этой проблемы нет. Дело в том, что тип Result, возвращаемый из функции предполагает только один тип возвращаемой ошибки.
Вот реальный пример описания типа ошибки из моего проекта:
pub enum PackageError { ReadSettings(SettingsError), ParseTheme(ParseThemeError), ParseSyntax(ParseSyntaxError), Io(IoError) }
Как видите, тут довольно много всего странного может произойти. Более того, SettingsError, ParseThemeError, ParseSyntaxError – это тоже типы-суммы, которые в свою очередь представляют собой по 5-7 разных исключительных ситуаций. Так что суммарно этот тип описывает одновременно порядка 20 различных исключений. Но это все еще один тип.
Так что если мне понадобиться расширить мои методы и добавить какие-то новые возможности, которые в свою очередь приведут к появлению новых типов ошибок, я просто расширю свой тип ошибки, но для пользователя этой будет тот же самый тип.
Это означает, что в 99% случаев добавление новых «исключений» не требует вообще никакого изменения в коде, который вызывает мои функции, даже если эти исключения пробрасываются дальше и инкапсулируются в другие, еще более вложенные типы ошибок.
0xd34df00d
27.01.2016 00:17+2Так и
doWork :: _ -> Either Error Int doWork = undefined doMoreWork :: _ -> Int doMoreWork = either (const 0) id . doWork
никто не мешает сделать. Если человек хочет проигнорировать ошибку, он её в любом языке проигнорировать может.
defuz
25.01.2016 18:27+3Насчет checked исключений откровенно странно. Получается, что они плохие просто потому, что разработчики не пользуются ними, перехватывая их каждый раз. Я не видел пока ничего подобного в Rust, не считая boilerplate кода, где хорошей практикой считается просто вызывать `unwrap()`.
На счет обратной совместимости. Во-первых, с ошибками из других библиотек принято работать через типажи `Error` и `Display`. Это означает, что в большинстве случаев вам не прийдется распаковывать значение ошибки и искать там конкретные варианты (конкретный тип исключения). Во-вторых, если вам все-таки нужно это сделать, то я предпочел бы, чтобы компилятор выдал сообщение об ошибке, вместо того, чтобы умолчать тот факт, что API библиотеки, которую я использую, изменилось, и теперь мое приложение может падать с ошибкой, о которой я ничего не знаю и нигде не обрабатываю.
Но в некотором смысле вы правы, ошибки – это часть API. Но, опять же, во многих случаях внутренности ошибки делают приватными, так что добавление нового варианта ошибки (тоже самое, что добавление в сигнатуру метода нового типа исключения в Java) вряд ли как-либо повлияет на работу вашего кода.
0xd34df00d
27.01.2016 00:07Кроме того, представьте себе, что вам например нужно добавить в самую общую, всеми используемую библиотеку еще один тип ошибок который она должна выбрасывать (возможно ранее какие-то функции вообще не выбрасывали ошибок, а теперь должны). Теперь в Rust и в java вам придется пройтись по ВСЕМУ коду (стандартному, стороннему, своему) и везде изменить сигнатуры функций. Ну и до кучи все перекомпилировать конечно.
И в хаскеле тоже, туда ещё пока анонимные типы-суммы не завезли. На плюсах такое, кстати, за счёт вариадиков выражается.
webmasterx
25.01.2016 16:33прощу прощения, не знаком с Rust, но у меня возник вопрос по
Странная фигня №2. В смысле, нет исключений?
Если функция выбрасывает исключение, то мне нужно каждый раз оборочивать вызов в
try!(load_header(file));
?
А что если я хочу использовать цепочку вызовов, и каждая из этих функций выбрасывает различные исключения, например:
getDB()->select()->from()->where()
defuz
25.01.2016 16:53Через несколько релизов появиться возможность вместо макроса try! использовать короткую нотацию, так что цепочка вызовов будет выглядеть примерно так:
get_db()?.select()?.from()?.where()?
Но вообще я сомневаюсь что операции select, from и where могут выбросить какие-либо исключения, скорее всего исключения возникнут уже на последнем этапе – когда будет выполнен execute.
То что исключения могут иметь разные типы решается за счет создания типа-суммы над всеми возможными исключениями и реализацией преобразований из конкретных исключений в исключение-обобщение.
Подробнее об этом можете посмотреть в подразделе книги «Cовмещение собственных типов ошибок».webmasterx
25.01.2016 16:58а если не писать try!, то не скомпилится?
senia
25.01.2016 17:08try! меняет тип с Result<A, E> на A. Если результат дальше используется как A, то не скомпилируется.
Если результат не используется (никуда не присваивается), то будет предупреждение при компиляции.
splav_asv
25.01.2016 17:13Нужно чем-то распаковать Result<T, E>. Это или сделает try! или вручную. Автоматически Result<T, E> к T естественно не приводится.
Astynax
26.01.2016 17:24+1У типа Result есть множество удобных методов для построения цепочек вычислений, напрмер .and_then:
getDB().and_then(select).and_then(from).and_then(where).or_else(foo)
kstep
28.01.2016 17:01Тут проблема в том, что многие люди к этому не привычны. Скажем какой хаскеллист или скалист вполне свободно будет писать и читать такие конвееры, но часто сталкиваюсь с людьми, которые пришли из си++, которым такой подход взрывает мозг.
Astynax
28.01.2016 19:51+1Не нужно гнаться за привычным — привычки нарабатываются. Особенно полезные. И не нужно путать привычность с простотой. Методы Optional/Result в Rust просто освоить и просто выработать привычку, да и в целом концепция — простая, хоть и по началу непривычная для кого то.
P.S. Optional в Java теперь тоже есть и даже аналог and_then имеет среди методов — flatMap.
Eivind
25.01.2016 17:07Заметил интересную тенденцию, почти все, кто сравнивает реализацию на каком-либо языке с реализацией на C++, приводят либо заведомо усложненную, либо вообще не имеющую ничего общего с реализацией на сравниваемом языке, реализацию на C++. С какой целью это делается остается только гадать. Корректная калька первого примера на C++ должна выглядеть как-то так:
#include <iostream> template <class T> struct Display; struct Foo { int x; }; template <> struct Display<Foo> { static auto& fmt( const Foo& self, std::ostream& os ) { return os << "(x: " << self.x << ")"; } }; struct Bar { int x; int y; }; template <> struct Display<Bar> { static auto& fmt( const Bar& self, std::ostream& os ) { return os << "(x: " << self.x << ", y: " << self.y << ")"; } }; template <class T> void print_me( const T& obj ) { Display<T>::fmt( obj, std::cout ) << std::endl; }; int main() { auto foo = Foo{7}; auto bar = Bar{5, 10}; print_me( foo ); print_me( bar ); }
defuz
25.01.2016 17:22+1Такой усложненный пример был приведен совсем не для того, чтобы показать, как на С++ сложно сделать такую простую задачу, а чтобы продемонстировать, сколько непростых вопросов может возникнуть при реализации этой задачи на C++.
Возможно вы не заметили, но ссылка на «хорошее решение» приводится дальше в по тексту самой статье как раз с пояснением, что ничто не мешает на C++ сделать в общем-то тоже самое.Eivind
25.01.2016 17:24«Хорошее решение» демонстрирует перегрузку функций, а в вашем примере то, что в C++ называется специализацией.
defuz
25.01.2016 17:29+1«Хорошее решение» демонстрирует код таким, каким он должен быть, если бы он изначально писался на С++, и писался хорошо.
Ваше решение просто пытается эмулировать принцип работы кода на Rust, в часности работу типажа Display через создание пустой шаблонной структуры. Это сработает только для очень простых примеров, для любой сложной реализации типажей у вас не получится провернуть подобный трюк.Eivind
25.01.2016 17:38«Хорошее решение» демонстрирует код таким, каким он должен быть, если бы он изначально писался на С++, и писался хорошо.
Вы сравниваете «специализацию» с наследованием, и в «хорошем решении» с перегрузкой функций. Дело не в том, как должно быть «хорошо», а в том, что вы сравниваете разные вещи.
Это сработает только для очень простых примеров, для любой сложной реализации типажей у вас не получится провернуть подобный трюк.
Подобные утверждения требуют какого-то подтверждения. Можете привести пример?defuz
25.01.2016 17:46Например, в Rust можно написать такое:
impl<T, U> Foo<T> for Container<U> where T: Bar<U> { /* реализация */ }
Что читается как: для любых типов T и U, таких что тип T реализует типаж Bar<U>, реализовать типаж Foo<T> для всех типов Container<T>.
Или вот вам еще достаточно простой пример:
impl<T> Foo for T where T: Bar { /* реализация */ }
Это читается так: реализовать типаж Foo для всех типов T, которые реализует типаж Bar.
Сможете выразить подобное через шаблоны/специализацию в С++?Eivind
25.01.2016 18:55+1Конечно.
#include <iostream> #include <type_traits> struct not_implemented { }; template <class T> struct Bar : not_implemented { }; template <class T, class = void> struct Foo { static void f() { std::cout << "not implemented\n"; } }; template <class T> struct Foo<T, typename std::enable_if<!std::is_base_of<not_implemented, Bar<T>>::value>::type> { static void f() { std::cout << "implemented\n"; } }; template <class T> struct Container { }; template <class T, class U> struct Bar2 : not_implemented { }; template <class T, class U, class = void> struct Foo2 { static void f() { std::cout << "not implemented\n"; } }; template <class T, class U> struct Foo2<T, Container<U>, typename std::enable_if<!std::is_base_of<not_implemented, Bar2<T, U>>::value>::type> { static void f() { std::cout << "implemented\n"; } }; struct A { }; template <> struct Bar<A> { }; template <class U> struct Bar2<A, U> { }; struct B { }; template <class T> struct Bar2<T, float> { }; int main() { Foo<A>::f(); // "implemented" Foo<B>::f(); // "not implemented" Foo2<A, Container<int>>::f(); // "implemented" Foo2<B, Container<int>>::f(); // "not implemented" Foo2<B, Container<float>>::f(); // "implemented" }
Eivind
25.01.2016 19:14+2А если ввести вот такой хелпер:
template <class T> using where = typename std::enable_if<!std::is_base_of<not_implemented, T>::value>::type;
Можно будет писать так:
template <class T> struct Foo<T, where<Bar<T>>> {}; template <class T, class U> struct Foo2<T, Container<U>, where<Bar2<T, U>>> {};
defuz
25.01.2016 19:26+2Ох жесть. :) А можно полностью избавится от тех реализаций, которые возвращают «not implemented»? Чтобы при попытке вызова
Foo<B>::f();
возникала ошибка на этапе компиляции? Ведь смысл как раз в том, что компилятор на уровне выведения типов понимает, для каких типов типаж реализован, а для каких нет.
И что будет в таком случае при вызове Foo2, у которого типы-параметры не выполняют условиеT: Foo<U>
?Eivind
25.01.2016 19:29+1Конечно, просто не приводить реализацию. Реализация «не реализовано» приведена только для того, чтобы пример компилировался.
template <class T, class = void> struct Foo;
defuz
25.01.2016 19:35В таком случае готов признать, что я недооценивал возможности шаблонов и бог знает чего еще вы там использовали в С++. :) Но раз вы так старались, приведу аналогичный код на Rust:
struct Container<U> { item: U } trait Foo<T> { fn implemented(&self); } trait Bar<U> {} impl<T, U> Foo<T> for Container<U> where T: Bar<U> { fn implemented(&self) {} } struct A; struct B; impl Bar<u64> for A {} impl Bar<f64> for B {} fn main() { let c1 = Container { item: 0u64 }; let c2 = Container { item: 0f64 }; (&c1 as &Foo<A>).implemented(); (&c2 as &Foo<B>).implemented(); // (&c1 as &Foo<B>).implemented(); // (&c2 as &Foo<A>).implemented(); }
Eivind
26.01.2016 17:03Можно сделать и более похоже на Rust:
Сделаем вот такие хелперы:
Код#include <type_traits> struct unimplemented { }; template <class Trait, class For, class Where = void> struct impl : unimplemented { }; template <class...> struct conjunction : std::true_type { }; template <class B1> struct conjunction<B1> : B1 { }; template <class B1, class... Bn> struct conjunction<B1, Bn...> : std::conditional_t<B1::value != false, conjunction<Bn...>, B1> { }; template <class T, class Trait> using implements_single = std::integral_constant<bool, !std::is_base_of<unimplemented, impl<Trait, T>>::value>; template <class T, class... Traits> using implements = conjunction<implements_single<T, Traits>...>; template <class... Ts> using where = typename std::enable_if<conjunction<Ts...>::value>::type;
Halt
26.01.2016 13:46+1Когда в С++ появятся концепты, можно будет вводить явные ограничения на параметр шаблона на уровне языка. К сожалению, сейчас это скорее приведет к вороху сообщений об ошибках, из которых понять причину будет довольно затруднительно.
valexey
25.01.2016 17:25Ну, если не знать языка, то непростых вопросов при решении простой задачи в любом случае возникнет масса. Вон, выше я завалил простыми вопросами, на которые ответы не просты. Просто потому, что я не владею в полном объеме языком Rust :-)
defuz
25.01.2016 17:33+8Вопрос «как мне одновременно модифицировать один и тот же объект в разных местах программы так, чтобы это было одновременно эфективно и безопасно с точки зрения многопоточности и управления памятью» не простой для любого языка программирования.
Возможно, другие языки не заставят вас думать об этом на ранних этапах. Из-за этого может возникнуть иллюзия простоты. Но вам нужно решить для себя самостоятельно, что для вас важнее: потратить меньше времени и ошибочно полагать что вы решили задачу, или потратить больше времени и действительно решить задачу.valexey
25.01.2016 17:48Про «потокобезопасно» в вопросе нигде не было :-) Так что, вероятно, тут придется заплатить цену за то, что не используешь. (имеется ввиду время на изучение не нужного сейчас знания)
Если у меня программа однопоточная by design (а это в ряде задач таки лучше чем многопоточка в том числе и с т.з. производительности, и энергоэффективности, что сейчас стало вновь очень важно), то это будет не иллюзия. А полную безопасность в плане управления памятью Rust все равно не обеспечивает (даже без unsafe блоков).
Также Rust не решает всех проблем и с многопоточкой, поэтому даже потратив больше времени всё равно задача действительно решена не будет. Просто потому, что этих усилий не достаточно чтобы её решить, нужно потратить будет еще. Для многопоточки.
Я не говорю что Rust это что-то плохое или не нужное. Я говорю, что в данной статье наезд на С++ был не в тему. Пример плохой и посыл неправильный. Если я язык не знаю, и не понимаю как тут что работает, то я либо задам простой вопросы и получу непростой ответ, либо просто отстрелю себе ногу при программировании на данном ЯП. Тем или иным способом отстрелю. И на Rust тоже.
Важная характеристика для непростых языков — можно ли их учить постепенно, не всасывая сразу всю спеку языка себе в мозг. Если ты можешь написать что-то тебе полезное на данном непростом языке не читая всей тонны спеков, то язык будет потихоньку изучаться. Если нет, то большинство отправит такой язык в топку. И я боюсь именно это может серьезно ограничить распространение и применимость Rust. Мозилла не вечна, поэтому неплохо бы чтобы Rust вырос за пределы мозиллы (в т.ч. чтобы появились независимые реализации языка) до того, как она схлопнется. Самсунг не будет самостоятельно продвигать этот язык. IMHOdefuz
25.01.2016 18:07+6Во-первых,
А полную безопасность в плане управления памятью Rust все равно не обеспечивает.
Вообще-то Rust гарантирует безопасность управления памятью.
Во-вторых, вопросы потокобезопасности и безопасности работы с данными вообще сильно пересекаются. Например, вы можете запросто получить трудноуловимые баги за мутабельные ссылки даже в простом однопоточном приложении. Прелесть Rust в данном случае заключается как раз в том, что вы как раз наоборот не платите дополнительную цену за то, что ваше приложение потокобезопасно.
Где вы увидели в этой статье наезды на С++? Статья рассказывает об особенностях Rust, которые отличают его от других языков программирования с точки зрения начинающих учить язык. Просто так получилось, что автор решил отталкиваться именно от С++.
Я посмею утверждать, что постепенное изучение Rust с нуля будет значительно проще и быстрее, чем изучение с нуля C++. Другое дело, что на С++ вы будете очень долго и упорно писать ужасный и забагованный код, будучи уверенным при этом, что все сделали правильно. Rust вам не позволит такого делать. По мере изучения, компилятор будет постоянно вежливо и подробно объяснять вам ваши ошибки и даже предлагать, как можно изменить код таким образом, чтобы он заработал корректно.
Опять же, возвращаемся к моему вопросу: что вам важнее, думать что вы что-то сделали правильно или действительно сделать это правильно? Rust позволяет делать простые вещи просто, а сложные возможными. Только ирония в том, что написание сложного кода как на С++, так и на Rust требует более менее одинаковой ментальной нагрузки, но в С++ вы узнаете об этом сильно позже, набив множество шишек и наступив на кучу грабель, а не сразу на этапе компиляции.
Я не вижу никакой пользы в появлении «независимых» реализаций языка, учитывая демократичность разработки спецификаций и существующего компилятора. Rust уже давно вышел за пределы Mozilla. Если показателем для вас является то, что на нем начнут делать интернет-магазины, то скажу вам сразу – нет, на нем не будут делать интернет-магазины, скорее всего никогда. Для этого есть Python/Ruby/PHP/JS. Но интернет-магазины и на С++ особо не пишут.valexey
25.01.2016 18:23+1Вообще-то Rust гарантирует безопасность управления памятью.
Не гарантирует. Утечки памяти + невозможность поймать момент когда память таки закончилась и как-то отработать эту ситацию несколько противоречат этому заявлению.
Например, вы можете запросто получить трудноуловимые баги за мутабельные ссылки даже в простом однопоточном приложении.
Например? Нет, я понимаю, что мутабельность переменных (любых!) это уже сразу unsafe и крайне малопредсказуемый небезопасный код с т.з. например хаскелиста. Но с этой же точки зрения весь Rust также небезопасен и малопредсказуем.
/* скипнуто много философии и рекламы, дабы не заниматься cat /dev/zero > /dev/null */
Я не вижу никакой пользы в появлении «независимых» реализаций языка, учитывая демократичность разработки спецификаций и существующего компилятора.
А очень зря. Независимые реализации приводят например к появлению стандарта на язык. И, как следствие, значительному уточнению спецификации самого языка. А также обеспечивает значительно большую непотопляемость языка.
Если показателем для вас является то, что на нем начнут делать интернет-магазины, то скажу вам сразу – нет, на нем не будут делать интернет-магазины, скорее всего никогда.
Вы за кого меня принимаете?defuz
25.01.2016 18:43+6Не гарантирует. Утечки памяти + невозможность поймать момент когда память таки закончилась и как-то отработать эту ситацию несколько противоречат этому заявлению.
Нет, потому что утечка памяти не имеет отношения в memory safety. Утечку памяти можно создать обычным бесконечным циклом, и компилятор вам тут не помощник. Memory safety – это отсутствие use-after-free, double-free, dangling-pointer, null-pointer access, buffer overflow и т.д. Утечка памяти – это просто утечка памяти, она не разрушает целосность выполнения программы. Хотя Rust позволит вам предотвратить большинство утечек еще до их появления.
Например? Нет, я понимаю, что мутабельность переменных (любых!) это уже сразу unsafe и крайне малопредсказуемый небезопасный код с т.з. например хаскелиста. Но с этой же точки зрения весь Rust также небезопасен и малопредсказуем.
Вот эту часть я не понял. Нет ничего плохого в изменемых состояниях, если пользоватся ними контролируемо. Проблема изменямых состояний в том, что изменение чего-то в одном месте может повлечь неконтролируемые и ошибочные изменения состояния в другом месте. В Rust такая ситуация невозможна, поскольку в один момент времени мутабельным состоянием управляет только один объект. Так что фраза «Но с этой же точки зрения весь Rust также небезопасен и малопредсказуем» мне вообще не понятна.
В независимых реализациях пока просто нет особой необходимости – компилятор Rust полностью открыт и без проблем принимает предложения по улучшению. Зачем расщеплять экосистему двумя потенциально не совместимыми компиляторами, если и один пока что отлично справляется? У языка D уже был печальный опыт Tango vs Phantom. Я бы лично не хотел повторения подобной истории для Rust.
Вы за кого меня принимаете?
За собеседника. Я лишь хотел сказать, что Rust никогда не задумывался как «универсальный язык программирования на все случаи жизни». Так что если он не удовлетворяет лично ваши (либо чьи-либо еще) потребности, то, возможно, это просто потому, что он не должен этого делать? К сожалению, многие высказывают недовольство Rust, аргументируя это тем, что «на Java/Python/PHP что-то там можно сделать проще».
defuz
25.01.2016 18:48На счет перехвата memory overflow – все зависит от конкретной реализации контейнера. Структуры данных в libstd паникуют в случае memory overflow. Это компромис. Никто не мешает создать такой контейнер, который будет выдавать обычную ошибку в случае, если произойдет memory overflow. Для этого есть прямой доступ к функциям аллокатора памяти. И опять же, такие контейнеры могут быть полностью безопасными.
Так же добавлю, что в случае memory overflow и вызова panic, Rust гарантировано правильно завершит поток: закроет все дескрипторы, сокеты и т. д. Это и есть memory safety, а конкретный способ того, как пользователю возвращается memory overflow (и возвращется ли вообще) тут не причем.valexey
25.01.2016 20:27Кстати, а действительно, каким образом в Rust идет управление ресурсами, которые не память? Что-то вроде RAII имеется?
defuz
25.01.2016 20:35Именно RAII и используется, как для памяти, так и для других ресурсов: файлов, сокетов, мьютексов, локов, и так далее.
valexey
25.01.2016 20:38Т.е. деструкторы таки есть?
defuz
25.01.2016 20:41А почему бы им не быть? Может, в каком-то не таком смысле, который вы ожидаете, но есть типаж `Drop`, который гарантированно вызывается, если переменная покидает область видимости. И у всех не «plain old data» типов он реализован.
valexey
25.01.2016 20:47Потому, что это довольно редкое явление среди разнообразных ЯП :-) И очень хорошо, что Rust это исключение из правила.
PS. Да, я уже успел посмотреть на это. Это реализовано ровно так как я ожидал. Ведь в Rust всё (ок, не все, но многое) делается через одно место — через trait'ы. В том числе и closures например.
vintage
25.01.2016 20:58+11. Эквивалентный код на D:
import std.stdio; import std.conv; struct Foo { int x; string Display( ) { return "Foo(x: " ~ x.to!string ~ ")"; } } struct Bar { int x; int y; string Display( ) { return "Bar(x: " ~ x.to!string ~ ", y: " ~ y.to!string ~ ")"; } } struct Broken { } void print_me( T )( T obj ) { writefln( "Value: %s", obj.Display() ); // Error: no property 'Display' for type 'Broken' } void main() { auto foo = Foo( 7 ); auto bar = Bar( 5, 10 ); auto broken = Broken(); print_me(foo); print_me(bar); print_me(broken); // Error: template instance app.print_me!(Broken) error instantiating }
2. Требовать от программиста проверять все типы исключений — разумно. Требовать делать это строго в непосредственно вызывающей функции — нет. У программиста должна быть возможность обработки исключительной ситуации на правильном уровне, не занимаясь ручным или полуавтоматическим (макросы) пробросом ошибки между функциями.
3. Классная штука, безусловно. Хотя, с реализацией кажется перемудрили. Мне кажется можно было бы сделать проще.defuz
25.01.2016 21:10Да, возвращение ошибок как значения требует явного проброса ошибок наверх в случае необходимости (выше я уже писал, что через несколько месяцев это можно будет делать всего одним символом –
foo()?
). Но такой подход позволяет строить очень выразительные конструкции с помощью комбинаторов вродеand_then
,or_else
,unwrap_or(default)
и многих других. Поэтому там, где С++/Java появляются лесницы из try-catch блоков, в Rust удается получить довольно выразительные конструкции, например:
// попытатся получить значение, а если случилась ошибка, // то использовать значение по-молчанию – 0. let value = foo().unwrap_or(0);
На счет «перемудрили»: когда начинаешь разбираться, то оказывается что не все так просто, и перемудрили вовсе не просто так, а потому что на то есть объективные причины.Eivind
25.01.2016 21:15Поэтому там, где С++/Java появляются лесницы из try-catch блоков, в Rust удается получить довольно выразительные конструкции
Но что мешает использовать тот же подход в C++/Java?defuz
25.01.2016 21:25Первое, что приходит в голову – отсутствие типов-сумм:
pub enum Result<T, E> { Ok(T), Err(E), }
Хотя что-то подобное наверное можно сделать через union в С++.
Второе, чего явно не хватает – это exhaustive matching. Rust не позволит вам не обработать все возможные варианты типа-суммы. Именно эта особенность не позволяет «просто проигнорировать» ошибку. В С++/Java ничего подобного нет.
Третее – не уверен, что в С++ получится c такой же легкостью передавать в комбинаторы замыкания, которые в результате будут заинлайнены и не будут создавать каких-либо накладных расходов. В Java точно не получится. Что-то вроде:
// попытаться открыть первый файл, и если не получилось, то попытаться открыть второй let file = File::open("output1.txt").or_else(|| File::open("output2.txt")).unwrap()
valexey
25.01.2016 21:33Первое, что приходит в голову – отсутствие типов-сумм
К жабе алгебраические типы вполне прикручиваются: github.com/sviperll/adt4j
Впрочем, если хочется без прикручивания, то там давно есть scala.defuz
25.01.2016 21:43Алгербраические типы вряд ли могут быть особо юзабельными в языках, в которых нет нормального pattern matching. Вот как раз на Scala я могу себе представить реализацию полностью аналогичного подхода. Думаю, Scala и замыкания должна уметь инлайнить, ведь так? А вот на Java/C++ я пока представить чего-то аналогичного и удобного не могу. Но Scala – далеко не системный язык программирования.
valexey
25.01.2016 21:47+1На плюсах замыкания/лямбды также отлично инлайнятся. То есть на выходе ровно тот же код что был бы без них. Я например использовал лямбды на С++ в коде для микроконтроллера у которого 512 байт ОЗУ.
Вменяемого pattern-matching'a и нормальных алгебраических типов (не нормальные алг. типы с ненормальным матчингом сейчас уже можно сделать, это будет юзабельно, не сказать чтобы очень удобно) конечно не хватает.
senia
25.01.2016 21:49Думаю, Scala и замыкания должна уметь инлайнить, ведь так? А вот на Java/C++ я пока представить чего-то аналогичного и удобного не могу.
Как раз тут больше надежды на Java и только как следствие улучшения в Scala. Лямбды в Java 8 очень сильно улучшены и JIT дает неплохие результаты. Сама же Scala в этом отношении может только то, что позволяет ей JVM.
senia
25.01.2016 21:47На правах фаната scala вмешаюсь.
Да, в scala такой подход практикуется и развивается. И он весьма удобен.
Более того, наличие for-comprehensions, позволяет использовать этот подход более полноценно.
Но все-таки у Rust есть неоспоримое преимущество: минимальные накладные расходы. Если в scala Option это либо ссылка на None, либо ссылка на Some, содержащая ссылку на T, то в Rust это структура, содержащая хедер и T. При желании все это находится на стеке.
В Dotty идут эксперименты по замене Option на 2 переменных: Boolean и T, но это лишь частный случай. Тогда как в Rust это из коробки для всех enum.
О массивах структур в scala нельзя и мечтать до Value Types в java, а это не раньше java 10.
Лично я очень надеюсь на скорейшее развитие Rust в качестве инструмента системного программирования.valexey
25.01.2016 21:49А разве jvm не размещает объекты на стеке, тогда когда это возможно? Там же есть эта оптимизация через escape analysis.
senia
25.01.2016 21:54Не всегда это возможно. И это оптимизация в рантайме. Это не только требует прогрева, но и не гарантируется.
senia
25.01.2016 22:10Если интересно — вот часть работы, которая ведется в scala в том числе и для избавления от оверхедов, которых в Rust нет изначально: видео, слайды. Докладчик — darkdimius.
valexey
25.01.2016 22:30Спасибо. Кстати, надо посмотреть что в этом плане есть в Kotlin'e, если есть вообще. Ну и как они там с ошибками борются. Насколько я помню, они там пытались по крайней мере null dereference откусить.
Yuuri
02.02.2016 18:02+2Более того, ЕМНИП, если T реализует NonZeroable (например, ссылки), то в качестве None будет использоваться «невозможное» нулевое значение, и оверхеда от Option не будет вообще.
Eivind
25.01.2016 22:10Всё, что «отсутствует» в C++ элементарно может быть реализовано:
#include <boost/variant.hpp> #include <iostream> #include <utility> template <class T, class E> class Result { public: template <class U> Result( U&& obj ) : value( std::forward<U>( obj ) ) { } T unwrap() && { assert( value.which() == 0 ); return std::move( boost::get<T>( value ) ); } template <class F> Result or_else( F f ) && { if ( value.which() == 0 ) { return std::move( boost::get<T>( value ) ); } else { return f(); } } private: boost::variant<T, E> value; }; class File { public: static Result<File, bool> open( const std::string& s ) { if ( s == "exist" ) { return File{}; } else { return false; } }; }; int main() { auto file = File::open( "unexist" ).or_else( [] { return File::open( "exist" ); } ).unwrap(); }
Третее – не уверен, что в С++ получится c такой же легкостью передавать в комбинаторы замыкания, которые в результате будут заинлайнены и не будут создавать каких-либо накладных расходов
Всё будет заинлейнено.splav_asv
25.01.2016 22:25+4Проблемы две:
1. Это не стандартный подход. Это увеличивает порог входа для новых разработчиков, уменьшает возможности по заимствованию кода из одного проекта в другой.
2. Понятие элементарно видимо сильно отличаются. «Шаблонная магия» — для многих и многих программистов на C++ это именно магия. Здесь конечно более читаемо, чем ваша эмуляция типажей, но все равно слегка «Ох жесть.»
Итого, на мой взгляд, в личном проекте так делать наверное можно, в очень крупном, где и так полно специфичных вещей — тоже. Но в остальных наверное не стоит.alexeibs
26.01.2016 16:35Нет там никакой магии, простая обертка над boost::variant
splav_asv
26.01.2016 17:20+4Да это понятно, но выглядит довольно монструозно в пересчёте на функциональность. Кстати boost всё же не часть языка, если писать без него вероятно еще более монструозно будет. Попытки восполнить недостающие конструкции таким образом имеют право на жизнь, но очень усложняют читаемость.
Еще вопросы возникнут с раскапыванием первопричины ошибки при опечатке, особенно с непривычки.
defuz
25.01.2016 22:47+2Опять же повторюсь, что все это можно конечно эмулировать, но это не будет иметь никаких особых преимуществ без полноценной поддержки pattern matching на уровне языка. Вот его вам врядли удастся нормально эмулировать.
0xd34df00d
27.01.2016 00:57Во-первых, как вам мой вариант чуть выше, например? Лямбды эти все с типами — для частного случая обработки разных ошибок сойдёт, ИМХО.
Во-вторых, дружно ждём и жаждем Mach7 в языке!
vintage
26.01.2016 00:32Эти выразительные конструкции легко сделать и в D:
import std.stdio; int foo(){ throw new Exception( "xxx" ); } Result unwrap_or( Result )( lazy Result expr , lazy Result def ){ try { return expr(); } catch( Exception e ) { return def(); } } void main() { writeln( foo().unwrap_or(0) ); }
Только так никто не делает, ибо игнорирование всех ошибок без разбора чревато печальными последствиями.
Зачастую всё же оказывается, что именно перемудрили. Вот читаю я исходники стандартной библиотеки D и диву даюсь как там всё переусложнено на ровном месте. А ведь можно, например, проще и быстрее раз в 10.
guai
26.01.2016 17:51-1Считаю, Андрей прав. Есть некий лимит сложности, после которого на язык махнет рукой большинство индустрии, и пойдет он пылиться на полку, как многие замечательные языки до него. Руст отожрал львиную долю этой сложности уже на проверки заимствований. Стоило ли оно того — хз.
У D же, напротив, все траблы чисто организационные. Допилить stdlib до нормальной работы без GC, развернуть аналогичный хайп, статьи там, конференции, книжки. Бабла, короче, закинуть, и будет норм. В самом языке ничего менять не надо. И он гораздо более привычен для сишников, и лимит сложности еще не исчерпан.valexey
26.01.2016 17:56Да, в плане D тут скорее не парадокс Блаба играет ключевую роль, а парадокс Бабла :-) Чтобы было бабло на раскрутку языка нужно чтобы язык был уже раскручен.
guai
26.01.2016 20:16-1Ну или поддержка серьёзной конторы.
За руст башляет мозилла (не знаю, откуда у них деньги, фонды наверное какие-то, спонсоры); груви был под pivotal, щас присосался к apache; цейлон делает RedHat; про скалу не знаю; го понятно чей; котлин от джетбрэйнс — почти все более-менее успешные языки, которые я заметил за последние сколько-то лет кем-то оплачивались.valexey
26.01.2016 20:26Мозилла вполне себе зарабатывает. Гугл ей платил за то, что гугл был дефолтным поисковиком, яндекс вроде как платит скорее всего (яндекс дефолтен для русской сборки) и так далее. То есть у мозиллы есть схемы монетизации.
Да, многие языки — это языки одной корпорации. Rust, Go, Java, цейлон. И, что характерно, подобные языки все как один не имеют стандарта вменяемого :-) Ибо есть ровно одна реализация (да, я знаю, что у java были сторонние релизации, от той же IBM например), которая является эталонной и которую тащит одна корпорация (в основном).
Ярко отличается от этих языков язык С++. И, например, Ада (но путь её становления совсем другой конечно, не как у С++).defuz
26.01.2016 22:53Какую роль стандартизация играет для ЯП?
Eivind
26.01.2016 23:16Принципиальную для промышленного использования. Практически весь софт для авионики написан на Ada, C и C++.
defuz
27.01.2016 09:40Вы точно не путаете причину и следствие? Давайте сразу отложим в сторону Аду – ее судьба была определена еще до ее создания. Другие языки кроме C/C++/Ada промышленно не используются?
valexey
27.01.2016 09:52В авионике — нет. Потому, что там нужна надежность. Mission critical solution. Это вам не браузер :-)
Пока у вас нет стандарта на язык, у вас нет четкой формальной неизменной спеки на язык, следовательно вы даже корректность компилятора проверить не можете.defuz
27.01.2016 10:24+2Во-первых, наличие спецификации и стандартизация языка это далеко не одно и то же. Во-вторых, я очень скептически отношусь к надежности, основанной на толстых пачках бумажек. Кто проверяет корректность спецификации? Человек? Что проверяет соответствие компилятора спецификации? Человек? Каким образом это увеличивает надежность?
splav_asv
26.01.2016 22:15+5Лимит сложности понятие относительное. Rust субъективно проще C++, а он является промышленным стандартом во многих областях. Так что подвинуть с такой точки зрения шансы подвинуть C++ в некоторых нишах у него есть.
Сложность в проверке заимствований не на пустом месте возникла, при написании многопоточного кода на C++ практически всё тоже самое надо держать в голове, только вот подсказать об ошибке, как это делает Rust, некому. Так что это по сути навязанный инструмент статического анализа из коробки. А ими пользуются, причём добровольно.
ozkriff
> Что Rust действительно делает – так это отделяет наследование от полиморфизма…
> Хотите верьте, хотите – нет, но по крайней мере в Rust 1.6 нет вообще никаких специальных инструментов для наследования структур…
Я уже везде об этом успел поныть, но напишу таки еще раз — хочу это самое наследование реализаций, отделенное от полиморфизма. Как анонимные поля в Go или что в таком духе. Хаки с Deref это не полноценное решение, а вручную «пробрасывать» методы в структуру-обертку — боль.
А официальной активности на этом фронте (https://github.com/rust-lang/rfcs/issues/349) как-то совсем не видно, только парочка статей с размышлениями была, и то н-цать месяцев назад :(.
defuz
Так ведь собираются к концу этого года добавить, я писал об этом в переводе Rust в 2016 году. За наследование возьмутся сразу после того, как реализуют специализацию, без нее наследование делать не будут.
Monnoroch
В D специально для этого есть alias this!