Pull to refresh

Comments 134

> Что Rust действительно делает – так это отделяет наследование от полиморфизма…
> Хотите верьте, хотите – нет, но по крайней мере в Rust 1.6 нет вообще никаких специальных инструментов для наследования структур…

Я уже везде об этом успел поныть, но напишу таки еще раз — хочу это самое наследование реализаций, отделенное от полиморфизма. Как анонимные поля в Go или что в таком духе. Хаки с Deref это не полноценное решение, а вручную «пробрасывать» методы в структуру-обертку — боль.

А официальной активности на этом фронте (https://github.com/rust-lang/rfcs/issues/349) как-то совсем не видно, только парочка статей с размышлениями была, и то н-цать месяцев назад :(.
Так ведь собираются к концу этого года добавить, я писал об этом в переводе Rust в 2016 году. За наследование возьмутся сразу после того, как реализуют специализацию, без нее наследование делать не будут.
В D специально для этого есть alias this!
Эээммм?

Управление памятью — редкая проблема? Мне казалось, что это стало чуть ли не решающим фактором, позволившим джаве втоптать C++ в землю и забрать себе всю энтерпрайз разработку.
Это заблуждение из девяностых. В современном С++ не все идеально, но при соблюдении нескольких простых правил никаких отстрелов ног не происходит, об этом что Саттер, что Страуструп, да и другие популяризаторы современного С++ на каждой конференции говорят.
Мне кажется, в статье как раз довольно тонко обыгрываются эти самые «нескольких простых правил», в разделе «Использование указателей после освобождения памяти». ;)

Я сильно сомневаюсь, что все прямо так радужно. Особенно если речь идет о многопоточных системах, на которые мы должны ориентироваться в первую очередь в 2016 году. Каким образом C++ позволяет убедиться, что можно передавать между потоками, а что нельзя? Какие данные должны быть доступны одновременно из нескольких потоков, а какие нет? Только на чтение, или можно изменять? А когда? Все это сложная, «монотонна и скучная» работа. И это только вопрос времени, когда и где именно вы ошибетесь. И какие у этого будут последствия.
На самом деле, можно рассматривать языки программирования не в вакууме, а вместе с командой, которая что-то программирует. В этой ситуации С++ дает техлиду свободу создать любую парадигму, какая лучше всего подходит именно для его задач. А Rust навязывает ту парадигму, которую в него заложили создатели. Другими словами, компилятор С++ программисту пытается всячески помочь, а от ошибок ограждает код-ревью и опыт, а в Rust компилятор пытается сам, в меру своего разумения, защитить программиста от глупых ошибок.
Я никак не могу с вами согласился. Да, есть базовые правила, вроде «никогда не может быть больше одной мутабельной ссылки». Но обычные ссылки – это далеко не все, что предоставляет вам Rust. Это только основа. Дальше у вас есть выбор из кучи инструметов, и возможность выбрать именно ту модель, которая наиболее приемлема для вас и вашей задачи.

Хотите – используйте unsafe. Хотите – используйте подсчет ссылок Rc/Arc. В ближайшем будущем у вас появится возможность еще и подключить сборку мусора, если захотите. И не просто подключить, а выбрать ту его реализацию, которая вам больше всего подойдет.

Если вам интересна эта тема, почитайте главу «Выбор гарантий» из официального руководства Rust. Она дает лишь основное представление о том, сколько различных инструментов предлагает Rust прямо из коробки для решения одних и тех же задач, давая при этом разную степерь удобства и разные гарантии по эфективности и безопасности.
Assembly навязывает ещё меньше парадигм.
> Управление памятью — редкая проблема

Я не вижу, где бы такое утверждалось. Речь как раз о том, что «безопасность и _ювелирное_ обращение с памятью» не так уж и сильно нужны большинству программистов, по крайней мере по их представлениям.
Странная фигня №1. Полиморфизм в стиле Rust

Погодите, но ведь это же статический полиморфизм (то есть полиморфизм времени компиляции) продемонстрирован, разве нет? Это как type class в Haskell если без расширизмов от ghc (без existential types), ну и как это будет в C++ когда введут наконец концепты в стандарт.

Как на Rust будет динамический полиморфизм (времени исполнения)? И какой при этом будет оверхед?
Динамический полиморфизм делается двумя способами:

1. Первый способ тупой: использовать тип-суммы. Но такой подход очевидно не расширяемый (нельзя добавить новые варианты enum). Оверхед – постоянное выполнения match.

2. Второй способ: типажи-объекты. К ним можно привести данные любого типа, которые реализуют определенный трейт. Представляют собой, собственно, данные и виртуальную таблицу. Вот на эту виртуальную таблицу и появляется оверхед. Где-то тоже самое, что приведение к абстрактному классу в C++.
Ага, спасибо. Посмотрю.

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

Оверхед от типажей-объктов будет при каждом вызове функции, или только в тех случаях когда компилятор действительно не может определить на этапе компиляции какую функцию нужно дернуть (в С++ компиляторах эта оптимизация делается всегда когда возможно).
Тип-сумма – один из алгебраических типов данных. Вообще есть еще тип-произведение, или проще говоря кортеж. Но вообще да, в Rust используются алгебраические типы данных.
Оверхед от типажей-объектов будет всегда, потому что это по-определению «данные какого-то неизвесного типа, реазующего заданный типаж». В Rust вы всегда явно контролируете то что используете – static dispatch или dynamic dispatch. Объект нужно явно привести к типаж-объекту перед использованием – это отдельный тип.

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

Хм. Интересно. А как в Rust статическим полиморфизмом обходится например такая задача, как написание GUI? Это вроде бы та область, где активно используется динамическая диспетчеризация/полиморфизм.

Примеры можно?
Если у нас есть контейнер, который содержит в себе виджеты одинакового типа (пример: абстрактный тип 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.
Спасибо. С т.з. типов более-менее понятно.

А можно пояснить что в последнем примере будет в плане размещения переменных/объектов в памяти? w1 и w2 будут на стеке? При приведении вида Box::new(w1) as Box значение w1 будет скопировано в кучу, таким образом, физически у нас станет на какой-то момент два w1?

Обычно в гуях какой-нибудь Layout держит в себе лишь указатели/ссылки/смартпоинтеры (в зависимости от ЯП) на виджеты, но не сами эти объекты.
В данном случае Box<Widget> – это и есть смарт-поинтер на кучу. Вообще в Rust часто используются умные указатели, например &str – это указатель на строковый слайс, представляющий собой ссылку на начало строки и к-во байт.

В данном случае в векторе layout.items будут хранится пары ссылок: ссылка на данные в куче и ссылка на виртуальную таблицу.

Теперь конкретно по-поводу того, что происходит в main:

Cначала w1 и w2 создаются на стеке.

Затем они перемещаются в кучу, при передаче их в конструктор Box::new. Это тоже самое что копирование, с той лишь разницей, что после перемещения изначальное значение будет не доступно. Так что у вас не будет такого момента, когда существуют одновременно две копии объекта (технически они будут, но переменные w1 и w2 больше не доступны для использования, поскольку их значения были перемещены).

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

Компилятор может оптимизировать процес перемещенний и создавать объект сразу на месте выделенной под него памяти в куче. Я описал процес так, как он происходит по своей сути, шаг за шагом.
А как это всё будет выглядеть если у меня два layout'a (или других подобных компонента) и в обоих нужен w1 (точнее ссылка на него)? В обоих местах естественно нужна возможность w1 модифицировать.

Просто так поперемещать, насколько я понимаю, уже не выйдет.
Интересно, что это за интерфейс, в котором на одном экране один и тот же виджет используется несколько раз? Какой это мог бы быть виджет вообще?
В гуйне обычно есть несколько сущностей которые хранят в себе ссылки на гуй-компонентины. Простой пример — layout этот + диспетчер клавиатуры, которому нужно знать кому посылать сообщение в случае если пользователь клавишу вдавил. Подобных штук там довольно много разных.

Один и тот же виджет входит сразу в несколько иерархий и списков виджетов.
Никогда такого не видел. С гуйнёй работаю последние 9-10 лет.
Я гуйней тоже занимался. Писал гуйно-фреймворк местный для Scada.

Да и судя по архитектуре Swing — там подобное тоже имеется.
Можно использовать Mutex, т.е. вроде бы должно получится, но не пробовал:
items: vec![Mutex::new(Box::new(w1) as Box), Mutex::new(Box::new(w2) as Box)];

достать — items[0].lock()
Чтобы в полной мере ответить вам на ваш вопрос, мне пришлось бы рассказать вам о концепции лайфтаймов, но боюсь, это был бы слишком длинный комментарий.

Если коротко, то у вас может быть много вариантов ссылок на типаж-объект. Box – это владеющая ссылка на кучу. В то же время, вы можете хранить ссылку на типаж-объект как &'a Widget. Это будет ссылка-заимствование. В данном случае 'a – это метка, указывающая на время жизни этого виджета. Таких ссылок может быть несколько.

Сразу скажу, что у вас принципиально не получится сделать несколько изменяемых ссылок на один и тот же объект (&'a mut Widget). Это фундаментальное ограничение Rust.

Варианта решения три:

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

Второй метод – это использовать умные указатели: Rc (reference counter, для однопоточного приложения) или Arc (atomic reference counter, для многопоточного приложения). Таким образом у вас появится возможность получать мутабельную ссылку на объект, но у вас возникнут накладные расходы во время выполнения, связанные с необходимостью подсчета ссылок. Также у вас появится возможность устроить утечку памяти, если виджеты будут циклически владеть ссылками друг на друга, как и всегда при использовании механизма подсчета ссылок.

Есть еще третий путь – interior mutability. Этот метод подходит для случаев, когда данные хоть и изменяются технически, но такие изменения не отражаются на их состоянии. Пример: мемоизация/кеширование выполнения функции.
На счет «всегда» я возможно погорячился, потому что на самом деле нет никаких преград для LLVM заинлайнить вызов функции по ссылке. Но нужно понимать, что если существует возможность избавится от виртуальной таблицы на этапе компиляции, то вам вообще не нужен типаж-объект в данном конкретном случае.

Гораздо больший оверхед от типаж-объектов заключается в том, что поскольку это unsized type (неизвестно какого размера могут быть данные), то они всегда живут только в куче, и передаются/принимаются только по ссылке на кучу. Обычные объекты в Rust в большинстве случаев живут прямо в стеке и вы либо передаете ссылку на стек, либо перемещаете их по значению (move-semantic).
Трейт-объекты, конечно же, могут жить на стеке:
use std::io::Write;

let buf: Vec<u8> = Vec::new();
let w: &mut Write = &mut buf;
Странная фигня №2. В смысле, нет исключений?

По моему, в Rust получилось нечто, что очень сильно напоминает checked exceptions в java со всеми их плюсами и минусами. А если учесть, что checked exceptions были признаны ошибкой…
Похожего у них только то, что в сигнатуре виден тип возращаемой ошибки. Принцип реализации совершенно другой. В Rust есть «ошибки», и есть «паники». Полученное значение Result можно всегда преобразовать в панику потока, просто вызвав метод `unwrap`. Паника гарантировано корректно завершит текущий поток, по-сути это unhandled exception.

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

Ну, это и есть основное отличие 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 вам придется пройтись по ВСЕМУ коду (стандартному, стороннему, своему) и везде изменить сигнатуры функций. Ну и до кучи все перекомпилировать конечно.

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

Если вы говорите про подход с обработкой ошибок с помощью 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 обеспечит, чтобы код, зависящий на старую версию библиотеки, не сломался.
Если вы говорите про подход с обработкой ошибок с помощью 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! ну и добавилось оверхеда в случае если ошбок не было.
Если бы вы относительно хорошо знали 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» тем более.

И последнее, вы сами дали четкий сигнал компилятору, что хотите проигнорировать возвращаемое значение из замыкания, используя специальное имя переменной "_". Если бы вы использовали нормальное имя, компилятор выдал бы вам предупреждение.
Я Rust вообще не знаю, и не скрываю этого :-)

А штука с unwarp совершенно не эквивалентна тому, что написал я, и тому, что на java — это не игнорирование ошибок, а паника в случае если ошибка возникла. Также unwarp придется вставлять в 100500 строчках внутри блока из изменять их все, если я таки решу ошибку как-то обработать.

Нормальный проброс ошибок не проще из за того, что в этом случае спецификация функции зависит от её раализации. Именно поэтому checked exceptions в java считаются не самой лучшей идеей.

Если вы читали доку на стандартную либу по Rust, то должны знать откуда я взял этот пример и как его изменил :-) Поэтому ваши примеры я уже видел :-)
Не обижайтесь, но мне кажется, что вы сейчас наглядно демонстрируете проявление парадокса, о котором говорится в статье. :) Вам зачем-то нужно обязательно убедится, что в Rust что-то сделано не так. Для этого вы берете опыт, который у вас есть на Java, и проецируете его на Rust, которого вы толком не знаете, и тут же выдаете вердикт: фигня, так работать не будет.

То-то сотни разработчиков Rust, многие из которых MS и PhD по компиляторам такие тупые и не додумались предложить лучшего решения. :)

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

Нормальный проброс ошибок не проще из за того, что в этом случае спецификация функции зависит от её раализации. Именно поэтому checked exceptions в java считаются не самой лучшей идеей.
Во-первых, вы сами как считаете, в API фукции должны входить возвращаемые ошибки или нет? Во-вторых, не вижу ничего странного в том, что функции с побочными эфектами и без должны иметь разные сигнатуры, ведь они не взаимозаменяемы. И в третьих, если вас действительно тревожит этот вопрос, можно привести все ошибки к Box<Error>.

Не обижайтесь, но мне кажется, что вы сейчас наглядно демонстрируете проявление парадокса, о котором говорится в статье. :) Вам зачем-то нужно обязательно убедится, что в 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), и вообще, много чего еще можно придумать (вплоть до того, какую память функция может потреблять, в каких колличествах, сколько времени может исполняться и какая трудоемкость алгоритмов — это всё бывает нужно).
Это была не попытка аппелировать к безымянному авторитету. Я просто хотел подбросить вам мысль о том, что возможно мы далеко не первые, кто задумались над этим, и что врядли разработчики Rust не додумались учесть таких простых вещей. Наверное, они долго думали чтобы прийти именно к такому решению (поверьте, вопрос обработок ошибок в сообществе Rust обсуждается давно и безостановочно). Я с бОльшей вероятностью готов предположить, что мы с вами чего-то не до конца понимаем, чем то, что разработчики Rust «недоглядели».

Мой основной опыт приходится на Python и другие динамические языки программирования, где обработка ошибок вроде бы есть, и вроде бы даже рай для ваших Java-программистов, но с точки зрения попыток написания надежного кода это полный провал.

Наверное, если нужно «фигак-фигак и в продакшен», то checked exceptions и правда не лучшее решение. Но Rust создавался с целью писать надежный код, и поверьте, там где это действительно необходимо, такая мелочь, как необхомость явно пробрасывать исключение – это последнее, что меня будет волновать в моем коде. А вот невозможность понять, что именно у меня может выбросить функция, меня будет волновать очень даже.

Давайте сойдемся на тем, что checked exceptions – это хорошая идея, если у разработчика на одном из первых мест находится надежность выполнения программы. Если нужно по-быстрому и в продакшен, то наверное нужно брать динамические языки программирования.
Все же я не понимаю, почему нельзя было ввести ключевое слово 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;
        }
Если бы это было ключевое слово на уровне компилятора, то тип Result пришлось бы делать встроенным, прибивать гвоздями к компилятору. А сейчас обработка ошибок реализуется полностью на библиотечном уровне. Result и try! — не часть языка, а часть стандартной библиотеки. Можно спорить хорошо это или плохо, но это один из столпов языка: создать мощную обобщённую и достаточно компактную базу, которая позволит выразить на уровне библиотек большинство концепций, которые обычно захламляют и усложняют компилятор. И лично мне, как и многим приверженцам раста, это нравится.

По поводу разных ошибок из одной функции, опять же язык достаточно мощный, чтобы выразить это на уровне типов. Для этого есть два инструмента: типы-суммы (enum, a.k.a. tagged union в C++) и типажи. Обычно для библиотеки создаётся тип-сумма всех возможных ошибок, которые может вернуть библиотечная функция, и расширение списка ошибок достигается увеличением списка доступных в типе-сумме вариантов. Впрочем, про это уже была рассказано не раз, так что повторяться опять и опять смысла не вижу. А для избавления от boiler-plate кода при описании таких типов-сумм для ошибок, язык опять же предоставляет достаточную базу, чтобы реализовать решение на уровне библиотек.
Тогда просто сделать возможность макросам быть многострочными. Псевдокод примера выше:
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 строк подряд могут упасть в любом месте не так уж редки. Плюс функционал многострочных макросов мог бы пригодиться где-нибудь еще.
Макросы и так многострочные. Вышеописанное, наверное, или уже сейчас, или через какое-то время можно будет сделать при помощи плагинов компилятора. Но мне все это видится излишней магией.

Нужны просто HKT и чертовы монады с `do` :).
> Макросы и так многострочные

Они же не зря в обязательном порядке со скобками вызываются :)
UFO just landed and posted this here
UFO just landed and posted this here
Растовский компилятор будет ругаться на игнорируемый разультат работы write_to_file_errors_ignored()
он не игнорируется :-) проверьте мой код. нет варнингов.
А ведь и в правду… Но это же извращение какое-то, зачем так делать? Насколько я помню с checked exception работают примерно такой — проброс на вверх пока нельзя выполнить: план б или конвертацию в RuntimeException.

Если вы хотите проигнорировать ошибку из Result, мне кажется проще и нагляднее делать `if let`

А то, что нет warnings логично же, сами написали `let _` намекнув компилятору о том, что вам это значение не интересно.
Я Rust не знаю. Это просто первое что пришло в голову на тему «как и тут проигнорировать checked exceptions, не пробрасывая и без варнингов». Если что, напомню, что выше товарищ утверждал, что в Rust в отличие от java, проигнорировать не выйдет ну вообще никак.

Если внезапно Rust станет популярным, то вот подобное извращение будет наименьшим из того, что народ на нем будет выделывать :-)

Проблема с checked exceptions ровно в том, что народ начал массово их игнорировать именно вот таким образом, потому, что сигнатуру менять по всей иерархии на каждый чих — не удобно, конвертировать тоже лениво. Ну, максимум что какую-то малополезную обработку вставляли. А, ну и еще там были проблемы с тем, что несмотря на checked exceptions всё равно были рантайм-паники, которые могут летать как хотят и которые вроде как не поймать. Поэтому доверия к нижележащему коду, даже если ты обвязал все обработками ошибок, всё равно нет.

Насколько я понимаю, в Rust примерно те же грабли.
Много в расте точно так же игнорируется. Просто компилятор более назойлевый чем у джава + checkstyle (который кстати будет ругаться на съеденое исключение).

У меня еще пока не было желания игнорировать Result в расте не было и все не такой как checked exception.

> Если внезапно Rust станет популярным, то вот подобное извращение будет наименьшим из того, что народ на нем будет выделывать :-)

Не думаю.

> Проблема с checked exceptions ровно в том, что народ начал массово их игнорировать именно вот таким образом, потому, что сигнатуру менять по всей иерархии на каждый чих — не удобно, конвертировать тоже лениво

А в Rust сигнатура такой какой была и остается.
https://github.com/mitsuhiko/redis-rs/blob/master/src/types.rs#L321 Вот типичный паттерн для ошибок в расте. Один единый тип ошибок на всю бибилиотеку. Тип ошибки — тип-суммы с имплементацией типажа Error или «толстая» структура которая хранит этот самый enum. Для всего удобства я себе crate сделал который избавляет от boilerplate кода.
Сложно назвать то что вы написали «игнорированием ошибки». Я вам подскажу:

file.write_all(b"This is a list of my best friends.").is_ok();

И никаких предупреждений.

Откуда вы вообще взяли все это массовое недовольство checked исключениеми? Такое ощущение, что больше всего недовольства они вызывают именно у вас. Если кто-то взялся писать на Java, а потом начал ныть, что ему «исключения мешают», так может ну его нахрен эту Java, и дело тут не в ней, а в том, что кто-то плохо подбирает инструменты для работы?
Я думаю речь о том, что checked exceptions плохо подходят для модели «фигак и в продакшн», поскольку подобный код обычно пишется наспех, переделывается много и необдуманно, а в итоге вылезают все прелести связанного полиморфизма и наследования вида: «мы объявили сигнатуру как IOException а там, оказывается, еще надо ловить исключения из совсем другой иерархии…».
Я кажется понял, в чем заключается наше с вами непонимание. Смотрите, в пишете:
Проблема с checked exceptions ровно в том, что народ начал массово их игнорировать именно вот таким образом, потому, что сигнатуру менять по всей иерархии на каждый чих — не удобно, конвертировать тоже лениво.
Вот только в Rust этой проблемы нет. Дело в том, что тип Result, возвращаемый из функции предполагает только один тип возвращаемой ошибки.

Вот реальный пример описания типа ошибки из моего проекта:

pub enum PackageError {
    ReadSettings(SettingsError),
    ParseTheme(ParseThemeError),
    ParseSyntax(ParseSyntaxError),
    Io(IoError)
}

Как видите, тут довольно много всего странного может произойти. Более того, SettingsError, ParseThemeError, ParseSyntaxError – это тоже типы-суммы, которые в свою очередь представляют собой по 5-7 разных исключительных ситуаций. Так что суммарно этот тип описывает одновременно порядка 20 различных исключений. Но это все еще один тип.

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

Это означает, что в 99% случаев добавление новых «исключений» не требует вообще никакого изменения в коде, который вызывает мои функции, даже если эти исключения пробрасываются дальше и инкапсулируются в другие, еще более вложенные типы ошибок.
UFO just landed and posted this here
Насчет checked исключений откровенно странно. Получается, что они плохие просто потому, что разработчики не пользуются ними, перехватывая их каждый раз. Я не видел пока ничего подобного в Rust, не считая boilerplate кода, где хорошей практикой считается просто вызывать `unwrap()`.

На счет обратной совместимости. Во-первых, с ошибками из других библиотек принято работать через типажи `Error` и `Display`. Это означает, что в большинстве случаев вам не прийдется распаковывать значение ошибки и искать там конкретные варианты (конкретный тип исключения). Во-вторых, если вам все-таки нужно это сделать, то я предпочел бы, чтобы компилятор выдал сообщение об ошибке, вместо того, чтобы умолчать тот факт, что API библиотеки, которую я использую, изменилось, и теперь мое приложение может падать с ошибкой, о которой я ничего не знаю и нигде не обрабатываю.

Но в некотором смысле вы правы, ошибки – это часть API. Но, опять же, во многих случаях внутренности ошибки делают приватными, так что добавление нового варианта ошибки (тоже самое, что добавление в сигнатуру метода нового типа исключения в Java) вряд ли как-либо повлияет на работу вашего кода.
UFO just landed and posted this here
прощу прощения, не знаком с Rust, но у меня возник вопрос по
Странная фигня №2. В смысле, нет исключений?

Если функция выбрасывает исключение, то мне нужно каждый раз оборочивать вызов в
try!(load_header(file));
?
А что если я хочу использовать цепочку вызовов, и каждая из этих функций выбрасывает различные исключения, например:
getDB()->select()->from()->where()

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

get_db()?.select()?.from()?.where()?


Но вообще я сомневаюсь что операции select, from и where могут выбросить какие-либо исключения, скорее всего исключения возникнут уже на последнем этапе – когда будет выполнен execute.

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

Подробнее об этом можете посмотреть в подразделе книги «Cовмещение собственных типов ошибок».
а если не писать try!, то не скомпилится?
try! меняет тип с Result<A, E> на A. Если результат дальше используется как A, то не скомпилируется.
Если результат не используется (никуда не присваивается), то будет предупреждение при компиляции.
Нужно чем-то распаковать Result<T, E>. Это или сделает try! или вручную. Автоматически Result<T, E> к T естественно не приводится.
У типа Result есть множество удобных методов для построения цепочек вычислений, напрмер .and_then:
getDB().and_then(select).and_then(from).and_then(where).or_else(foo)
Тут проблема в том, что многие люди к этому не привычны. Скажем какой хаскеллист или скалист вполне свободно будет писать и читать такие конвееры, но часто сталкиваюсь с людьми, которые пришли из си++, которым такой подход взрывает мозг.
Не нужно гнаться за привычным — привычки нарабатываются. Особенно полезные. И не нужно путать привычность с простотой. Методы Optional/Result в Rust просто освоить и просто выработать привычку, да и в целом концепция — простая, хоть и по началу непривычная для кого то.

P.S. Optional в Java теперь тоже есть и даже аналог and_then имеет среди методов — flatMap.
Они, наверное, пришли из C++98/03. Им просто всё объяснить так: "каждая функция возвращает ссылку на функтор".
Заметил интересную тенденцию, почти все, кто сравнивает реализацию на каком-либо языке с реализацией на 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 );
}
Такой усложненный пример был приведен совсем не для того, чтобы показать, как на С++ сложно сделать такую простую задачу, а чтобы продемонстировать, сколько непростых вопросов может возникнуть при реализации этой задачи на C++.

Возможно вы не заметили, но ссылка на «хорошее решение» приводится дальше в по тексту самой статье как раз с пояснением, что ничто не мешает на C++ сделать в общем-то тоже самое.
«Хорошее решение» демонстрирует перегрузку функций, а в вашем примере то, что в C++ называется специализацией.
«Хорошее решение» демонстрирует код таким, каким он должен быть, если бы он изначально писался на С++, и писался хорошо.

Ваше решение просто пытается эмулировать принцип работы кода на Rust, в часности работу типажа Display через создание пустой шаблонной структуры. Это сработает только для очень простых примеров, для любой сложной реализации типажей у вас не получится провернуть подобный трюк.
«Хорошее решение» демонстрирует код таким, каким он должен быть, если бы он изначально писался на С++, и писался хорошо.

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

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

Подобные утверждения требуют какого-то подтверждения. Можете привести пример?
Например, в 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.

Сможете выразить подобное через шаблоны/специализацию в С++?
Конечно.
#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"
}
А если ввести вот такой хелпер:
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>>> {};
Ох жесть. :) А можно полностью избавится от тех реализаций, которые возвращают «not implemented»? Чтобы при попытке вызова Foo<B>::f(); возникала ошибка на этапе компиляции? Ведь смысл как раз в том, что компилятор на уровне выведения типов понимает, для каких типов типаж реализован, а для каких нет.

И что будет в таком случае при вызове Foo2, у которого типы-параметры не выполняют условие T: Foo<U>?
Конечно, просто не приводить реализацию. Реализация «не реализовано» приведена только для того, чтобы пример компилировался.
template <class T, class = void>
struct Foo;
В таком случае готов признать, что я недооценивал возможности шаблонов и бог знает чего еще вы там использовали в С++. :) Но раз вы так старались, приведу аналогичный код на 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();
}
Можно сделать и более похоже на 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;


Тогда программа будет выглядеть вот так:
template <class U>
struct Container {
    U item;
};

template <class T>
struct Foo {
    template <class U>
    static void implemented( U&& self )
    {
        impl<Foo, typename std::decay<U>::type>::implemented( std::forward<U>( self ) );
    }
};

template <class U>
struct Bar {
};

template <class T, class U>
struct impl<Foo<T>, Container<U>, where<implements<T, Bar<U>>>> {
    static void implemented( const Container<U>& )
    {
    }
};

struct A {
};
struct B {
};

template <>
struct impl<Bar<int>, A> {
};

template <>
struct impl<Bar<float>, B> {
};


int main()
{
    auto c1 = Container<int>{64};
    auto c2 = Container<float>{64.f};
    Foo<A>::implemented( c1 );
    Foo<B>::implemented( c2 );
    // Foo<B>::implemented( c1 );
    // Foo<A>::implemented( c2 );
}
Нужeн static_assert иначе сообщение об ошибке скорее всего будет нечитаемым
Когда в С++ появятся концепты, можно будет вводить явные ограничения на параметр шаблона на уровне языка. К сожалению, сейчас это скорее приведет к вороху сообщений об ошибках, из которых понять причину будет довольно затруднительно.
Ну, если не знать языка, то непростых вопросов при решении простой задачи в любом случае возникнет масса. Вон, выше я завалил простыми вопросами, на которые ответы не просты. Просто потому, что я не владею в полном объеме языком Rust :-)
Вопрос «как мне одновременно модифицировать один и тот же объект в разных местах программы так, чтобы это было одновременно эфективно и безопасно с точки зрения многопоточности и управления памятью» не простой для любого языка программирования.

Возможно, другие языки не заставят вас думать об этом на ранних этапах. Из-за этого может возникнуть иллюзия простоты. Но вам нужно решить для себя самостоятельно, что для вас важнее: потратить меньше времени и ошибочно полагать что вы решили задачу, или потратить больше времени и действительно решить задачу.
Про «потокобезопасно» в вопросе нигде не было :-) Так что, вероятно, тут придется заплатить цену за то, что не используешь. (имеется ввиду время на изучение не нужного сейчас знания)

Если у меня программа однопоточная by design (а это в ряде задач таки лучше чем многопоточка в том числе и с т.з. производительности, и энергоэффективности, что сейчас стало вновь очень важно), то это будет не иллюзия. А полную безопасность в плане управления памятью Rust все равно не обеспечивает (даже без unsafe блоков).

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

Я не говорю что Rust это что-то плохое или не нужное. Я говорю, что в данной статье наезд на С++ был не в тему. Пример плохой и посыл неправильный. Если я язык не знаю, и не понимаю как тут что работает, то я либо задам простой вопросы и получу непростой ответ, либо просто отстрелю себе ногу при программировании на данном ЯП. Тем или иным способом отстрелю. И на Rust тоже.

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

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

Где вы увидели в этой статье наезды на С++? Статья рассказывает об особенностях Rust, которые отличают его от других языков программирования с точки зрения начинающих учить язык. Просто так получилось, что автор решил отталкиваться именно от С++.

Я посмею утверждать, что постепенное изучение Rust с нуля будет значительно проще и быстрее, чем изучение с нуля C++. Другое дело, что на С++ вы будете очень долго и упорно писать ужасный и забагованный код, будучи уверенным при этом, что все сделали правильно. Rust вам не позволит такого делать. По мере изучения, компилятор будет постоянно вежливо и подробно объяснять вам ваши ошибки и даже предлагать, как можно изменить код таким образом, чтобы он заработал корректно.

Опять же, возвращаемся к моему вопросу: что вам важнее, думать что вы что-то сделали правильно или действительно сделать это правильно? Rust позволяет делать простые вещи просто, а сложные возможными. Только ирония в том, что написание сложного кода как на С++, так и на Rust требует более менее одинаковой ментальной нагрузки, но в С++ вы узнаете об этом сильно позже, набив множество шишек и наступив на кучу грабель, а не сразу на этапе компиляции.

Я не вижу никакой пользы в появлении «независимых» реализаций языка, учитывая демократичность разработки спецификаций и существующего компилятора. Rust уже давно вышел за пределы Mozilla. Если показателем для вас является то, что на нем начнут делать интернет-магазины, то скажу вам сразу – нет, на нем не будут делать интернет-магазины, скорее всего никогда. Для этого есть Python/Ruby/PHP/JS. Но интернет-магазины и на С++ особо не пишут.
Вообще-то Rust гарантирует безопасность управления памятью.
Не гарантирует. Утечки памяти + невозможность поймать момент когда память таки закончилась и как-то отработать эту ситацию несколько противоречат этому заявлению.
Например, вы можете запросто получить трудноуловимые баги за мутабельные ссылки даже в простом однопоточном приложении.
Например? Нет, я понимаю, что мутабельность переменных (любых!) это уже сразу unsafe и крайне малопредсказуемый небезопасный код с т.з. например хаскелиста. Но с этой же точки зрения весь Rust также небезопасен и малопредсказуем.

/* скипнуто много философии и рекламы, дабы не заниматься cat /dev/zero > /dev/null */

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

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

Вы за кого меня принимаете?
Не гарантирует. Утечки памяти + невозможность поймать момент когда память таки закончилась и как-то отработать эту ситацию несколько противоречат этому заявлению.
Нет, потому что утечка памяти не имеет отношения в 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 что-то там можно сделать проще».
На счет перехвата memory overflow – все зависит от конкретной реализации контейнера. Структуры данных в libstd паникуют в случае memory overflow. Это компромис. Никто не мешает создать такой контейнер, который будет выдавать обычную ошибку в случае, если произойдет memory overflow. Для этого есть прямой доступ к функциям аллокатора памяти. И опять же, такие контейнеры могут быть полностью безопасными.

Так же добавлю, что в случае memory overflow и вызова panic, Rust гарантировано правильно завершит поток: закроет все дескрипторы, сокеты и т. д. Это и есть memory safety, а конкретный способ того, как пользователю возвращается memory overflow (и возвращется ли вообще) тут не причем.
Кстати, а действительно, каким образом в Rust идет управление ресурсами, которые не память? Что-то вроде RAII имеется?
Именно RAII и используется, как для памяти, так и для других ресурсов: файлов, сокетов, мьютексов, локов, и так далее.
Т.е. деструкторы таки есть?
А почему бы им не быть? Может, в каком-то не таком смысле, который вы ожидаете, но есть типаж `Drop`, который гарантированно вызывается, если переменная покидает область видимости. И у всех не «plain old data» типов он реализован.
Потому, что это довольно редкое явление среди разнообразных ЯП :-) И очень хорошо, что Rust это исключение из правила.

PS. Да, я уже успел посмотреть на это. Это реализовано ровно так как я ожидал. Ведь в Rust всё (ок, не все, но многое) делается через одно место — через trait'ы. В том числе и closures например.
«Через одно место» – это ваше субъективное восприятие концепции типажей, или оно чем-то обосновано?
А разве через разные места? По моему через одно и то же. Вон, в спеке на язык написано.
UFO just landed and posted this here
Вот только Rust уже успел стать популярнее чем D. Что-то тут напутано. :)
UFO just landed and posted this here
Ну так «сишку» никто в морг везти и не собирается. :)
1. Эквивалентный код на 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. Классная штука, безусловно. Хотя, с реализацией кажется перемудрили. Мне кажется можно было бы сделать проще.
Да, возвращение ошибок как значения требует явного проброса ошибок наверх в случае необходимости (выше я уже писал, что через несколько месяцев это можно будет делать всего одним символом – foo()?). Но такой подход позволяет строить очень выразительные конструкции с помощью комбинаторов вроде and_then, or_else, unwrap_or(default) и многих других. Поэтому там, где С++/Java появляются лесницы из try-catch блоков, в Rust удается получить довольно выразительные конструкции, например:

// попытатся получить значение, а если случилась ошибка, 
// то использовать значение по-молчанию – 0.
let value = foo().unwrap_or(0);

На счет «перемудрили»: когда начинаешь разбираться, то оказывается что не все так просто, и перемудрили вовсе не просто так, а потому что на то есть объективные причины.
Поэтому там, где С++/Java появляются лесницы из try-catch блоков, в Rust удается получить довольно выразительные конструкции

Но что мешает использовать тот же подход в C++/Java?
Первое, что приходит в голову – отсутствие типов-сумм:

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()
Первое, что приходит в голову – отсутствие типов-сумм

К жабе алгебраические типы вполне прикручиваются: github.com/sviperll/adt4j
Впрочем, если хочется без прикручивания, то там давно есть scala.
Алгербраические типы вряд ли могут быть особо юзабельными в языках, в которых нет нормального pattern matching. Вот как раз на Scala я могу себе представить реализацию полностью аналогичного подхода. Думаю, Scala и замыкания должна уметь инлайнить, ведь так? А вот на Java/C++ я пока представить чего-то аналогичного и удобного не могу. Но Scala – далеко не системный язык программирования.
На плюсах замыкания/лямбды также отлично инлайнятся. То есть на выходе ровно тот же код что был бы без них. Я например использовал лямбды на С++ в коде для микроконтроллера у которого 512 байт ОЗУ.

Вменяемого pattern-matching'a и нормальных алгебраических типов (не нормальные алг. типы с ненормальным матчингом сейчас уже можно сделать, это будет юзабельно, не сказать чтобы очень удобно) конечно не хватает.
Думаю, Scala и замыкания должна уметь инлайнить, ведь так? А вот на Java/C++ я пока представить чего-то аналогичного и удобного не могу.

Как раз тут больше надежды на Java и только как следствие улучшения в Scala. Лямбды в Java 8 очень сильно улучшены и JIT дает неплохие результаты. Сама же Scala в этом отношении может только то, что позволяет ей JVM.
На правах фаната 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 в качестве инструмента системного программирования.
А разве jvm не размещает объекты на стеке, тогда когда это возможно? Там же есть эта оптимизация через escape analysis.
Не всегда это возможно. И это оптимизация в рантайме. Это не только требует прогрева, но и не гарантируется.
Если интересно — вот часть работы, которая ведется в scala в том числе и для избавления от оверхедов, которых в Rust нет изначально: видео, слайды. Докладчик — darkdimius.
Спасибо. Кстати, надо посмотреть что в этом плане есть в Kotlin'e, если есть вообще. Ну и как они там с ошибками борются. Насколько я помню, они там пытались по крайней мере null dereference откусить.
В Kotlin null поддерживается компилятором. Сточки зрения компилятора T и T? — разные типы. В рантайме — один.
Все, что приходит из Java, считается nullable, если не помечено NotNull.
Так что оверхеда в этом случае нет.
А вот для исключений, кажется, ничего (тут могу врать).
Более того, ЕМНИП, если T реализует NonZeroable (например, ссылки), то в качестве None будет использоваться «невозможное» нулевое значение, и оверхеда от Option не будет вообще.
Всё, что «отсутствует» в 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 такой же легкостью передавать в комбинаторы замыкания, которые в результате будут заинлайнены и не будут создавать каких-либо накладных расходов

Всё будет заинлейнено.
Проблемы две:
1. Это не стандартный подход. Это увеличивает порог входа для новых разработчиков, уменьшает возможности по заимствованию кода из одного проекта в другой.
2. Понятие элементарно видимо сильно отличаются. «Шаблонная магия» — для многих и многих программистов на C++ это именно магия. Здесь конечно более читаемо, чем ваша эмуляция типажей, но все равно слегка «Ох жесть.»
Итого, на мой взгляд, в личном проекте так делать наверное можно, в очень крупном, где и так полно специфичных вещей — тоже. Но в остальных наверное не стоит.
Нет там никакой магии, простая обертка над boost::variant
Да это понятно, но выглядит довольно монструозно в пересчёте на функциональность. Кстати boost всё же не часть языка, если писать без него вероятно еще более монструозно будет. Попытки восполнить недостающие конструкции таким образом имеют право на жизнь, но очень усложняют читаемость.
Еще вопросы возникнут с раскапыванием первопричины ошибки при опечатке, особенно с непривычки.

Опять же повторюсь, что все это можно конечно эмулировать, но это не будет иметь никаких особых преимуществ без полноценной поддержки pattern matching на уровне языка. Вот его вам врядли удастся нормально эмулировать.
UFO just landed and posted this here
Выглядит жутковато, если честно. :) Я вообще ничего не понял.
Народ уже привык, и парадигмы уже не сменить.
Эти выразительные конструкции легко сделать и в 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.
UFO just landed and posted this here
Да, в плане D тут скорее не парадокс Блаба играет ключевую роль, а парадокс Бабла :-) Чтобы было бабло на раскрутку языка нужно чтобы язык был уже раскручен.
UFO just landed and posted this here
Мозилла вполне себе зарабатывает. Гугл ей платил за то, что гугл был дефолтным поисковиком, яндекс вроде как платит скорее всего (яндекс дефолтен для русской сборки) и так далее. То есть у мозиллы есть схемы монетизации.

Да, многие языки — это языки одной корпорации. Rust, Go, Java, цейлон. И, что характерно, подобные языки все как один не имеют стандарта вменяемого :-) Ибо есть ровно одна реализация (да, я знаю, что у java были сторонние релизации, от той же IBM например), которая является эталонной и которую тащит одна корпорация (в основном).

Ярко отличается от этих языков язык С++. И, например, Ада (но путь её становления совсем другой конечно, не как у С++).
Какую роль стандартизация играет для ЯП?
Принципиальную для промышленного использования. Практически весь софт для авионики написан на Ada, C и C++.
Вы точно не путаете причину и следствие? Давайте сразу отложим в сторону Аду – ее судьба была определена еще до ее создания. Другие языки кроме C/C++/Ada промышленно не используются?
В авионике — нет. Потому, что там нужна надежность. Mission critical solution. Это вам не браузер :-)

Пока у вас нет стандарта на язык, у вас нет четкой формальной неизменной спеки на язык, следовательно вы даже корректность компилятора проверить не можете.
Во-первых, наличие спецификации и стандартизация языка это далеко не одно и то же. Во-вторых, я очень скептически отношусь к надежности, основанной на толстых пачках бумажек. Кто проверяет корректность спецификации? Человек? Что проверяет соответствие компилятора спецификации? Человек? Каким образом это увеличивает надежность?
Это в C и C++-то надёжность?

https://people.mpi-sws.org/~dreyer/papers/rustbelt/paper.pdf
http://plv.mpi-sws.org/rustbelt/


Прошло два года, и Раст стал формально верифицированным языком.


В авионике — нет. Потому, что там нужна надежность. Mission critical solution. Это вам не браузер :-)

С++ надежный? Ахах, ну да.

Лимит сложности понятие относительное. Rust субъективно проще C++, а он является промышленным стандартом во многих областях. Так что подвинуть с такой точки зрения шансы подвинуть C++ в некоторых нишах у него есть.

Сложность в проверке заимствований не на пустом месте возникла, при написании многопоточного кода на C++ практически всё тоже самое надо держать в голове, только вот подсказать об ошибке, как это делает Rust, некому. Так что это по сути навязанный инструмент статического анализа из коробки. А ими пользуются, причём добровольно.
Sign up to leave a comment.

Articles