Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
Странная фигня №1. Полиморфизм в стиле Rust
Но на практика довольно редко приходится использовать типаж-объекты.
struct Tree<T> {
items: Vec<T>
}
// для любого типа T реализующего типаж TreeElement реализовать типаж Widget для Tree<T>
impl<T> Widget for Tree<T> where T: TreeElement {
...
}
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>];
}
}
Странная фигня №2. В смысле, нет исключений?
Похожего у них только то, что в сигнатуре виден тип возращаемой ошибки.
«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»
Кстати, какова эффективность (по сравнению с исключениями на современных архитектурах и компиляторов) у такого подхода? Накладных расходов на каждый успешных вызов функции нет?
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»
catch (Exception e) в Java. Если функция, которую вы вызываете, может завершиться с ошибкой, то её возвращаемое значение будет типа Result<T, Error>, из которого собственно T можно достать только явно, через паттернматчинг (ну или через конструкции, к нему сводящиеся — монадические комбинаторы или макрос try!()). Да, некоторые операции типа записи в поток ввода-вывода могут ничего не возвращать, и в таком случае возможность случайно проигнорировать ошибку возрастает, но компилятор в таком случае выдаст предупреждение.Кроме того, представьте себе, что вам например нужно добавить в самую общую, всеми используемую библиотеку еще один тип ошибок который она должна выбрасывать (возможно ранее какие-то функции вообще не выбрасывали ошибок, а теперь должны). Теперь в Rust и в java вам придется пройтись по ВСЕМУ коду (стандартному, стороннему, своему) и везде изменить сигнатуры функций. Ну и до кучи все перекомпилировать конечно.
Это же Ад ломающий обратную совместимость.
Если вы говорите про подход с обработкой ошибок с помощью 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();
}
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) {}
}
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");
}
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 в java считаются не самой лучшей идеей.Во-первых, вы сами как считаете, в API фукции должны входить возвращаемые ошибки или нет? Во-вторых, не вижу ничего странного в том, что функции с побочными эфектами и без должны иметь разные сигнатуры, ведь они не взаимозаменяемы. И в третьих, если вас действительно тревожит этот вопрос, можно привести все ошибки к Box<Error>.
Не обижайтесь, но мне кажется, что вы сейчас наглядно демонстрируете проявление парадокса, о котором говорится в статье. :) Вам зачем-то нужно обязательно убедится, что в Rust что-то сделано не так. Для этого вы берете опыт, который у вас есть на Java, и проецируете его на Rust, которого вы толком не знаете, и тут же выдаете вердикт: фигня, так работать не будет.
То-то сотни разработчиков Rust, многие из которых MS и PhD по компиляторам такие тупые и не додумались предложить лучшего решения. :)
пробросить ее наверх даже проще
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");
}
Во-первых, вы сами как считаете, в API фукции должны входить возвращаемые ошибки или нет? Во-вторых, не вижу ничего странного в том, что функции с побочными эфектами и без должны иметь разные сигнатуры, ведь они не взаимозаменяемы. И в третьих, если вас действительно тревожит этот вопрос, можно привести все ошибки к Box.
Я сам не знаю как лучше. Долго думал и о вопросе обработки ошибок, смотрел разные ЯП (haskell, c++, go, ada, SPARK) решения не нашел (пока?). Поэтому и интересуюсь и делюсь мнением, потому, что некоторые знания по этому вопросу имеются. А у вас какой background?
Кроме того, если уж в API функции начали вытаскивать такие детали реализации, как ошибки которые могут возникнуть при работе, то можно вытащить и побольше, например информацию о том, как зависит результат функции от входных параметров (и зависит ли) (SPARK), есть ли побочные эффекты у функции (Haskell), и вообще, много чего еще можно придумать (вплоть до того, какую память функция может потреблять, в каких колличествах, сколько времени может исполняться и какая трудоемкость алгоритмов — это всё бывает нужно).
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;
}
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");
}
}
file.write_all(b"This is a list of my best friends.").is_ok();Проблема с checked exceptions ровно в том, что народ начал массово их игнорировать именно вот таким образом, потому, что сигнатуру менять по всей иерархии на каждый чих — не удобно, конвертировать тоже лениво.Вот только в Rust этой проблемы нет. Дело в том, что тип Result, возвращаемый из функции предполагает только один тип возвращаемой ошибки.
pub enum PackageError {
ReadSettings(SettingsError),
ParseTheme(ParseThemeError),
ParseSyntax(ParseSyntaxError),
Io(IoError)
}
Странная фигня №2. В смысле, нет исключений?
try!(load_header(file));?
getDB()->select()->from()->where()
get_db()?.select()?.from()?.where()?
#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 );
}
«Хорошее решение» демонстрирует код таким, каким он должен быть, если бы он изначально писался на С++, и писался хорошо.
Это сработает только для очень простых примеров, для любой сложной реализации типажей у вас не получится провернуть подобный трюк.
impl<T, U> Foo<T> for Container<U> where T: Bar<U> { /* реализация */ }
impl<T> Foo for T where 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>>> {};
Foo<B>::f(); возникала ошибка на этапе компиляции? Ведь смысл как раз в том, что компилятор на уровне выведения типов понимает, для каких типов типаж реализован, а для каких нет.T: Foo<U>?template <class T, class = void>
struct Foo;
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();
}
#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 );
}
А полную безопасность в плане управления памятью Rust все равно не обеспечивает.Вообще-то Rust гарантирует безопасность управления памятью.
Вообще-то Rust гарантирует безопасность управления памятью.Не гарантирует. Утечки памяти + невозможность поймать момент когда память таки закончилась и как-то отработать эту ситацию несколько противоречат этому заявлению.
Например, вы можете запросто получить трудноуловимые баги за мутабельные ссылки даже в простом однопоточном приложении.Например? Нет, я понимаю, что мутабельность переменных (любых!) это уже сразу unsafe и крайне малопредсказуемый небезопасный код с т.з. например хаскелиста. Но с этой же точки зрения весь Rust также небезопасен и малопредсказуем.
Я не вижу никакой пользы в появлении «независимых» реализаций языка, учитывая демократичность разработки спецификаций и существующего компилятора.А очень зря. Независимые реализации приводят например к появлению стандарта на язык. И, как следствие, значительному уточнению спецификации самого языка. А также обеспечивает значительно большую непотопляемость языка.
Если показателем для вас является то, что на нем начнут делать интернет-магазины, то скажу вам сразу – нет, на нем не будут делать интернет-магазины, скорее всего никогда.
Не гарантирует. Утечки памяти + невозможность поймать момент когда память таки закончилась и как-то отработать эту ситацию несколько противоречат этому заявлению.Нет, потому что утечка памяти не имеет отношения в memory safety. Утечку памяти можно создать обычным бесконечным циклом, и компилятор вам тут не помощник. Memory safety – это отсутствие use-after-free, double-free, dangling-pointer, null-pointer access, buffer overflow и т.д. Утечка памяти – это просто утечка памяти, она не разрушает целосность выполнения программы. Хотя Rust позволит вам предотвратить большинство утечек еще до их появления.
Например? Нет, я понимаю, что мутабельность переменных (любых!) это уже сразу unsafe и крайне малопредсказуемый небезопасный код с т.з. например хаскелиста. Но с этой же точки зрения весь Rust также небезопасен и малопредсказуем.Вот эту часть я не понял. Нет ничего плохого в изменемых состояниях, если пользоватся ними контролируемо. Проблема изменямых состояний в том, что изменение чего-то в одном месте может повлечь неконтролируемые и ошибочные изменения состояния в другом месте. В Rust такая ситуация невозможна, поскольку в один момент времени мутабельным состоянием управляет только один объект. Так что фраза «Но с этой же точки зрения весь Rust также небезопасен и малопредсказуем» мне вообще не понятна.
Вы за кого меня принимаете?За собеседника. Я лишь хотел сказать, что Rust никогда не задумывался как «универсальный язык программирования на все случаи жизни». Так что если он не удовлетворяет лично ваши (либо чьи-либо еще) потребности, то, возможно, это просто потому, что он не должен этого делать? К сожалению, многие высказывают недовольство Rust, аргументируя это тем, что «на Java/Python/PHP что-то там можно сделать проще».
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
}foo()?). Но такой подход позволяет строить очень выразительные конструкции с помощью комбинаторов вроде and_then, or_else, unwrap_or(default) и многих других. Поэтому там, где С++/Java появляются лесницы из try-catch блоков, в Rust удается получить довольно выразительные конструкции, например:// попытатся получить значение, а если случилась ошибка,
// то использовать значение по-молчанию – 0.
let value = foo().unwrap_or(0);
Поэтому там, где С++/Java появляются лесницы из try-catch блоков, в Rust удается получить довольно выразительные конструкции
pub enum Result<T, E> {
Ok(T),
Err(E),
}
// попытаться открыть первый файл, и если не получилось, то попытаться открыть второй
let file = File::open("output1.txt").or_else(|| File::open("output2.txt")).unwrap()
Первое, что приходит в голову – отсутствие типов-сумм
Думаю, Scala и замыкания должна уметь инлайнить, ведь так? А вот на Java/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 такой же легкостью передавать в комбинаторы замыкания, которые в результате будут заинлайнены и не будут создавать каких-либо накладных расходов
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) );
}
https://people.mpi-sws.org/~dreyer/papers/rustbelt/paper.pdf
http://plv.mpi-sws.org/rustbelt/
Прошло два года, и Раст стал формально верифицированным языком.
В авионике — нет. Потому, что там нужна надежность. Mission critical solution. Это вам не браузер :-)
С++ надежный? Ахах, ну да.
Rust и парадокс Блаба