The Ten Unobvious Benefits Of Using Rust

The Ten Unobvious Benefits Of Using Rust Main Logo

The Ten Unobvious Benefits Of Using Rust

The Rust is a young and ambitious system programming language. It implements automatic memory management without garbage collector and other execution time overhead. In addition, in the Rust language, the semantics of the default movement is used, there are unprecedented rules for accessing the data being changed, and the lifetimes of the links are also taken into account. This allows him to guarantee the safety of memory and facilitates multi-threaded programming, due to the lack of data races.

All this is already well known to all who are at least a little watching the development of modern programming technologies. But what if you are not a system programmer, and there is not a lot of multi-threaded code in your projects, but you are still attracted by the performance of Rust. Will you get any additional benefits from its use in applied tasks? Or all that he will give you, in addition, is a bitter struggle with the compiler, which will force you to write a program so that it consistently follows the rules of the language for borrowing and possession?

This article contains a dozen unobvious and not particularly advertised advantages of using Rust, which I hope it’s will help you to decide on the choice of this language for your projects.

1. Universality of language

Despite the fact that Rust is positioned as a language for system programming, it is also suitable for solving high-level applied problems. You do not have to work with raw pointers if this is not necessary for your task. The standard language library has already implemented most of the types and functions that may be needed in application development. You can also easily connect external libraries and use them. The type system and generalized programming in Rust make it possible to use abstractions of a sufficiently high level, although there is no direct support for OOP in the language.

Let’s look at a few simple examples of using Rust.

An example of combining two iterators into one iterator over pairs of elements:



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

Note: the call of the format name! (…) is a call to the function macro. The names of such macros in Rust always end with a! A character so that they can be distinguished from the names of functions and other identifiers. The advantages of using macros will be discussed below.SmartSpate

An example of using the regex external library for working with regular expressions:



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

An example of the implementation of type Add for its own Point structure to overload the addition operator:



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;

An example of using a generic type in a structure:



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

On Rust, you can write effective system utilities, large desktop applications, microservices, web applications (including the client part, since Rust can be compiled into Wasm), mobile applications (although the language ecosystem is still poorly developed in this direction). Such versatility can be an advantage for multi-project teams because it allows the use of the same approaches and the same modules in many different projects. If you are accustomed to the fact that each tool is designed for its narrow scope, then try to look at Rust as a box with tools of the same reliability and convenience. Perhaps this is what you lacked.

2. Convenient build and dependency management tools

This is clearly not advertised, but many people notice that Rust implements one of the best build and dependency management systems available today. If you programmed in C or C ++, and the issue of using the external libraries painlessly was acute for you, using Rust with its build tool and Cargo dependency manager would be a good choice for your new projects.

Besides the fact that Cargo will download dependencies for you and manage their versions, build and run your applications, run tests and generate documentation, in addition, it can be extended with plugins for other useful functions. For example, there are extensions that allow Cargo to identify outdated dependencies of your project, produce a static analysis of the source code, build and redo client parts of web applications, and much more.

The Cargo configuration file uses the friendly and minimalistic markup language tool to describe the project settings. Here is an example of a typical Cargo.toml configuration file:



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

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

[dev-dependencies]
rand = "*"

And below are three typical commands for using Cargo:



$ cargo check
$ cargo test
$ cargo run

They will be used to check the source code for compilation errors, build the project and run tests, build and run the program, respectively.

3. Built-in tests

It is so easy and simple to write unit tests in Rust that you want to do it again and again. Often it will be easier for you to write a unit test than to try to test the functionality in another way. Here is an example of the functions and tests for them:



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

Functions in the test module, labeled with the # [test] attribute, are unit tests. They will run in parallel when you call the cargo test command. The conditional compilation attribute # [cfg (test)], which marks the entire module with tests, will result in the module being compiled only when executing tests, and will not fall into a normal assembly.

It is very convenient to place the tests in the same module as the test functionality, simply by adding the test submodule to it. And if you need integration tests, then simply place your tests in the tests directory in the project root, and use your application in them as an external package. A separate test module and conditional compilation directives, in this case, do not need to be added.

  • Examples of documentation that are executed as tests deserve special attention, but this will be discussed below.

Built-in performance tests (benchmarks) are also available, but they are not yet stabilized, and therefore are only available in nightly compiler builds. In stable Rust for this type of testing will have to use external libraries.

4. Good documentation with current examples

The standard Rust library is very well documented. Html documentation is automatically generated from source code with markdown descriptions in doc comments. Moreover, the docking comments in the Rust code contain code examples that are executed during the test run. This guarantees the relevance of the examples:



/// 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
}

Here is an example of using the as_bytes method of the String type.



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

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

Will be executed as a test during the test run.

In addition, for Rust libraries, it is common practice to create examples of their use in the form of small independent programs located in the examples directory at the root of the project. These examples are also an important part of the documentation and they are also compiled and executed during the test run, but they can be run independently of the tests.

5. Smart type autocast

In the program for Rust, you can not explicitly specify the type of expression if the compiler is able to output it automatically, based on the context of use. And this applies not only to those places where variables are declared. Let’s look at this example:



let <g class="gr_ gr_2035 gr-alert gr_spell gr_inline_cards gr_run_anim ContextualSpelling ins-del multiReplace" id="2035" data-gr-id="2035">mut</g> vec = Vec::new();
let text = "Message";
vec.push(text);

If we arrange the type annotations, this example will look like this:



let <g class="gr_ gr_2063 gr-alert gr_spell gr_inline_cards gr_run_anim ContextualSpelling ins-del multiReplace" id="2063" data-gr-id="2063">mut</g> vec: Vec&lt;&amp;str&gt; = Vec::new();
let text: &amp;str = "Message";
vec.push(text);

That is, we have a vector of string slices and a variable of type string slice. But in this case, it is completely unnecessary to specify the types, since the compiler can output them himself (using the extended version of the Hindley – Milner algorithm). The fact that vec is a vector is already clear from the type of return value from Vec:: new (), but it is not yet clear what the type of its elements will be. The fact that the text type is a string slice is understandable by the fact that it is assigned a literal of this type. Thus, after vec.push (text), the type of vector elements becomes obvious. Note that the type of the vec variable was fully determined by its use in the execution thread, and not during the initialization phase.

  • Such a type auto-derivation system saves code from unnecessary noise and makes it as concise as any code in a dynamically typed programming language. And this while maintaining strict static typing!

Of course, we cannot completely get rid of specifying types in a statically typed language. The program must have points at which the types of objects are guaranteed to be known so that in other places these types can be output. Such points in Rust are declarations of user-defined data types and function signatures, in which the types used cannot be indicated. But you can enter “metavariable types” in them when using generic programming.

6. Pattern matching in variable declaration spaces

Let operation



<span class="hljs-keyword">let</span> p = Point::new();

In fact, it is not limited to declaring new variables. What it actually does is to match the expression to the right of the equal sign with the sample to the left. And new variables can be introduced into the composition of the sample (and only this way). Take a look at the following example, and it will become clearer to you:



<span class="hljs-keyword">let</span> Point { x, y } = Point::new();

This is where the restructuring is performed: such a mapping will introduce variables x and y, which will be initialized with the x and fields of the object of the Point structure, which is returned by calling Point:: new (). In this case, the mapping is correct, since the type of the expression on the right Point corresponds to the type of Point on the left. Similarly, you can take, for example, the first two elements of an array:



<span class="hljs-keyword">let</span> [a, b, _] = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>];

And do a lot more. The most remarkable thing is that mappings of this kind are made in all places where new variable names can be entered in Rust, namely: in the match, let, if let, while letting operators, in the for loop header, in the arguments of functions and closures. Here is an example of the elegant use of pattern matching in a for loop:



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

The enumerate method, called by the iterator, constructs a new iterator that will iterate through not the initial values, but the tuples, the “ordinal index, initial value” pairs. During iterations of the cycle, each of these tuples will be matched with the specified sample (i, ch), as a result of which the variable I will receive the first value from the tuple — the index, and the variable ch — the second, that is, the string character. Later in the body of the loop, we can use these variables.

Another popular example of using a sample in a for:



for _ in 0..5 {
&nbsp;&nbsp;&nbsp;&nbsp; // Body executes 5 times
}

Here we just ignore the iterator value using the _ pattern. Because the iteration number in the body of the loop, we do not use. The same can be done, for example, with a function argument:



fn foo (a: i32, _: bool) {
&nbsp;&nbsp;&nbsp;&nbsp; // The second argument is never used.
}

Or when matching in the match statement:


matching p {
Point {x: 1, ..} => println! ("Point with x == 1 detected"),
Point {y: 2, ..} => println! ("Point with x! = 1 and y == 2 detected"),
_ => (), // Do not do anything in all other cases
}

Pattern matching makes the code very compact and expressive, and in the match operator, it is generally indispensable. The match operator is a full variable analysis operator, so you will not be able to accidentally forget to check one of the possible matches for the analyzed expression.

7. Syntax Extension and User DSL

The syntax of the Rust language is limited, largely due to the complexity of the type system used in the language. For example, in Rust, there are no named arguments of functions and functions with a variable number of arguments. But you can circumvent these and other limitations with macros. There are two types of macros in Rust: declarative and procedural. With declarative macros, you will never have the same problems as with macros in C, because they are hygienic and work not at the level of textual substitution, but at the level of substitution in an abstract syntax tree. Macros allow you to create abstractions at the level of the syntax of the language. For example:



<span class="hljs-built_in">println!</span>(<span class="hljs-string">"Hello, {name}! Do you know about {}?"</span>, <span class="hljs-number">42</span>, name = <span class="hljs-string">"User"</span>);

In addition to the fact that this macro extends the syntactic capabilities of calling the “function” of printing a formatted string, it will also, in its implementation, check whether the input arguments match the specified format string at compile time, not at runtime. Using macros, you can enter a concise syntax for your own project needs, create and use DSL. Here is an example of using JavaScript code inside a Rust program compiled into 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);

Macro js! is defined in the stdweb package and it allows you to embed full JavaScript code into your program (with the exception of strings in single quotes and operators not terminated by a semicolon) and use Rust code objects in it using the @ {expr} syntax.

Macros offer tremendous opportunities for adapting the syntax of Rust programs to the specific tasks of a specific subject area. They will save your time and attention when developing complex applications. Not by increasing the runtime overhead, but by increasing the compile time.

8. Auto-generation of the dependent code

Procedural derive macros in Rust are widely used for automatic implementation of types and other code generation. Here is an example:



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

Since all these types (Copy, Clone, Debug, Default, PartialEq, and Eq) from the standard library are implemented for the field type of the i32 structure, then for the entire structure as a whole, their implementation can be derived automatically. Another example:



extern crate serde_derive;
extern crate serde_json;

use serde_derive :: {Serialize, Deserialize};

# [derive (Serialize, Deserialize)]
struct point {
&nbsp;&nbsp;&nbsp;&nbsp; x: i32,
&nbsp;&nbsp;&nbsp;&nbsp; y: i32,
}

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

// Serialize Point to JSON string.
let serialized = serde_json :: to_string (&amp; point) .unwrap ();

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

// Deserialize JSON strings to Point.
let deserialized: Point = serde_json :: from_str (&amp; serialized) .unwrap ();

Here, using the Serialize and Deserialize derive macros from the serde library for the Point structure, methods for its serialization and deserialization are automatically generated. Then you can transfer an instance of this structure to various serialization functions, for example, converting it to a JSON string.

You can create your own procedural macros that will generate the code you need. Or use the many already created macros by other developers. In addition to saving the programmer from writing the template code, macros have the advantage that you do not need to maintain different parts of the code in a consistent state. For example, if a third field z is added to the Point structure, then to ensure its correct serialization, if you derive, you don’t need to do anything more. If we ourselves implement the necessary types for serialization of Point, then we will have to ensure that this implementation is always consistent with the latest changes in the Point structure.

9. Algebraic data type

The algebraic data type, to put it simply, is a composite data type that is a union of structures. More formally, this is a type-sum of product types. In Rust, this type is defined using the enum keyword:



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

The type of a specific value of a variable of the Message type can be only one of the structure types listed in the Message. This is either a unit-like Quit borderless structure or one of the ChangeColor or Write tuple structures with unnamed fields or the usual Move structure. A traditional enumerated type can be represented as a special case of an algebraic data type:



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

It is possible to figure out which type really took a value in a particular case using pattern matching:



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

...

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

In the form of algebraic data types, Rust implements such important types as Option and Result, which are used to represent the missing value and the correct/erroneous result, respectively. Here is how the Option is defined in the standard library:



pub enum Option&lt;T&gt; {
None,
Some(T),
}

In Rust, there is no null-value, exactly like the annoying errors of unexpected access to it. Instead, where it is really necessary to indicate the possibility of missing a value, Option is used:



fn divide(numerator: f64, denominator: f64) -&gt; Option&lt;f64&gt; {
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}

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

The algebraic data type is quite a powerful and expressive tool that opens the door to Type-Driven Development. A competently written program in this paradigm imposes on the type system most of the checks on the correctness of its work. Therefore, if you lack some Haskell in everyday industrial programming, Rust can be your outlet.

10. Easy refactoring

The developed strict static type system in Rust and an attempt to perform as many checks as possible during compilation leads to the fact that modifying and refactoring the code becomes quite simple and safe. If, after the changes, the program is assembled, it means that only logical errors remain in it that are not related to the functionality that was checked by the compiler. Combined with the ease of adding unit tests to verify the logic, this leads to serious guarantees for the reliability of programs and an increase in programmer’s confidence in the correct operation of their code after making changes.

Perhaps this is all I wanted to talk about in this article. Of course, Rust has many other advantages, as well as a number of drawbacks (some dampness of the language, lack of the usual programming idioms, “non-literary” syntax), which are not mentioned here.