📜 ⬆️ ⬇️

Rust through its founding principles

I have a few thoughts about learning programming languages.

First, we approach this incorrectly. I am sure that you felt the same way. You are trying to learn a new language and do not quite understand how everything is arranged in it. Why in one place one syntax is used, and in another another? All these oddities are annoying, and as a result we return to the usual language.

I believe that our perception of languages ​​plays a cruel joke with us. Remember the last time you discussed a new language. Someone mentioned it, and someone else inquired about its speed, syntax or existing web framework.
')
This is very similar to the discussion of cars. Have you heard of the new Ford Bratwurst? How fast is he? Can I drive it across the lake?

When we talk about languages ​​in a similar way, we mean that they are interchangeable. Like cars. If I know how to drive a Toyota Hamhock, then I can drive a Ford Bratwurst without any problems. The difference is only in speed and dashboard, is not it?

But imagine what a PHP car will look like. Now imagine how much a Lisp car will be. To change from one to another will require much more than to learn which button controls the heating.

The car is easy to change, because they all solve one problem, and we already have an idea of ​​what this solution should be. Programming languages, on the contrary, solve different problems, and each of them expresses its own philosophy regarding the approach to their elimination.

The syntax and speed of a language express its key characteristics. For example, Ruby is known for having the “developer comfort” most of all, and this has influenced all its features. Java attaches great importance to backward compatibility, which is also reflected in the language.

Thus, my next idea is this: it is better to learn a language through its key features. If we understand why certain decisions were made in the language, it will be easier to understand exactly how it works.


Let's look at the key values ​​of Rust:



Let's postpone parallelism for a while and focus on the other two goals of Rust: speed and security of working with memory.

It should be clarified: in Rust, "memory security" means that it will not allow a segmentation fault (segmentation fault), which you know firsthand if you have worked with C or C ++. If you (like me) avoided these languages, then it may be unusual. Imagine the following situations:



In Ruby, you can get an exception, but in languages ​​like C, something worse will happen. Your program may end abnormally. Or maybe it will execute some arbitrary code, and your small C program will lead to a huge vulnerability. Oops.

By "memory security" in Rust is meant that such a problem does not arise.
Translator's note: Rust resolves memory leaks in safe code and cannot guarantee their absence in general. Since it is impossible to guarantee the absence of circular references for Rc / Arc (an index with a reference count), the forget function is not unsafe. The logic is clear, although I don’t like it very much - I would prefer this function to be unsafe to emphasize that it should be handled with care.

Ruby also protects you from segmentation errors, but uses a garbage collector for this. It's great, but it has a negative effect on performance.

But after all, Rust attaches great importance to speed! Following this goal, Rust abandons the garbage collector. Memory management is a programmer's task. Wait, what about all these horrific bugs that I mentioned ?! Since Rust appreciates speed, he makes me manage my memory. But if the second key value of this language is the safety of working with memory, then why does it force me to work with it manually ?!

There is a clear contradiction between these two goals. With this in mind, let's try to figure out Rust!


fn main() { let x = 1; println!("{}", x); } 

[Run]

This is one of the easiest programs to write on Rust.

In an effort to ensure the safety of working with memory, as an option, you can prevent data changes.

Thus, in Rust, everything defaults to immutable.


 fn main() { let x = 1; x = x + 1; // error: re-assignment of immutable variable `x` [E0384] println!("{}", x); } 

[Run]

Of course, the creators of Rust want people to use their language. So we can declare variables to be mutable, if it’s really necessary.


 fn main() { let mut x = 1; x = x + 1; println!("{}", x); // 2 } 

[Run]

Using the keyword mut you can explicitly indicate that the value is subject to change. Implicitness is the fourth of the main features of Rust. It's funny, but I did not see that it was generally explicitly mentioned among the goals of the language, although if there is a choice between explicit and implicit, Rust usually chooses the first.

Whether here only will create problems an opportunity to change the data? One of the goals of the language is the safety of working with memory. Data variability and memory security seem to be mutually exclusive.


 fn main() { let mut x = 1; //    ,      println!("{}", x); //        } 

[Run]

Staying true to its core values, Rust introduces a new idea - ownership. In Rust, each value must have one owner. And the part of the memory belonging to the owner is released when it goes out of scope.

Let's see how it works:


 fn main() { let original_owner = String::from("Hello"); let new_owner = original_owner; println!("{}", original_owner); // error: use of moved value: `original_owner` [E0382] } 

[Run]

The verbose syntax String::from creates a string that we really own. Then we transfer ownership, and at that moment the original_owner owns ... nothing. Our line can have only one owner.

Through the system of ownership Rust ensures the safety of working with memory. If data can have only one owner, then the possibility to change them by different control flows is simultaneously excluded. For the same reasons, it is impossible to access data that has been destroyed.

Earlier, I said that values ​​are destroyed when they go out of scope. So far, we have dealt with only one scope, our main function. Most programs have more than one scope. In Rust, scope is limited to curly braces.


 fn main() { let first_scope = String::from("Hello"); { let second_scope = String::from("Goodbye"); } println!("{}", second_scope); // error: unresolved name `second_scope` [E0425] } 

[Run]

When the internal scope ends, the second_scope destroyed and we can no longer refer to this variable.

This is another key component of the security of working with memory. If we are unable to access variables that are out of scope, then we are sure that no one can use the deleted data. The compiler simply won't allow it.

Now we understand a little more about how Rust works:



Let's try to do something useful on Rust. At least as useful as it fits into this article. Suppose we want to write a function that will determine whether two strings are equal.

To begin with about functions. We declare them in the same way as our main function:


 fn same_length() { } fn main() { same_length(); } 

[Run]

Our same_length function should take two parameters: the source string and the string for comparison.


 fn same_length(s1, s2) { // error: expected one of `:` or `@`, found `,` } fn main() { let source = String::from("Hello"); let other = String::from("Hi"); println!("{}", same_length(source, other)); } 

[Run]

Rust has a special love for being explicit, so we cannot declare a function without specifying which data will be passed to it. Rust uses strong static typing in function signatures. In this way, the compiler can make sure that we use our functions correctly, preventing errors. Explicit typing also makes it easy to see what the function takes. Our function accepts only strings, which we indicate:


 fn same_length(s1: String, s2: String) { } fn main() { let source = String::from("Hello"); let other = String::from("Hi"); println!("{}", same_length(source, other)); // error: the trait `core::fmt::Display` is not implemented for the type `()` [E0277] } 

[Run]

Most compiler messages are useful, although this may not be so. It tells us that our function returns an empty value () that cannot be displayed. Thus, our function should return something. A boolean value seems appropriate. For now let's just return false .


 fn same_length(s1: String, s2: String) { // error: mismatched types: expected `()`, found `bool` false } fn main() { let source = String::from("Hello"); let other = String::from("Hi"); println!("{}", same_length(source, other)); } 

[Run]

And again about the clearness. Functions must declare not only what they accept, but also the type of the return value. We return the bool :


 #[allow(unused_variables)] fn same_length(s1: String, s2: String) -> bool { false } fn main() { let source = String::from("Hello"); let other = String::from("Hi"); println!("{}", same_length(source, other)); // false } 

[Run]

Cool. This is compiled. Let's try to implement a comparison. Strings have a len function that returns their length:


 fn same_length(s1: String, s2: String) -> bool { s1.len() == s2.len() } fn main() { let source = String::from("Hello"); let other = String::from("Hi"); println!("{}", same_length(source, other)); // false } 

[Run]

Great. Now we make two comparisons!


 fn same_length(s1: String, s2: String) -> bool { s1.len() == s2.len() } fn main() { let source = String::from("Hello"); let other = String::from("Hi"); let other2 = String::from("Hola!"); println!("{}", same_length(source, other)); println!("{}", same_length(source, other2)); // error: use of moved value: `source` [E0382] } 

[Run]

Remember the rules? There can be only one owner, and the values ​​are destroyed after the closing brace. When we call the same_length , we transfer to it ownership of our strings, and upon completion of this function, they are deleted. Comments will make things easier for you.


 fn same_length(s1: String, s2: String) -> bool { s1.len() == s2.len() } fn main() { let source = String::from("Hello"); // source  "Hello" let other = String::from("Hi"); // other  "Hi" let other2 = String::from("Hola!"); // other2  "Hola! println!("{}", same_length(source, other)); //   `same_length`  source  other, //         println!("{}", same_length(source, other2)); // error: use of moved value: `source` [E0382] // source     } 

[Run]

This seems like a serious limitation. Well, that Rust values ​​the safety of working with memory so highly, but is it worth it?

Rust ignores our discontent and adheres to its values, introducing the concept of borrowing. The value can have only one owner, but any number of borrowers. Borrowing in Rust is denoted by the symbol & .


 #[allow(unused_variables)] fn main() { let original_owner = String::from("Hello"); let new_borrower = &original_owner; println!("{}", original_owner); } 

[Run]

Previously, we tried to implement this through the transfer of ownership and failed, but with a loan, everything worked out. Let's try to use the same approach in our function:


 fn same_length(s1: &String, s2: &String) -> bool { s1.len() == s2.len() } fn main() { let source = String::from("Hello"); // source  "Hello" let other = String::from("Hi"); // other  "Hi" let other2 = String::from("Hola!"); // other2  "Hola!" println!("{}", same_length(&source, &other)); // false //    source  other  same_length,       println!("{}", same_length(&source, &other2)); // true //    source ! } 

[Run]

We obviously lent our data to a function, which clearly says that it only lends them, and does not take possession. When the same_length completed, the borrowing is also completed, but the data is not destroyed.

Wait, doesn’t this violate the safety of the memory we talked about so much? Will this not lead to a catastrophe?


 fn main() { let mut x = String::from("Hi!"); let y = &x; y.truncate(0); //  ! truncate   ! } 

[Run]

Hmm ... no. The following rules follow from the memory security in Rust:



Run the above code and see the result.


 <anon>:4:3: 4:4 error: cannot borrow immutable borrowed content `*y` as mutable 

Thanks to these two rules, the safety of working with memory is observed, and this happens without sacrificing speed. All these rules are checked at compile time, without affecting the speed of execution.

This is a very superficial introduction to the concept of borrowing and memory. Rust offers many interesting tools, but they are not always easy to describe. Once you understand the key features of the language, comes an understanding of why it works that way.

If my theories mentioned at the beginning of the article are correct, this introduction to lending did not work so hard. I hope this is clearer than if I said: "Rust allows you to lend data, just do not forget to use & , and everything will be fine." I hope.

It is difficult to learn a new language: stupid syntax, a strange way of dealing with string delimiters, and so on. However, it is powerful. The study of the philosophy of language broadens the mind, but it works only if we consider languages ​​not as an interchangeable jumble of syntax, but as an expression of their principles. Learning a language through its key features not only facilitates the process, but also allows you to feel the language.

Source: https://habr.com/ru/post/280882/


All Articles