Главная » Хабрахабр » 10 неочевидных преимуществ использования Rust

10 неочевидных преимуществ использования Rust

В нем реализовано автоматическое управление памятью без сборщика мусора и прочих накладных расходов времени исполнения. 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]);

Запустить

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

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

extern crate regex; use regex::Regex; let re = Regex::new(r"^\d-\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 с его инструментом сборки и менеджером зависимостей Cargo будет хорошим выбором для ваших новых проектов. Это явно не рекламируется, но многие замечают, что в Rust реализована одна из лучших на сегодняшний день система сборки и управления зависимостями.

Например, существуют расширения, позволяющие 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)); }
}

Запустить

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

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

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

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

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

Html-документация генерируется автоматически по исходному коду с markdown-описаниями в док-комментариях. Стандартная библиотека Rust очень хорошо документирована. Этим гарантируется актуальность примеров: Более того, док-комментарии в коде на 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);

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

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

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

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

Операция let

let p = Point::new();

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

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

Запустить

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

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);
}

Запустить

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

Другой популярный пример использования образца в цикле 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();

Запустить

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

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

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

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

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

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

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), };
}

Запустить

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

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

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

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 на практике. Если вам есть, что о них рассказать — напишите в комментариях. И вы, наконец, получите именно тот набор инструментов, в котором долго нуждались. И может быть его достоинства для вас перевесят все его недостатки, как это произошло в моем случае.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Вышел Rust 2018… но что это такое?

Статья написана Лин Кларк в сотрудничестве с командой разработчиков Rust («мы» в тексте). Можете прочитать также сообщение в официальном блоге Rust. В этом релизе мы сосредоточились на производительности, чтобы разработчики Rust стали работать максимально эффективно. 6 декабря 2018 года вышла ...

50 лет спустя. The Mother of All Demos

«Компьютерная революция еще не случилась.(The computer revolution hasnt happened yet)»— Алан Кей Всем привет. И я стартую проект «Энгельбарт» (чтобы это ни было и что бы это ни значило). Сегодня 50 лет с исторического события, известного как "Мать всех демонстраций" ...