Everyone is looking for something different in programming languages: the functional side is important to someone, the richness of libraries is important to someone, and the other will immediately pay attention to the length of keywords. In this article I would like to tell what is especially important for me in Rust - its obviousness. Not that it is easy to learn from scratch, but how easy it is to examine the behavior of a code by looking at its fragments. I will list the features of the language, allowing you to accurately determine what a particular function does, taken out of context.
I especially note that I am not trying to fully describe the language, but only one side of it. This is my personal view of the Rust philosophy, which does not necessarily coincide with the official position of the developers! In addition, Rust will not be obvious to a stranger from other languages: the learning curve is quite sharp, and more than once the compiler will force you to mentally say “wtf” on the road to enlightenment.
Dangerous code - unsafe
"Normal" code is considered safe for memory access. This has not yet been proven formally, but this is the idea of ​​the developers. This security does not mean that the code will not fall. It means that there will be no reading of another's memory, like many other variants of indefinite behavior. As far as I know, in normal code, all behavior is defined. If you try to do something illegal that cannot be tracked during assembly, the worst thing that can happen is a controlled drop.
If you do something outside the simple rules of the language, you frame the corresponding cunning code in
unsafe {} . For example, you can find unsafe code in the implementation of synchronization primitives and smart meters (
Arc, Rc, Mutex, RwLock ). Note that this does not make these elements dangerous, because they expose a completely safe (from the point of view of Rust) interface:
')
So, if you came across a function with an unsafe block, you need to carefully look at the contents. If not, be calm, the behavior of the function is strictly defined (no
undefined behavior ). Taking this opportunity ... hello, C ++!
Exceptions that are not
There is Java code: functions that call other functions, etc. And you can trace what causes who and where it returns, build a tree, if you want. But here's a misfortune - every function call can return as usual, and through an exception. Somewhere they are caught and processed, and somewhere they are forwarded to the top. Undoubtedly, the system of exceptions is a powerful and expressive tool that can be used for good. However, it prevents obvious: looking at any piece of code, the programmer must understand and keep track of which functions can cause which exceptions, and what to do with it all.
So the author ZeroMQ decided that this complexity
only hinders , and the developers of Rust agree with him. We have no exceptions, and potential errors are part of the returned (algebraic) types:
fn foo() -> Result<Something, SomeError>; ... match foo() { Ok(t) => (...),
You see how and what the functions return, and you cannot take the result without checking for potential errors (hello, Go!).
Limited type inference
Looking at a function in Python, I do not always understand what it does. There are, of course, good names and names of parameters, but this is not checked and guaranteed. This property allows the language to be compact and focus on actions, not data.
In Rust, there is type inference, but it is purely local. You must specify the return type of the function, all its parameters, and the types of static values. This approach allows you not to rummage in the code of someone else's function in search of the same type, if it is specified there.
Local variables
This property seems so simple and obvious ... until the guys from Oberon appear (hello!). Global variables have positive moments, but they make it difficult to understand code fragments.
Immutable variables
Oh yes, the default variables cannot be changed twice. Want to change after initialization - please indicate the word
mut . At the same time, the compiler guarantees this property:
fn foo(x: &u32) -> u32 { ...
Or with a changeable state:
fn foo(x: &mut u32) { ...
Compare this with
const in C:
unsigned foo(const unsigned *const x) { ...
Pointers you won't see
Rust has pointers, but they are used only in small pieces of dangerous code. The reason for this is that dereferencing of any pointer is an unsafe operation. Custom code is richly built on links. A reference to an object guarantees its existence while it is alive. So there is no null pointer problem in Rust.
Of course, I hear loud cries that null pointers simply carry the meaning of a non-existent object, which we all the same express in Rust one way or another with all the logical errors that follow. Yes, there is
Option <& Something> , however it is not exactly the same. From the point of view of Rust, your code, say, Java, is replete with pointers that can fall down at one point. You may know which of them cannot be null, but keep it in your head. Your colleague cannot read your thoughts, and the compiler cannot protect you from memory failure.
In Rust, the semantics of the missing object are
obvious : it is explicit in the code, and the compiler obliges you (and your colleague) to check the existence of the object upon access. Most of the objects with which we deal are transmitted by simple links, their existence is guaranteed:
fn get_count(input: Option<&str>) -> usize { match input { Some(s) => s.len(), None => 0, } }
Of course, all of you can also fall in the place where you expect something that is not there. But the fall will be deliberate (through calling
unwrap () or
expect () ) and explicit.
Modules
Everything in scope can be found by local ads and the keyword
use . You can extend the scope of visibility directly in blocks of code, which further enhances locality:
fn myswap(x: &mut i8, y: &mut i8) { use std::mem::swap; swap(x, y); }
The problem is essentially only in C and C ++, but there it delivers quite. How to understand what exactly is in scope? You need to check the current file, then all included files, then all of them to include, and so on.
Composition instead of inheritance
Rust has no class inheritance. You can inherit interfaces (traits), but structures still need to explicitly implement all the interfaces that the desired interface inherits. Suppose you see a call to the
object.foo () method. What kind of code will be executed? In languages ​​with inheritance (especially plural), you need to look for this method in a class of type
object , then in its parent classes, and so on - until you find a implementation.
Inheritance is a powerful weapon that allows you to achieve beautiful polymorphism in a huge number of tasks. In Rust,
disputes still
linger over how to get something similar, while maintaining the beauty of the language. However, I am convinced that it makes it difficult to understand the code.
Without inheritance, the situation is a little level. First, we look at the implementation of the structure itself: if the method is there, then the story ends there. Next, we look at which interfaces in scope, which ones have this method, and which ones are implemented for your structure. At the intersections of these subsets there will be one single interface if the compiler does not swear. The code itself will be either in the implementation of this interface by the structure, or in its declaration itself (the default implementation).
Explicit implementation of generalizations
Separately, I would like to note the point that in order to satisfy a certain interface, it must be explicitly indicated:
impl SomeTrait for MyStruct {...}
This can be done where the interface or target structure is declared, but not in an arbitrary place. Hi, Go, where the magic of implicit realizations reigns. No, the concept in Go is very beautiful and original, I do not argue, but I would question the obviousness of what is happening.
Generalized restrictions
Templates in C ++ are, oddly enough, elements of meta-programming. A sort of matured Turing-complete macros. They allow you to save a lot of code and create real miracles (hi, Boost!). However, it is difficult to say what exactly will happen at the moment of substitution of a particular type. What requirements for the type to be substituted is also not immediately clear.
In Rust (and in many other languages), instead of templates, there are generalizations. Their parameters are required to provide a specific interface for substitution, and the correctness of such generalizations is reliably verified by the compiler:
It is worth noting that the committee
recognizes the importance of the concept , so that soon we can see something similar in C ++.
Non-generalizations
There is such a cool thing in C ++ - template specialization. It allows you to flexibly override the work of common functions for specific types. We can get better execution speed or reduce the amount of code, but this opportunity has a price. You see, you see the pattern of the method, and what it actually does you will know only when you study the entire code base for these specializations. Rust is simpler: if you have a generalized method in front of you, then you should not look for its code anywhere else
The obviousness of the language follows from
locality : everything that determines the behavior of the code can be found in its immediate vicinity. These properties give rise to
predictability : it is not necessary to write tests for each function and run it in the debugger to understand what it does. Predictability, on the other hand, makes it easier to read someone else’s code and find errors (or prevent them) in one’s own, which leads to better
control . For a programmer, this all means: ease of development in a team and debugging, confidence in the future and good sleep.
Rust is not dark magic: it does not quicken the dead and does not turn water into wine. Similarly, it does not solve all the problems of our craft. However, it makes us think and write code in such a way that potential problems are on the surface. In a sense, Rust bends the reality of programming, making it easier for us to move around in it, like a warp-drive.