The one thing I learnt after one week as a Social Media Manager

As a Digital Marketing Specialist and enthusiast, I know the benefits of Social Media and the Internet, but it was working as a Social Media Manager that opened my eyes to the other facet of the…

Smartphone

独家优惠奖金 100% 高达 1 BTC + 180 免费旋转




My experiences learning Rust

In the first chapter of the book the main idea of Rust is coined as follows.

Coming from the embedded world I especially like the fact that Rust has a strong focus on safety, performance and concurrency. Many other modern languages have chosen an approach with garbage collection and many also require a runtime which can make them unsuitable for low-level programming. This is probably one of the main reasons that languages such as C and C++ are still very widely used.

Rust is a compiled language and the binaries are compiled directly to target architecture. Rust also does the safety checks during compilation so the added safety does not have runtime performance penalty, which makes it quite unique.

Ownership is a key concept in Rust. Each value has a variable that is owner of that data, and there can only be one owner at a time. If the value is moved to a new variable, the ownership is transferred and the old variable becomes invalid. Values can also be borrowed using references but there can only be one mutable reference at a time. These rules are enforced at compile time by a borrow checker.

In contrast, C/C++ do not have a concept of ownership similar to Rust’s. For instance, in C++ there can be multiple pointers and references to same data simultaneously, and it is the programmer’s responsibility to make sure that there are, for instance, no data races. At first it might feel that the borrow checker “just get’s in the way” because it produces a lot of compiler errors (at least before you get used to the ownership concept), but in the end it provides valuable safe-guards against many common issues.

Developers who are familiar with languages like C++, Python and Java should also be familiar with exceptions as an error handling mechanism. When an error occurs, an exception is thrown and the call stack is unwind until the exception is caught. If the exception is not caught at all, the program is terminated. This scheme allows some parts of the code, for instance libraries, to easily let the application logic to handle errors in a meaningful way. One drawback however, especially in C++, is that it is hard to tell which exceptions can be thrown and also the error handling code can be far away from the part that triggered the error.

The error handling approach in Rust is not to use exceptions. Instead, Rust has two different mechanisms for error handling. If the error is unrecoverable, the program will panic (i.e. terminate). Alternatively, for recoverable errors Result<T,E> type is used. With this approach the compiler can enforce that the error variant is always handled when the return value is obtained from the Result. This is also much more robust than encoding the failure information to the returned value (e.g. returning -1). Moreover, Rust also provides syntax to easily propagate errors from functions with ? operator.

In the example above, if either open or metadata call fails, the returned error is automatically propagated without the need to explicitly read and return it. This removes unnecessary boilerplate code and makes the resulting code simpler and cleaner.

I have strong C/C++ background so for me enumerations are just a collection of explicitly named constants that use integral type as underlying type. In Rust enums are much more than that, and they are better described as a type that represents one or many variants. These variants can also include data, and what’s more each variant can also have different data.

Another cool feature in Rust are patterns. Patterns and pattern matching are used in many places but probably the most powerful is the match operator. It can be thought of as, well, switch-case on steroids. Instead of matching simple values, it is possible to have complex patterns and have additional match guards as well. Also, the compiler enforces that all enum variants are always handled. Here’s an example:

Rust is a statically typed language which means that the compiler must know all the types during compilation. In other statically typed languages like C this means that the programmer must explicitly tell what types variables, parameters and return values are. In Rust a technique called type inference is used to deduce the types. So, even though the language is statically typed, most of the time programmer does not need to write the type explicitly. The compiler is able to figure it out from the context.

Rust also uses deref coercion which is easily demonstrated with a simple example.

Here’s an example that shows functional programming in Rust:

The code calculates occurences of characters from input string, and generates HashMap with char-count pairs. The example above outputs:

One interesting idea in Rust is that the language does not have a concept of null. At first thought this might seem very strange since we’re all so used to it, but it does remove a myriad of common bugs altogether. In other languages like C/C++ pointers have to be explicitly checked for null or the application will crash and burn if the null object is dereferenced.

Usually null value is used to indicate that some resource is not valid, and this information is indicated by a special null value. The problem is that the null condition needs to be manually checked which is error prone. The concept of something not necessarily being valid is still needed in Rust. It is just implemented differently.

Instead of indicating the invalid state of an object by special value, Rust implements this by wrapping the actual object in Option<T> or Result<T,E> type. This way when the concrete type is accessed the compiler will enforce that also the fail case is handled or the code won’t compile.

Rust is, for instance, able to detect data races between threads. That is, when multiple threads try to modify same data without proper mutual exclusion. The ownership system and the way mutexes are implemented also guarantees that locks are always acquired and released. Also the trait system is used to “mark” types that are thread-safe which means that the program will not compile if unsafe methods are used in a threaded context.

Rust uses a convention over configuration design paradigm which aims to decrease the number of decisions developers need to make. This paradigm is used heavily on code and test organization. Code modules, source files, unit tests and integrations tests are organized in a certain way and the compiler is able to find these items without explicit configuration.

One of the main benefits of this paradigm, besides the reduced configuration, is that different projects are organized in a similar manner which makes navigating other project’s sources easier. This is certainly not the case with C++.

If you have ever made a mistake with C++ templates (for instance the STL), you probably know what useful error message does not look like. You’ll most likely get couple of screenfuls of incomprehensible text. The error messages can be cryptic even for the most simplest of errors.

“Consider removing this semicolon” and it is even pointed out in the code. Pretty useful. This is important because especially in the beginning you are likely to get errors often (at least I did) due to the many checks Rust does. This of course is a good thing because the checks point out potential bugs in the code. But it certainly helps a lot when the compiler points you out to right direction.

In C and C++ the compiler converts numeric types automatically. So a C/C++ developer would expect that assigning a uint8 to uint32 should just work.

Well, Rust is more strict about types and the code above would produce an error. The programmer must explicitly cast the value to correct type: let b: i32 = a as i32;

Inheritance sets Rust apart from many common high-level languages. That is because Rust does not have it even though it does have other object-oriented features like encapsulation. On the other hand, Rust has a feature called traits to describe a common behavior same way as interfaces are used in other languages. Also trait objects can be used in a same way as polymorphic types in languages with inheritance.

Add a comment

Related posts:

Patience in Discomfort

When something uncomfortable arises, your first response may be to move or change as quickly as possible, temporarily restoring a fragile hold on peace. Listen instead to the wisdom of your heart. Be…

5 open source project management tools

OpenProject is a Web based project management solution which was designed using Ruby on Rails and Angular JS. It was released under the GNU General Public License Version 3 and is under consistent…