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



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


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


1. Универсальность языка


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


Давайте рассмотрим несколько простых примеров использования Rust.


Пример совмещения двух итераторов в один итератор по парам элементов:


let zipper: Vec<_> = (1..).zip("foo".chars()).collect();

assert_eq!((1, 'f'), zipper[0]);
assert_eq!((2, 'o'), zipper[1]);
assert_eq!((3, 'o'), zipper[2]);

Запустить


Примечание: вызов формата name!(...) — это вызов функционального макроса. Имена таких макросов в Rust всегда заканчиваются символом !, чтобы их можно было отличить от имен функций и прочих идентификаторов. О преимуществах использования макросов еще будет сказано ниже.

Пример использования внешней библиотеки regex для работы с регулярными выражениями:


extern crate regex;

use regex::Regex;

let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
assert!(re.is_match("2018-12-06"));

Запустить


Пример реализации типажа Add для собственной структуры Point, чтобы перегрузить оператор сложения:


use std::ops::Add;

struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point { x: self.x + other.x, y: self.y + other.y }
    }
}

let p1 = Point { x: 1, y: 0 };
let p2 = Point { x: 2, y: 3 };

let p3 = p1 + p2;

Запустить


Пример использования обобщенного типа в структуре:


struct Point<T> {
    x: T,
    y: T,
}

let int_origin = Point { x: 0, y: 0 };
let float_origin = Point { x: 0.0, y: 0.0 };

Запустить


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


2. Удобные инструменты сборки и управления зависимостями


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


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


Конфигурационный файл Cargo использует для описания настроек проекта дружелюбный и минималистичный язык разметки toml. Вот пример типичного файла конфигурации Cargo.toml:


[package]
name = "some_app"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]

[dependencies]
regex = "1.0"
chrono = "0.4"

[dev-dependencies]
rand = "*"

А ниже приведены три типичные команды использования Cargo:


$ cargo check
$ cargo test
$ cargo run

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


3. Встроенные тесты


Модульные тесты в Rust писать настолько легко и просто, что хочется это делать снова и снова. :) Зачастую вам будет проще написать модульный тест, чем пытаться протестировать функциональность другим способом. Вот пример функций и тестов к ним:


pub fn is_false(a: bool) -> bool {
    !a
}

pub fn add_two(a: i32) -> i32 {
    a + 2
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn is_false_works() {
        assert!(is_false(false));
        assert!(!is_false(true));
    }

    #[test]
    fn add_two_works() {
        assert_eq!(1, add_two(-1));
        assert_eq!(2, add_two(0));
        assert_eq!(4, add_two(2));
    }
}

Запустить


Функции в модуле test, помеченные атрибутом #[test], являются модульными тестами. Они будут выполняться параллельно при вызове команды cargo test. Атрибут условной компиляции #[cfg(test)], которым помечен весь модуль с тестами, приведет к тому, что модуль будет компилироваться только при выполнении тестов, а в обычную сборку не попадет.


Очень удобно располагать тесты в том же модуле, что и тестируемый функционал, просто добавив в него подмодуль test. А если вам нужны интеграционные тесты, то просто разместите ваши тесты в директории tests в корне проекта, и используйте в них ваше приложение как внешний пакет. Отдельный модуль test и директивы условной компиляции в этом случае добавлять не нужно.


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


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


4. Хорошая документация с актуальными примерами


Стандартная библиотека Rust очень хорошо документирована. Html-документация генерируется автоматически по исходному коду с markdown-описаниями в док-комментариях. Более того, док-комментарии в коде на Rust содержат примеры кода, которые исполняются во время запуска тестов. Этим гарантируется актуальность примеров:


/// Returns a byte slice of this `String`'s contents.
///
/// The inverse of this method is [`from_utf8`].
///
/// [`from_utf8`]: #method.from_utf8
///
/// # Examples
///
/// Basic usage:
///
/// ```
/// let s = String::from("hello");
///
/// assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes());
/// ```
#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn as_bytes(&self) -> &[u8] {
    &self.vec
}

Документация


Здесь пример использования метода as_bytes у типа String


let s = String::from("hello");

assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes());

будет выполнен как тест во время запуска тестов.


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


5. Умное автовыведение типов


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


let mut vec = Vec::new();
let text = "Message";
vec.push(text);

Запустить


Если мы расставим аннотации типов, то данный пример будет выглядеть так:


let mut vec: Vec<&str> = Vec::new();
let text: &str = "Message";
vec.push(text);

То есть мы имеем вектор строковых срезов и переменную типа строковый срез. Но в данном случае указывать типы совершенно излишне, так как компилятор их может вывести сам (пользуясь расширенной версией алгоритма Хиндли — Милнера). То, что vec — это вектор, уже понятно по типу возвращаемого значения из Vec::new(), но пока не понятно, какой будет тип его элементов. То, что тип text — это строковый срез, понятно по тому, что ему присваивается литерал именно такого типа. Таким образом, после vec.push(text) становится очевидным и тип элементов вектора. Обратите внимание, что полностью тип переменной vec был определен ее использованием в потоке выполнения, а не на этапе инициализации.


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


Конечно, мы не можем польностью избавиться от указания типов в статически типизированном языке. В программе должны быть точки, в которых типы объектов гарантированно известны, чтобы в других местах можно было эти типы выводить. Такими точками в Rust являются объявления пользовательских типов данных и сигнатуры функций, в которых нельзя не указывать используемые типы. Но в них можно вводить "метапеременные типов", при использовании обобщенного программирования.


6. Сопоставление с образцом в местах объявления переменных


Операция let


let p = Point::new();

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


let Point { x, y } = Point::new();

Запустить


Здесь произведена деструктуризация: такое сопоставление введет переменные x и y, которые будут проинициализированы значением полей x и y объекта структуры Point, который возвращается вызовом Point::new(). При этом сопоставление корректное, так как типу выражения справа Point соответствует образец типа Point слева. Похожим образом можно взять, например, два первых элемента массива:


let [a, b, _] = [1, 2, 3];

И сделать много чего еще. Самое замечательное, что подобного рода сопоставления производятся во всех местах, где могут вводиться новые имена переменных в Rust, а именно: в операторах match, let, if let, while let, в заголовке цикла for, в аргументах функций и замыканий. Вот пример элегантного использования сопоставления с образцом в цикле for:


for (i, ch) in "foo".chars().enumerate() {
    println!("Index: {}, char: {}", i, ch);
}

Запустить


Метод enumerate, вызванный у итератора, сконструирует новый итератор, который будет перебирать не исходные значения, а кортежи, пары "порядковый индекс, исходное значение". Каждый из этих кортежей при итерациях цикла будет сопоставляться с указанным образцом (i, ch), в результате чего переменная i получит первое значение из кортежа — индекс, а переменная ch — второе, то есть символ строки. Далее в теле цикла мы можем использовать эти переменные.


Другой популярный пример использования образца в цикле for:


for _ in 0..5 {
    // Тело выполняется 5 раз
}

Здесь мы просто игнорируем значение итератора, используя образец _. Потому что номер итерации в теле цикла мы никак не используем. То же самое можно сделать, например, с аргументом функции:


fn foo(a: i32, _: bool) {
    // Второй аргумент никогда не используется
}

Или при сопоставлении в операторе match:


match p {
    Point { x: 1, .. } => println!("Point with x == 1 detected"),
    Point { y: 2, .. } => println!("Point with x != 1 and y == 2 detected"),
    _ => (), // Ничего не делаем во всех остальных случаях
}

Запустить


Сопоставление с образцом делает код весьма компактным и выразительным, а в операторе match оно вообще незаменимо. Оператор match — это оператор полного вариативного анализа, поэтому случайно забыть в нем проверить какое-то из возможных совпадений для анализируемого выражения у вас не получится.


7. Расширение синтаксиса и пользовательские DSL


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


println!("Hello, {name}! Do you know about {}?", 42, name = "User");

Помимо того, что данный макрос расширияет синтаксические возможности вызова "функции" печати форматированной строки, он еще будет в своей реализации проверять соответствие входных аргументов указанной строке формата во время компиляции, а не во время выполнения. С помощью макросов вы можете вводить лаконичный синтаксис под ваши собственные проектные нужды, создавать и использовать DSL. Вот пример использования кода на JavaScript внутри Rust-программы, компилирующейся в Wasm:


let name = "Bob";
let result = js! {
    var msg = "Hello from JS, " + @{name} + "!";
    console.log(msg);
    alert(msg);
    return 2 + 2;
};
println!("2 + 2 = {:?}", result);

Макрос js! определен в пакете stdweb и он позволяет встраивать полноценный JavaScript-код в вашу программу (за исключением строк в одинарных кавычках и операторов, не завершенных точкой с запятой) и использовать в нем объекты из Rust-кода с помощью синтаксиса @{expr}.


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


8. Автогенерация зависимого кода


Процедурные derive-макросы в Rust широко используются для автоматической реализации типажей и прочей кодогенерации. Вот пример:


#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
struct Point {
    x: i32,
    y: i32,
}

Так как все эти типажи (Copy, Clone, Debug, Default, PartialEq и Eq) из стандартной библиотеки реализованы для типа полей структуры i32, то и для всей структуры в целом их реализация может быть выведена автоматически. Другой пример:


extern crate serde_derive;
extern crate serde_json;

use serde_derive::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Point {
    x: i32,
    y: i32,
}

let point = Point { x: 1, y: 2 };

// Сериализация Point в JSON строку.
let serialized = serde_json::to_string(&point).unwrap();

assert_eq!("{\"x\":1,\"y\":2}", serialized);

// Десериализация JSON строки в Point.
let deserialized: Point = serde_json::from_str(&serialized).unwrap();

Запустить


Здесь с помощью derive-макросов Serialize и Deserialize из библиотеки serde для структуры Point автоматически генерируются методы ее сериализации и десериализации. Дальше можно передавать экземпляр этой структуры в различные функции сериализации, например, преобразующие его в JSON строку.


Вы можете создавать собственные процедурные макросы, которые сгенерируют нужный вам код. Либо пользоваться множеством уже созданных макросов другими разработчиками. Помимо избавления программиста от написания шаблонного кода, у макросов есть еще то преимущество, что вам не нужно поддерживать в согласованном состоянии разные участки кода. Скажем, если в структуру Point будет добавлено третье поле z, то для обеспечения ее корректной сериализации в случае использования derive ничего делать больше не нужно. Если же мы будем сами реализовывать необходимые типажи для сериализации Point, то нам придется следить за тем, чтобы эта реализация всегда была согласована с последними изменениями в структуре Point.


9. Алгебраический тип данных


Алгебраический тип данных, говоря упрощенно — это составной тип данных, являющийся объединением структур. Более формально — это тип-сумма из типов-произведений. В Rust такой тип определяется с помощью ключевого слова enum:


enum Message {
    Quit,
    ChangeColor(i32, i32, i32),
    Move { x: i32, y: i32 },
    Write(String),
}

Тип конкретного значения переменной типа Message может быть только одним из перечисленных в Message типов-структур. Это либо unit-подобная структура Quit без полей, либо одна из кортежных структур ChangeColor или Write с безымянными полями, либо обычная структура Move. Традиционный перечислимый тип может быть представлен как частный случай алгебраического типа данных:


enum Color {
    Red,
    Green,
    Blue,
    White,
    Black,
    Unknown,
}

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


let color: Color = get_color();
let text = match color {
    Color::Red => "Red",
    Color::Green => "Green",
    Color::Blue => "Blue",
    _ => "Other color",
};
println!("{}", text);

...

fn process_message(msg: Message) {
    match msg {
        Message::Quit => quit(),
        Message::ChangeColor(r, g, b) => change_color(r, g, b),
        Message::Move { x, y } => move_cursor(x, y),
        Message::Write(s) => println!("{}", s),
    };
}

Запустить


В виде алгебраических типов данных в Rust реализованы такие важные типы, как Option и Result, которые используются для представления отсутствующего значения и корректного/ошибочного результата, соответственно. Вот как определяется Option в стандартной библиотеке:


pub enum Option<T> {
    None,
    Some(T),
}

В Rust отсутствует null-значение, ровно как и досадные ошибки непредвиденного обращения к нему. Вместо этого там, где действительно необходимо указать возможность отсутствия значения, используется Option:


fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

let result = divide(2.0, 3.0);
match result {
    Some(x) => println!("Result: {}", x),
    None => println!("Cannot divide by 0"),
}

Запустить


Алгебраический тип данных — достаточно мощный и выразительный инструмент, который открывает дверь в Type-Driven Development. Грамотно написанная программа в этой парадигме возлагает на систему типов большую часть проверок корректности своей работы. Поэтому если вам нехватает немного Haskell в повседневном промышленном программировании, Rust может стать вашей отдушиной. :)


10. Легкий рефакторинг


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




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

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


  1. manoftheyear
    18.11.2018 18:08
    +2

    Круто! Спасибо за статью. Тоже присматриваюсь к Rust.
    На каких языках писали до того как начали использовать Rust. Сложен ли переход был? Что было сложнее всего или просто не привычнее?


    1. Webdesus
      18.11.2018 19:06
      +2

      Могу поделиться собственным опытом. Делал переход с шарпа на раст. Уже больше полугода пишу в основном только на расте. Первый раз в 16 году попробовал данный язык. Единственная сложность была с лайфтаймами. И судя по общению в чатах у всех новичков проблемы с этим, так как данная особенность существует пока только у раста и не сразу въезжаешь что к чему. Но к счастью явно прописывать время в программе на практике практически не приходится(во всяком случае в прикладных задачах точно можно без этого обходится).

      Так же хочется отметить что на расте архитектура кода получается менее перегруженной, чем в той же java или c#. Но к этому нужно придти, потому что в начале я по привычке все равно городил сложные абстракции кода. Но со временем приходит понимание как нужно писать на расте. В итоге решения на расте как правило получаются более читаемые и более тестированные. Я не говорю что данные вещи возможны только на расте, просто на других языках как правило больше возможностей в плане решения задачи. В итоге прописываешь первое попавшиеся и идешь дальше. Хотя данное решение может быть не самым оптимальным. А в расте приходится в некоторых моментах задумываться.Хочешь ли ты кучу лайфтаймов прописать или можно решить как то задачу более изящным способом. Но как я выше сказал это мой опыт, и может я только писал столько не оптимальный код в других язык))) И по этому возможно для Вас разницы ни какой не будет, после перехода на раст(Ну кроме лайфтаймов)


    1. mr_elzor
      18.11.2018 19:18
      +2

      Я стал использовать Rust больше года назад. До этого писал в основном на функциональных языках. Пока что он прижился у меня, как средство оптимизации Erlang/Elixir приложений и инструмент реализации сложных алгоритмов. Язык собрал в себе множество концепций, и если вы с ними сталкивались в других языках, думаю начать писать не составит особого труда. Многие коллеги жалуются на синтаксис Rust, но у меня такой проблемы не было, впрочем, с Erlang тоже проблем с синтаксисом языка не испытывал.
      Резюмируя, лично меня Rust привлекает целостной экосистемой языка, приемлемым уровнем производительности, удобством интеграции в уже существующие проекты. На нем действительно можно писать как большие системы, так и маленькие низкоуровневые блоки.


    1. freecoder_xx Автор
      18.11.2018 19:36
      +2

      Переходил на Rust с Java, полтора года назад. Понадобилось использовать C++, но после Java управление внешними зависимостями в C++ показалось настоящим кошмаром. А так, в разное время приходилось программировать на C/C++, Python, PHP, JavaScript и Java.


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


      Синтаксис Rust'а поначалу казался вырвиглазным, но, как ни странно, к нему довольно быстро привык и трудности в чтении чужого кода не испытываю.


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


      1. vt4a2h
        18.11.2018 22:01
        +5

        Хотел уточнить, а при переходе на Rust, у вас не возникло проблем с тем, что на нём немного сложно писать программы в объектно-ориентированном стиле? Ну, по крайней мере для меня. Вот что меня немного смутило:
        1) Невозможно наследовать структуры данных. Да, я знаю про агрегацию и композицию, но это не всегда применимо. Возникала ли у вас подобная проблема, и если да, то как вы с ней справлялись?
        2) При наследовании трейтов не сохраняются реализации функций. Т.е. я, скажем наследую В от А, в А есть реализация какой-то функции, но вот в В её уже не будет. Я читал много SO и доков и там есть решения, но это всё же немного не то. Были ли у вас проблемы с этим?
        3) Более-менее сложная иерархия объектов выглядит довольно громоздко. Допустим, какой-нибудь простой оконный интерфейс, где есть отношение parent-child между виджетами, причём, дочерний виджет, допустим может хранить слабую ссылку на родительский, в некоторых случаях. Да, это возможно и реализуемо, но выглядит переусложнённым.

        Я тоже пробовал писать на Rust, но столкнулся с проблемами, которые я описал выше, и для себя решил, что я это язык сугубо для системного программирования, и альтернатива скорее С, чем С++. Хотя вот вижу, что ребята переходят с C# и Java, в общем-то вполне себе ОО-языков для прикладного программирования, и меня начинают мучить сомнения… Мб я сильно где-то не прав?


        1. freecoder_xx Автор
          18.11.2018 23:08
          +3

          1) Где вам нужно наследовать данные — агрегация всегда применима, разве нет? Другое дело, что если таким образом имитировать наследование в стиле ООП, то придется писать много дополнительного кода с пробросом вызова методов. Но так писать не надо ) Я стараюсь просто избегать раздувания структур и создания больших иерархий. Лучше создавать больше маленьких типов, активнее пользоваться типажами и средствами обобщенного программирования. Проблема с этим была только в самом начале, когда я пытался писать на Rust в стиле ООП.


          2) Не уверен, что правильно понял ваш вопрос, но предположу, что после:


          trait Foo {
              fn foo(&self);
          }
          
          trait FooBar: Foo {
              fn foobar(&self);
          }

          Вы ожидаете, что в FooBar появится метод foo. Это не так, потому что требование реализации : Foo относится к тому типу, который будет реализовывать FooBar, а не к самому типажу FooBar. Строго говоря, вы не можете наследовать методы между типажами. Да и зачем это нужно? Необходимости в этом у меня не возникало.


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


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


          1. vt4a2h
            18.11.2018 23:26
            +3

            1) Нет не всегда. Приведу один пример. Допустим я получаю какой-то JSON по сети и мне его надо смапить в структуру данных. Всё прекрасно, я использую serde и радуюсь жизни. Но дальше мне нужно получить другой JSON по сети, который отличается на одно-два поля и точно так же его смапить в структуру, которая просто представляет собой расширение первой. Вот тут агрегация просто не слишком удобна, хотя и возможна, но не так удобная.
            2) Допустим я хочу использовать интерфейс IFoo, и его реализацию по умолчанию IFooBase. Ну и наследоваться от IFooBase, и только там, где мне надо реализовывать другое поведение, но не во всех классах.
            3) Мне да, очень часто.

            С обобщённым программированием я знаком (на плюсах в основном), что такое характеристики типов я понимаю. В плане наследования я с вами не совсем согласен, т.к. считаю, что образование подтипа, путём наследования данных — это вполне себе валидно. Популярные и хорошо написанные C++ библиотеки и использую и код смотрю. Думаю, что надо больше посмотреть для Rust.

            Сдаётся мне, что пока я просто или не до конца понимаю как использовать Rust для прикладного программирования, или просто неправильно его использую.


            1. newpavlov
              18.11.2018 23:44
              +3

              1) Часто в таких случаях хватает одной структуры в которой «расширенные» поля будут «опциональными».
              2) Чем вас не устраивают методы трейтов с реализацией «по умолчанию»? Не нужно никакого FooBase, вы просто пишете:

              trait Foo {
                  // обязательный к реализации метод
                  fn foo1(&self);
                  // метод с дефолтной реализацией на основе метода foo1
                  fn foo2(&self) { .. }
              }
              
              // типаж-расширение (extension trait) для которого все методы реализованы
              // с использованием методов типажа Foo
              trait Bar: Foo {
                  fn bar1(&self) { .. }
                  fn bar2(&self) { .. }
              }
              
              struct A;
              
              // поведение foo2 можно изменить
              impl Foo for A {
                  fn foo1(&self) { .. }
              }
              
              // здесь можно переписать поведение методов типажа Bar при необходимости
              // стоит отметить что без этой строчки вы не сможете использовать методы
              // типажа Bar
              impl Bar for A {}
              


              3) Это означает что ООП подход сильно повлиял на ваш стиль мышления (импринтинг), и что, как уже написали, вам нужно ломать свои привычки и учиться смотреть на задачи с другой стороны. Дело нелёгкое и для многих весьма неприятное, но в целом полезное.


              1. vt4a2h
                18.11.2018 23:49
                +2

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


                1. PsyHaSTe
                  19.11.2018 00:01
                  +2

                  1. А что, если структуры у вас разойдутся по полям? Почти всегда лучше выделить другую структуру обычным копи-пастом. Потому как сегодня общих полей — 2, а завтра — 3. Если просто так совпали звезды и структуры похожи, то это не повод переиспользовать их структуру, это можно быть дорогой в никуда (примерно, как наследовать Point3D : Point2D { public int Z }). В крайнем случае можно просто написать макрос в стиле "скопируй поля вот той структуры". Мерзковато, но возможно.
                  2. Первый трейт называется Bar, опечатка.
                  3. Ответил ниже.


              1. 0xd34df00d
                21.11.2018 01:54

                К слову об 1. А в расте можно сделать что-то вроде такого?


                {-# LANGUAGE DataKinds, GADTs #-}
                {-# LANGUAGE StandaloneDeriving #-}
                
                data StructType = Reduced | Full
                
                data FullField sty ty where
                  FieldExists :: ty -> FullField 'Full ty
                  FieldEmpty :: FullField 'Reduced ty
                
                deriving instance Eq ty => Eq (FullField sty ty)
                deriving instance Ord ty => Ord (FullField sty ty)
                deriving instance Show ty => Show (FullField sty ty)
                
                data Struct sty = Struct
                  { name :: String
                  , age :: Int
                  , salary :: Int
                  , dept :: FullField sty String
                  , smthElse :: FullField sty Int
                  } deriving (Eq, Ord, Show)
                
                reduced :: Struct 'Reduced
                reduced = Struct "meh" 20 42 FieldEmpty FieldEmpty
                
                full :: Struct 'Full
                full = Struct "meh" 20 42 (FieldExists "IT") (FieldExists 10)

                Ну, чтобы статически знать, есть там дополнительные поля или нет.


            1. PsyHaSTe
              18.11.2018 23:56
              +4

              Как выше сказано, у вас чувствуется очень сильное влияние ООП на стиль мышления.

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

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

              В общем, с точки зрения образования оно того стоит. ООП это не панацея, просто набор практик, хороший, но не единственный. Тот же Expression Problem очень неприятен при расширении иерарахии. В итоге в ООП мире обычно все сводится к заучиванию паттерна визитор и все. А в языках с богатыми типами возможно вообще оптимальное решение задачи с помощью tagless final. А вот в ООП оно реализуется, увы, костыльно, хотя и не нереально. Тут подробнее.


          1. 0xd34df00d
            21.11.2018 01:41

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

            Но вы знаете, что реализующий трейт FooBar тип реализует также Foo. Протайпчекается ли обобщённый код, принимающий FooBar и вызывающий на нём foo?


            1. ZyXI
              21.11.2018 02:32
              +1

              Как ни странно, не только пройдёт проверку, но даже и не потребует явным образом импортировать Foo: следующий код выводит HERE.


              mod foo {
                  pub trait Foo {
                      fn foo(&self);
                  }
              }
              
              mod foobar {
                  use crate::foo::Foo;
                  pub trait FooBar : Foo {
                      fn foobar(&self) {
                          self.foo();
                      }
                  }
              }
              
              mod strct {
                  use crate::foo::Foo;
                  use crate::foobar::FooBar;
              
                  pub struct Struct {}
              
                  impl Foo for Struct {
                      fn foo(&self) {
                          println!("HERE");
                      }
                  }
              
                  impl FooBar for Struct {}
              }
              
              mod runfoo {
                  use crate::foobar::FooBar;
              
                  pub fn run_foo(s : &FooBar) {
                      s.foo();
                  }
              }
              
              fn main() {
                  let s = strct::Struct {};
                  runfoo::run_foo(&s);
              }

              Вот если у вас много foo, то извольте и импортировать, и указать явно, какой вы хотите (playground):


              mod foo {
                  pub trait Foo {
                      fn foo(&self);
                  }
              }
              
              mod foo2 {
                  pub trait Foo {
                      fn foo(&self);
                  }
              }
              
              mod foobar {
                  use crate::foo::Foo;
                  use crate::foo2::Foo as Foo2;
              
                  pub trait FooBar : Foo {
                      fn foobar(&self) {
                          self.foo();
                      }
                  }
              
                  pub trait FooAllBar : Foo + Foo2 {
                      fn foobar(&self) {
                          Foo2::foo(self);
                      }
                  }
              }
              
              mod strct {
                  use crate::foo::Foo;
                  use crate::foo2::Foo as Foo2;
                  use crate::foobar::FooBar;
                  use crate::foobar::FooAllBar;
              
                  pub struct Struct {}
              
                  impl Foo for Struct {
                      fn foo(&self) {
                          println!("HERE");
                      }
                  }
              
                  impl Foo2 for Struct {
                      fn foo(&self) {
                          println!("THERE");
                      }
                  }
              
                  impl FooBar for Struct {}
                  impl FooAllBar for Struct {}
              }
              
              mod runfoo {
                  use crate::foobar::FooBar;
              
                  pub fn run_foo(s : &FooBar) {
                      s.foo();
                  }
              }
              
              mod runfooall {
                  use crate::foobar::FooAllBar;
                  use crate::foo2::Foo;
              
                  pub fn run_foo(s : &FooAllBar) {
                      Foo::foo(s);
                  }
              }
              
              fn main() {
                  let s = strct::Struct {};
                  runfoo::run_foo(&s);
                  runfooall::run_foo(&s);
              }


              1. anonymous
                21.11.2018 03:48

                Тут есть небольшая проблема, если я правильно понимаю, &FooBar привести к &Foo не получится.


                Например, если run_foo вдруг захочет вызвать другую функцию, которой нужен &Foo. Или вернуть &Foo в составе какой-то структуры.


                В первом случае ещё можно выкрутиться как-то так (ну или через newtype):


                impl <'a> Foo for &'a FooBar {
                    fn foo(&self) {
                        FooBar::foo(*self)
                    }
                }

                А вот вернуть из функции &Foo, если дан &FooBar уже никак (впрочем, решается тривиально через добавление функции fn as_foo(&self) -> &Foo на FooBar).


        1. BlessMaster
          18.11.2018 23:19
          +6

          После ООП языков с Rust достаточно тяжело. Привычные приёмы не работают, попытки писать в привычном стиле ведут к достаточно кривому и некрасивому коду. С Rust нужно переучиваться. Но, когда понимаешь, как здесь правильно писать, всё становится на порядок проще, код становится лаконичен, прекращается борьба с компилятором и вместо соперника он становится другом и на самом деле очень мощным инструментом, берущим на себя массу рутинной работы.


          При наследовании трейтов не сохраняются реализации функций

          На самом деле это не совсем так. Реализации, сделанные в трейтах — вполне наследуются, но это не то наследование к которому Вы могли привыкнуть в других языках.


          https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=f187e1d7dc3c6e9290c63558c25bd7d3


          Запись B: A, выглядит несомненно как наследование, но, несмотря на сходство — это не оно. Это декларация ограничения — типаж B может быть реализован только для тех типов, для которых реализован типаж A. Соответственно типаж B может расчитывать на методы типажа A, но он не находится в привилегированном положении наследника (и таких типажей может быть реализовано несколько для одного типа), он не переопределяет реализации из другого типажа, а при вызове без уточнения, какую реализацию мы желаем вызвать, компилятор закономерно удивится. Привычное интуитивное "наследование" здесь не работает, хотя вначале кажется — вот оно, и пытаешься строить наследование на типажах :-)


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


          1. vt4a2h
            18.11.2018 23:34
            +4

            Спасибо, это хороший пример!

            Будет здорово, если вы напишите статью о переходе с ОО язков на Rust. Думаю, многие скажут вам спасибо, да и карму вам выше 4х поднять получится, а то сейчас хабр не даёт :)


            1. PsyHaSTe
              19.11.2018 00:04
              +2

              Я планировал написать статью еще полгода назад, но все руки не доходят, про то, как выглядит написание телеграм-бота глазами C# разработчика (меня). Только я сейчас смотрю на все эти футуры в 3 экрана, и думаю, что лучше подождать релиза async/await, а то вместо привлечения людей только напугаю всех :)


              1. vt4a2h
                19.11.2018 00:11
                +3

                Тоже было бы интересно посмотреть.
                Однако я в большей степени говорил о статье, построенной по принципу, который использовал Майерс в молодости, когда переучивал С разработчиков на C++: «ты делал так, а вот теперь надо вот так вот». Ну, а глубина пояснения почему надо так может быть в принципе любой.


              1. Space__Elf
                19.11.2018 03:07

                Ну, можно и без асинхрона остальное написать.


                1. PsyHaSTe
                  19.11.2018 11:20

                  У меня четкое неприятие синхронного ожидания асинхронного кода. Тем более, что в расте «забить» и «сделать хоть как-то» зачастую так же сложно или сложнее, чем сделать корректно.


              1. DelphiCowboy
                19.11.2018 09:48
                +2

                Тоже был бы рад статье как с ООП C++/C# перейти на Rust.
                И хотелось бы в этом году, а не через пять лет.


                1. PsyHaSTe
                  19.11.2018 11:21
                  +2

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


    1. vitvakatu
      18.11.2018 19:37
      +2

      Я немного писал на С++ и Java, пробовал много других языков, но в конце концов (это произошло в начале 2017 года) начал писать на расте и с тех пор пишу только на нем. Не могу сказать, что с самим языком возникали какие-то особенные трудности — первая версия официальной книги позволяла начать писать простые программы и читать чужой код буквально через пару недель чтения по вечерам. Сейчас уже есть уже третье, если не ошибаюсь, издание этой книги — не читал новые версии, но беглое просматривание показывает, что все стало только лучше.
      Все новички непременно сталкиваются с лайфтаймами — да, у меня тоже было время, когда ты рандомно расставляешь в коде неведомые закорючки и молишься, чтобы компилятор сжалился. Впрочем, это довольно быстро проходит после некоторой практики. В ежедневной разработке с лайфтаймами почти не сталкиваешься — в большинстве случаев они просто не нужны или IDE расставит их за тебя.


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


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


      P.S. freecoder_xx отличная статья, мне прям понравилось. Долго писал?


      1. freecoder_xx Автор
        18.11.2018 19:44

        80% статьи я написал за один вечер. А оставшиеся 20% растянулись на два дня :)


    1. tongohiti
      18.11.2018 20:21
      +4

      До Rust был обширный опыт c Java, а также Go. Ну и C/C++ тоже, но всё чаще в режиме read-only. Начал писать на Rust совершенно безболезненно, я бы даже сказал — в эйфории, настолько там всё круто и продумано. Эйфория, конечно, прошла, но язык этот всё равно отличный. Когда сравниваешь его с любым другим языком (вообще любым из известных мне) — то на ум почему-то приходит слово «беспомощный» применительно к этому второму языку.

      Главный минус у него ровно один — он сейчас мало где используется в продакшене, и у работодателей не слишком-то популярен (как говорит один популярный сайт, «There are no open rust jobs anywhere in the world. That doesn't sounds right...»), поэтому пока все мои проекты на Rust — личные.


      1. erlyvideo
        21.11.2018 04:34

        вакансий мало, но мы в эрливидео возможно скоро будем ещё набирать на Rust


    1. Crandel
      18.11.2018 21:48
      +3

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


    1. ZyXI
      19.11.2018 02:19
      +13

      Писал на Rust несколько проектов:


      • Самый первый проект на Rust: преобразователь UART в CAN на основе микроконтроллера STM32. До этого такие проекты у нас писались на C, хотя я лично их не писал.


        Из плюсов:


        • Уже были отличные библиотеки (cortex-m*) и несколько структур данных для асинхронного программирования (вида «получили в прерывании байты, записали в очередь, в основном цикле обработали, записали ответ в очередь на отправку, начали отправку, уснули»). Найти готовую реализацию очереди для микроконтроллеров на C можно. Подключить её нормально в качестве зависимости — нет.
        • Написать тесты, запускающиеся на хосте, с Rust легче.
        • Отладка с gdb уже работает.

        Из минусов:


        • Экосистема только развивающаяся. Многое из готового не найти. Значительная часть проектов висели на одном гуру (кстати, часть из них смотрю перехали в аккаунт организации https://github.com/rust-embedded). Там, где я реализовывал собственно UART и CAN было несколько больше unsafe кода, чем нужно, прямые записи в регистры. Пытаться создавать безопасные абстракции, соответствующие духу языка, я не стал — мало времени и, к тому же, я отлично понимаю, что в первом проекте «как надо» не получится ни за что.
        • Непривычные макросы. Я не могу просто взять и «как в C» обратиться к переменной, объявленной за пределами макроса, без передачи её каким?либо явным образом. Здесь, с одной стороны, всегда видно, что код макроса зависит от переменной. С другой стороны, вызовы макросов становятся длиннее.
        • Для того, чтобы установить бит в РСН на Rust нужно написать конструкцию вида apb1.enr().modify(|_, w| w.i2c1().set_bit());. Как видите, здесь пять(!) вызовов функций (.enr(), .modify(), вызов лямбды (внутри .modify()), .i2c1(), .set_bit()). Компилятор это каким?то образом оптимизирует во что?то адекватное, но, во?первых, выглядит для человека, писавшего на C, это дико. Во?вторых, если вы попросите его не оптимизировать, то он не оптимизирует — при отладке такое сильно замедляет программу.

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


        Из плюсов:


        • Rust может компилироваться в DLL, которую можно вызвать из LabVIEW.
        • На Rust легко написать простой tcp сервер.
        • На Rust можно быстрее писать сложные программы, чем на LabVIEW (кто бы сомневался).
        • Опять же, тесты создать проще.
        • Легко обойтись без аллокаций памяти в куче.
        • Rust обрабатывает данные быстрее LabVIEW.

        Из минусов:


        • Rust при использовании через DLL роняет LabVIEW. И вообще, этот интерфейс в LabVIEW медленнее, чем я думал (но это, конечно, не проблема Rust). Падения я сначала записывал на паники вроде тех, что происходят при переполнении в арифметических операциях, но исправление замеченных ошибок и изменение операторов сложения/вычитания/умножения на подходящие по смыслу функции вроде saturating_add/wrapping_add/… ничего не дали. После переписывание DLL в tcp сервер LabVIEW перестал падать, а сервер падать не стал.
        • Паники сложно перехватывать. Насколько я понял, перехват не всегда работает. Мой код перехвата, кажется, так ни разу и не заработал — хотя, возможно, паник просто не было, а падения вызывались чем?то ещё.
        • Немного повоевал с borrow checker когда решил вести в программе отдельный журнал, но при этом получать имя журнала из LabVIEW. А до получения имени — отправлять данные в stderr.

      • Писал программу анализа бинарных логов, созданных LabVIEW. До этого использовалась программа на Python.


        Из плюсов:


        • С поправкой на чтение документации (только третий проект, я ещё не всё запомнил!) пишется примерно так же быстро, как и на Python. В т.ч. можно так же легко создать цепочку вида файл>буфер (правда на Python буфер включён по?умолчанию и должен явно отключаться, а тут он является отдельной сущностью и должен быть прописан явно)>потоковый распаковщик (xz или gz, по расширению) и передать её в качестве объекта std::io::Read в процедуры анализа.
        • При этом работает на два порядка быстрее, при том что ни оптимизацией программы на Python, ни оптимизацией программы на Rust я не занимался.
        • Компилятор вылавливал те ошибки, которые на Python я бы не заметил до запуска анализа журнала с определёнными данными.

        Из минусов:


        • Вот здесь уже пришлось много воевать с компилятором за то, чтобы вернуть trait object (тот самый std::io::Read) из функции и потом передать в другую функцию. Основная проблема: не сразу понял, что нужно делать, когда компилятор мне говорит, что он не знает размер данных и по этому поводу не хочет ничего никуда передавать.
        • Лапша парсера и сериализатора результатов в человекочитаемую форму на Python всё же короче.


    1. anonymous
      19.11.2018 11:16
      +3

      Писал в основном на Java более 10 лет (другие языки использовал наскоками, по мере необходимости).

      В принципе, на Java я ООП использовал весьма ограничено. Предпочитал отделять данные от функций, их обрабатывающих, неизменяемые структуры данный, стиль с легким налетом функционального программирования, минимум наследования. С этой частью проблем особо не было при переходе на Rust.

      С владением в Rust поначалу было непривычно, но процентов 80, наверное, освоил довольно быстро (простые случаи). Более глубокое понимание на уровне интуиции, наверное, выработалось где-то через полгода, особенно после разработки нескольких вариантов API типа reflection в Java (обобщенный обход данных заданных некоторой схемой).

      Из необычного было то, что мы используем Rust совсем не как системный язык программирования, а для самого что ни на есть энтерпрайза, фактически как аналог Java / .NET. Поэтому, сложно сказать, какая часть трудностей из-за языка как такового, а какая — из-за особенностей его применения. Часть сложностей была, возможно, из-за недостатка опыта (у меня) в обоих областях: и в знании Rust, и в знании предметной области.

      Приходилось проявлять, гм, смекалку. Даже, не побоюсь признаться, приходится заимствовать некоторые подходы из Java (что порой вызывает удивление коллег).

      С точки зрения «разгона» команды, старт у нас был довольно трудным, на мой взгляд, — Rust довольно сложный язык, особенно как второй язык после JavaScript. Но постепенно ситуация исправляется, сейчас у нас примерно человек 8 пишущих активно на Rust. Плюс нам повезло нанять пару человек, имеющих активное участи в Rust экосистеме (в том числе и с позиции менторинга), поэтому я настроен оптимистично :)

      У меня нет опыта разработки и поддержки больших систем на динамически типизированных языках (Java отнесём в категорию «условно статически-типизированных»), но пока что наш код на Rust переживает рефакторинги весьма успешно.


  1. NishchebrodKolya
    18.11.2018 19:07

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


    1. freecoder_xx Автор
      18.11.2018 19:12
      +1

      У точки с запятой, однако, есть и свои синтаксические преимущества. Так что в Rust это не просто бесполезный рудимент.


    1. AngReload
      18.11.2018 20:56
      +8

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


  1. Space__Elf
    18.11.2018 19:20

    Что там в Расте с ООП?


    1. ozkriff
      18.11.2018 19:23

      У mkpankov есть об этом вопросе заметка на rustycrate — https://rustycrate.ru/обучение/2017/06/11/oop-in-rust.html — и есть целая глава в растбуке — https://doc.rust-lang.org/book/second-edition/ch17-00-oop.html (перевод).


    1. freecoder_xx Автор
      18.11.2018 20:09
      +2

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


    1. BlessMaster
      18.11.2018 22:39
      +4

      Добавлю к вышесказанному, что с одной стороны, элементы ООП всё же есть — со структурами можно работать как с объектами, типажи можно наследовать и даже есть типаж-объекты, но с другой стороны, отсутствие классов и наследования структур данных — это фича языка, введённая с целью избежать негативных практик ООП, в том числе чрезмерно разрастающихся иерархий классов, которые становится впоследствии дорого поддерживать и развивать. И на примере достаточно крупных проектов можно сказать, что это достаточно продуктивная модель.
      Вообще, Rust — это очередной гибрид функционального и объектного подходов, с существенным перевесом в пользу функционального, но без возведения его в абсолют. Однако история о том, какой будет ООП сторона Rust, ещё не завершена, и соответствующие фичи и RFC потихоньку пилятся и эволюционируют.


      1. rogoz
        19.11.2018 04:10
        +1

        на примере достаточно крупных проектов

        А можно примеры? Прям чтоб чтот крупное. А то с ходу нагуглися только проект Мозиллы Servo, но это и понятно.


        1. PsyHaSTe
          19.11.2018 12:09
          +1

          Parity, actix, chalk — первое, что в голову пришло.


        1. freecoder_xx Автор
          19.11.2018 12:11
          +1

          Примеры можно посмотреть здесь.



        1. Crandel
          19.11.2018 17:52

          alacritty — очень быстрый терминал на расте, для Линукс и МакОс


      1. snuk182
        19.11.2018 15:21

        А есть ли уже сборник шаблонов проектирования или best practices?
        Я периодически мониторю реддит — вопросы о таковых всплывают регулярно, ответы — в стиле ниже, «откройте крупный проект и смотрите».


        1. freecoder_xx Автор
          19.11.2018 15:40

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


          1. snuk182
            19.11.2018 15:45

            Наверное это одна из основных причин, по которым до сих пор нет вменяемой UI библиотеки. Там надо четко просматриваемые шаблоны расширения/переиспользования/переопределения функционала.


        1. Webdesus
          19.11.2018 23:31
          +1

          1. snuk182
            19.11.2018 23:59

            Ну, паттернов там по факту нет вовсе. И это немного не best practices, больше как туториал по языку на уровне чуть выше базового. Эти ссылки знаю. Awesome когда-то давно мониторил, потом забросил, спасибо за напоминание.


  1. rPman
    18.11.2018 19:26

    Вот бы еще сказали, на сколько весь этот синтаксический и языковой сахар влияет на производительность результирующего кода?

    Ведь речь идет о языке системного уровня, я понимаю, на уровне языка уменьшать количество ошибок это уже круто, но производительность было до сих пор чуть ли не главным достоинством c/c++, чье место по уму собирается забирать Rust?


    1. vitvakatu
      18.11.2018 19:40
      +3

      Одна из главных идей раста — так называемые zero-cost abstractions. Это значит, что почти каждая языковая абстракция никак не влияет на производительность. Результаты бенчмарков с другими языками найти достаточно легко — Раст настолько же быстр, насколько и С/C++


      1. newpavlov
        18.11.2018 20:43
        +1

        К сожалению, тут стоит поставить звёздочку, конечно очень часто компилятор достаточно умён и действительно способен выдать на выходе zero-cost abstractions, но на практике бывают случаи (особенно с итераторами) когда код приходится немного «отуплять» дабы компилятор смог провести необходимые оптимизации. Например, тут достаточно плохо себя вёл step_by по ренджам, до того момента как для этого случая добавили специализацию.


        1. BlessMaster
          18.11.2018 23:28
          +1

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


  1. Kyushu
    19.11.2018 11:24

    Конверторов нет на Rust?


    1. vitvakatu
      19.11.2018 11:47
      +1

      Конвенторов из чего во что?


    1. ozkriff
      19.11.2018 12:08
      +1

      Для Си есть, как минимум, два более-менее живых:



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


      Можно тут почитать отчет о недавней попытке использовать для портирования библиотеки: https://wiki.alopex.li/PortingCToRust


  1. Kyushu
    19.11.2018 12:02

    Конверторов программ с какого-либо алгоритмического языка на Rust.


    1. newpavlov
      19.11.2018 12:05
      +1

      Есть экспериментальные конверторы из C в Rust: citrus, corrode, c2rust. Разумеется на выходе у них весь код помечен как unsafe.


  1. vba
    19.11.2018 12:31
    +2

    На мой взгляд, сейчас очень сильна хайп волна от Golang, но на мой взгляд Rust гораздо лаконичней и перспективней. Вопрос, не повлияет ли все таки пресловутая хайп волна на популярность Rust.


    1. freecoder_xx Автор
      19.11.2018 12:44
      +1

      Раньше были опасения, что Rust может сильно отстать от Go и Swift из-за отсутствия маркетинговой раскрутки. Но сейчас таких опасений нет, Rust хорошо развивается как язык, появляется все больше интересных проектов, его сообщество постоянно растет. Лично я считаю, что Rust — сильно недооцененная технология. Это "мина замедленного действия", "подрыв" которой — вопрос времени.


      1. Gorthauer87
        19.11.2018 13:50

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


    1. kuftachev
      20.11.2018 02:42

      При чем тут хайп к Go?


      Его создали одни из крутейших инженеров в Ай-Ти мире, реально как раз хайпа и маркетинга особого и нету, что ещё пока живы куча бессмысленных вещей типа Python в вебе.


      Ну и Go это не системный язык, хоть и компилируется в нативный код. Он не прямой конкурент для Rust. Для Rust, как я понимаю, конкурент C++, так как C он тоже скорее всего не заменит.


  1. Stormwalker
    19.11.2018 12:48

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


    1. freecoder_xx Автор
      19.11.2018 12:55
      +2

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


    1. ozkriff
      19.11.2018 13:11
      +2

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


      Обычно говорят что раст императивный, но с вывертом: чистые функциональные языки стараются не допускать общего изменяемого состояния (shared mutable state), запрещая изменения, а Ржавчина запрещает именно одновременность общего и изменяемого состояния. Т.е. в один момент времени состояние может быть или общим (много & указателей на переменную, например), или изменяемым, но с уникальным доступом (&mut указатель требует эксклюзивности доступа). Т.е. цель сходная с чистой функциональщиной, но путь к ней другой из-за необходимости быть более низкоуровневым.


      P.S. недавняя статья на тему


      1. DelphiCowboy
        19.11.2018 14:00
        -3

        неизменяемость переменных по умолчанию

        ?! * в шоке *
        А как мне значение переменной поменять не одну тысячу раз? O_O
        Выделять под каждый новый экземпляр переменной новую дополнительную память?! * в ауте *
        Скоро можно будет. RFC о юникодных идентификаторах приняли не так давно, срачей вокруг него куча была.

        А IDE позволяет отличить русское «с» от латинской «c»?
        И вроде, ещё японские и китайские иероглифы имеют разные кодировки в Юникоде при одинаковом внешнем виде. Японские иероглифы — это немного модифицированные в XX веке китайские (не считая японского слогового алфавита), так что большая часть иероглифов выглядит одинаково, но часть иероглифов отличается.


        1. ozkriff
          19.11.2018 14:07
          +2

          Спокойно, просто вместо let a = 5; надо написать let mut a = 5; и она станет изменяемой.


          Про идентификаторы — я подозреваю что в серьезных международных проектах просто будет включено предупреждение об использовании не-ascii идентификаторов (#![forbid(non_ascii_idents)]) и все.


          1. DelphiCowboy
            20.11.2018 05:29

            Уф! Напугали!
            Хорошо, можно делать изменяемые переменные.


            1. PsyHaSTe
              20.11.2018 18:51

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

              И это не работает в миллион раз медленнее, чем С, по многим причинам. И да, это зачастую удобнее, чем мутировать переменную.


  1. wikipro
    19.11.2018 13:51

    А в Rust можно использовать кириллические имена переменных и функций ?


    1. ozkriff
      19.11.2018 13:54

      Скоро можно будет. RFC о юникодных идентификаторах приняли не так давно, срачей вокруг него куча была.


    1. KvanTTT
      19.11.2018 14:31
      +4

      А зачем?


      1. ilammy
        19.11.2018 15:56
        +4

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


        1. el777
          19.11.2018 16:35
          +3

          Только работать с такими переменными будет неудобно.
          Далеко не каждый терминал позволяет удобно вводить их с клавы — значит нужно будет каждый раз Copy+Paste делать. Еще хуже ситуация — вы на маке, коллега на винде. У вас чуть разное отображение шрифтов и разный ввод символов. Брррр.
          Не понимаю, зачем из нормального языка делать 1С.


          1. ilammy
            19.11.2018 16:48
            +3

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

            Стандартная библиотека языка остаётся ASCII-only и на английском. Авторы RFC советуют другим библиотекам поступать так же, если у них нет других соображений. Так что я бы не назвал это превращением Rust в 1С.


            1. PsyHaSTe
              20.11.2018 18:54

              Как выше ответили — уже сейчас можно сделать объявление соответствующей фичи и писать: play.rust-lang.org/?version=nightly&mode=debug&edition=2015&gist=b344b0c2bd40673455a7518af1a7a2a8

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


        1. Antervis
          19.11.2018 19:24

          зато читать код, в котором вместо разных переменных одинаковые квадратики — сложнее


      1. wikipro
        20.11.2018 13:02

        Не профессиональным программистам так удобнее. Вообще была попытка русифицировать Lua для торгового терминала OUIK — чтобы можно было писать код и на русском и латиницей https://тхаб.рф/wiki/LuaRu
        Есть ещё очень профессионально сделанный 1Script http://oscript.io — активно развивается, есть сообщество поддержка MS Visual Studio Code


  1. dbelka
    19.11.2018 14:29
    +3

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


  1. nesteg
    19.11.2018 17:26

    Rust годный язык для разных задач в том числе и для Web, но в дальнейшем ему надо будет бороться за место под солнцем c одной стороны с С++(С++11, С++14,C++17,C++20)- это со стороны как системного, c другой с набирающем популярность Go — это со стороны Web. Далее — с GUI для десктоп у него пока никак (QT — живее всех живых), бодаться за Web фронт c JavaScript бесполезно(например — Dart как бы есть и как бы нет его).Далее — embedded очень консервативен — тоже С/C++ во всех проектах. Далее — машинное обучение — Python, BigData — Scala/Java/Python ну и по списку можно продолжать.
    Ну а то что его на StackOverflow любят — ну так любить не значит жениться.
    Короче, тяжело ему будет у работадателей зацепиться.Вакансий реально мало.


    1. snuk182
      19.11.2018 19:22
      +1

      Я не был бы так категоричен. Походив по Rust-собеседованиям и поопрашивав собеседователей, услышал забавное — во-первых, альтернативе Rust просто нет для тех бизнес целей (скорость + надежность) изначально, во вторых, опробовав, люди его уже не сменят ни на что — несмотря на рассказы о смертельной кривой обучения (которые разгоняются в основном неосилившими не желающими осилить борроучекер), язык конвертируется в бизнес-выгоду молниеносно, на уровне Java, и это при том, что для многого нет сторонних библиотек, и надо все отсутствующее дописывать с нуля. Дословно, фраза СЕО «I have no single minute of regret about the decision of taking Rust as a primary language, although it holds our production for more than two years already». Да, серьезному бизнесу вроде финтеха прыгать в Rust пока страшно, но процесс освоения рынков идет. Лично я жду, когда веб-разработчикам наконец-то надоест жевать производные джаваскрипта, отдавая браузеру все больше ресурсов, и начнется великое пришествие WebAssembly.

      А С++ никуда не денется ближайшее время, потому что то, что можно было у него откусить, уже давно откушено Java и C#, а оставшаяся публика слишком консервативна. В нее никто не целится.


  1. kosmonaFFFt
    19.11.2018 20:29
    +1

    А как у него обстоят дела с IDE? Что-то уровня Idea уже есть?


    1. anonymous
      19.11.2018 20:45
      +2

      Почти: intellij-rust.github.io :D

      P.S. Не идеален, но работает сносно (у нас проект на ~100 KLOC строк кода). Некоторые функции работают очень медленно (например, «найди мне все реализации этого типажа»).


    1. vitvakatu
      19.11.2018 20:50
      +2

      Есть плагин для IntelliJ IDEA :D Кстати сказать, официальный, от JetBrains (что намекает на перспективы языка). Конечно, до уровня поддержки Java пока далеко, но в целом — отлично работает, умеет почти все, что требуется в повседневной разработке (кроме дебага — для этого потребуется лицензионный CLion или старый-добрый gdb)


      Также есть плагин для VS Code — он использует Language Server Protocol. Работает вроде неплохо, но пока что нестабильно и для по-настоящему больших проектов, пожалуй, не подходит.


      Разумеется, есть и плагины под другие редакторы — статус поддержки можно посмотреть на https://areweideyet.com


    1. enabokov
      21.11.2018 04:13

      Есть удобный плагин для IDEA.


  1. nesteg
    19.11.2018 21:37
    +1

    Да я как раз хотел черкануть(ну упустил из виду), что WebAassembly — это как раз одна из ниш именно Rust.
    И еще прикол-после 6 летней спячки проснулся создатель node.js, покаялся в ошибках(хотя кто их не делает ) и уже начал пиарить новый проект deno, а в нем он использует Rust (а хотел Go ), ну и до кучи там C и javascript (внезапно!) typesrcipt(клевый язык — чуствуется рука создателя Delphi и C#).Ну на этом все — пост все таки про Rust.


  1. Jenyay
    20.11.2018 09:10

    А как у Rust с научными расчетами? Есть какие-то общепризнанные математические библиотеки для работы с матрицами, математическими функциями вроде функций Бесселя и т.п? И есть ли библиотека для построение 2D- и 3D-графиков?

    Интересно, удастся ли полноценно заменить связку Python + numpy + matplotlib на Rust + что-то.


    1. newpavlov
      20.11.2018 11:51
      +2

      Наиболее развитый проект для матриц это nalgebra, к сожалению, без const generics (которые обещают в начале следующего года в Nightly) данному крейту приходится использовать typenum, что не лучшим образом сказывается на эргономичности. Есть ещё немного заброшенный cgmath, но он сильно проще и создан с расчётом на компьютерную графику, т.е. никаких многомерных матриц.

      Для 2D и 3D графиков хороших нативных решений пока нет (и вероятно ещё достаточно долго не будет, т.к. это весьма нетривиальная задача). Большинство либо использует gnuplot, либо дёргает matplotlib через крейты вроде pyo3.


      1. DelphiCowboy
        20.11.2018 14:28

        А матрицы там считают с параллелизацией?


        1. newpavlov
          20.11.2018 14:44

          Вы про SIMD или про параллелизацию между тредами? Если первое, то пока что приходится полагаться на автовекторизацию, т.к. разработчики решили что явный SIMD сделает код значительно сложнее без достаточного выигрыша (но это может поменяться с добавлением portable SIMD в стандартную библиотеку). Есть ещё биндинги к lapack и GLM. Разумеется есть ещё биндинги Если второе, то вроде бы в определённых случаях можно использовать связку nalgebra+rayon, но подробностей не скажу.


    1. snuk182
      20.11.2018 12:33
      +1

      Я использовал бинды к Gnuplot для отрисовки графиков. Без звезд с неба (как например питоновская Панда), но с задачами справляется.


  1. CheatEx
    20.11.2018 09:58

    «Модульные тесты в Rust писать настолько легко и просто, что хочется это делать снова и снова. :) Зачастую вам будет проще написать модульный тест, чем пытаться протестировать функциональность другим способом.» и тест тавтологии и 2 + 2. Серьёзно? Вам отчаяно не хватало пунктов до круглого числа или Вы реально думаете что такой пример что-то доказывает?


    1. ozkriff
      20.11.2018 10:02

      Пример показывает, что тесты писать просто — нашлепнул на функцию #[test] и готово, остальное встроено в стандартный cargo.


      1. anonymous
        20.11.2018 10:35
        +2

        Это пока чего-то более серьёзного не захочется.

        До JUnit в Java ещё пока не дотягивает, на мой взгляд. Например, не хватает возможности удобно писать data-driven тесты. Так, чтобы 1 файл с данными = 1 тест, например; с возможностью запуска индивидуальных тестов.

        Какие-то движения вокруг custom test frameworks вроде происходят, но, к сожалению, нет времени, чтобы во всём этом разобраться.

        Пока выкручиваюсь с proc macro, который сканирует файлы и создаёт #[test] функцию на каждый файл, выглядит примерно так (второй параметр — регулярное выражение для матча на «основной» файл теста, остальные параметры — шаблоны-подстановки по этому регулярному выражению):

        // запустится с A.input.txt / A.output.txt, B.input.txt / B.output.txt, и.т.д.
        #[datatest("tests/test-cases", r"^(.*)\.input\.txt", r"$1.output.txt")]
        fn sample(input: String, output: String) {
          assert_eq!(format!("Hello, {}!", input), output);
        }
        


        Можно, конечно, сделать так, чтобы все эти тесты в одной #[test] функции выполнялись, но тогда очень неудобно с этими тестами работать. Например, если делается рефакторинг и половина тестов поломана — хочется возможности запустить один тест, чтобы начать с чего-то мелкого. Плюс, хочется видеть все поломки, а не первую. И так далее.

        Может быть, можно как-то через кишки модуля test выкрутиться?..

        P.S. Похоже, можно попробовать через «harness = false» в Cargo.toml. Пора снова переписывать фреймворк :D


    1. freecoder_xx Автор
      20.11.2018 13:44
      +1

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