DEV Community

Cover image for Discovering Rust
Joaquín Caro
Joaquín Caro

Posted on • Edited on

Discovering Rust

Being a developer it is easy to jump on the hype train and try to learn or even use in pet projects the latest libraries, frameworks and why not programming languages.
In a sector where everything evolves at a very high speed, sometimes it is difficult to keep the focus and not go crazy with the constant changes.
In my case, I like to read and practice different programming languages and the more the better, because they bring different visions of how to solve problems and even give you more tools for your day to day. And today’s article is about Discovering Rust.

Why Rust??

In one of the lunch & learn that we do in Apiumhub, a colleague gave me an idea to migrate an internal client that we have in pure bash to some other language and as candidates were Go and Rust, as between us we did not agree, we decided that each would choose a language and make its port.

The result was that that client never migrated, but along the way, both of us were able to discover the advantages and disadvantages of languages like GO and Rust.

From that moment on, I was hooked on Rust and desperate since then, any personal project or proof of concept or idea that I can think of, I try to go first with Rust.

What is Rust?

Rust is based on three pillars:

  • Performance, fast and efficient with memory, no runtime or Garbaje collector.
  • Reliability, ownership model guarantee memory-safety and thread-safety.
  • Productivity, great documentation, a compiler that in most cases tells you how to do it and gives you references to the documentation so you understand the problem and a tool that serves as a package manager, code formatter, improvement and suggestions.

Rust began by defining itself as a systems language, but over time that definition has been disappearing, there are more and more libraries for web, GUI , games and of course for systems!

The first thing that you think when you read “system language” is… I will have to deal with pointers, I will have to work releasing and assigning memory, I will have to work at a very low level without abstractions and of course… if it is not your day to day because it is scary to return to the origins of C C++, but the great majority of these fears are very well resolved and by the compiler! with which it facilitates the way a lot to us.

In this article we will see some basic concepts of rust and some of its features that under my point of view make it a very attractive language.

Rust Concepts

  • Rust has a wide range of primitive types
  • Rust bets on immutability, therefore, all declared variables are immutable unless they are specified with the keyword mut at the time of declaration.
  • The same happens with visibility, everything is private and in the case that you want to make it public you will do it with the keyword pub.
  • In the case of functions we can specify the keyword return to indicate that it is the value to be returned from the function or if the last sentence does not include the ; it will become the return of the function.
  • Rust has a very complete type inference and we rarely need to specify the type of variable we are creating.
  • Other features that give Rust a plus are the Generic types, Pattern matching, Macro system and many concepts of functional programming such as first-class functions, closures, Iterators
let name = "Jon".to_string(); // String type
let year = 2020; // i32 type
let age: u8 = 35;

let mut index = 0; //mutable variable
index = index +=1; 

let add_one = |x: i32| x + 1; //closure
let two = add_one(1);

let numbers = vec![1, 2, 3, 4, 5, 6];
let pairs: Vec<i32> = numbers.into_iter()
        .filter(|n| n % 2 == 0)
        .collect(); // [2, 4, 6]

fn greet(name: String) {
    println!("hello {}", name);
}
pub fn plus_one(x: i32) -> i32 {
    return x + 1; 
}
pub fn plus_two(x: i32) -> i32 {
    x + 2 
}
Enter fullscreen mode Exit fullscreen mode

Ownership, borrowing & lifetimes

These three concepts are the greatest complexity that we will find in Rust, since they are concepts that in languages with QA it is the QA itself that treats them making it transparent for the developer.

As it does not have an associated runtime or a Garbage Collector that frees the memory of the objects it does not use, all this is handled by the compiler with the help of the ownership,
Although the concept is suitable for more than one article, let’s see the basics of the concept with some examples.

Each value has an assigned variable (owner) and there can only be one owner at a time, when that owner is out of scope the value will be released.

So we have the following example:

fn main() {
    let name = "Jon".to_string();
    greet(name);
    println!("goodbye {}", name); //^^^^ value borrowed here after move
}

fn greet(name:String) {
    println!("hello {}", name);
}
Enter fullscreen mode Exit fullscreen mode

The compiler is telling us that the owner of the variable name has been passed to the greet function, so after running greet it is no longer in that scope. To solve it is as simple as indicating that what we want is to lend the owner, so that when the function ends, get the owner again, and that is indicated with &

fn main() {
    let name = "Jon".to_string();
    greet(&name);
    println!("goodbye {}", name);
}

fn greet(name:&String) {
    println!("hello {}", name);
}
Enter fullscreen mode Exit fullscreen mode

The lifetime is a check of the management of ownership and borrowing of the values, most of the times the compiler knows how to interpret it, but sometimes it is necessary to detail it. Without going into too much detail since the same as the ownership and borrowing, gives for a series of articles.

Structs

Structs exist to define our own types, they are created with the keyword struct and have no associated behavior.

To give behavior to a struct it will be done with the keyword impl.

struct Point {
    x: f32,
    y: f32,
}
impl Point {
    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}
let point1 = Point {
  x: 10.0,
  y: 20.0,
};

let point2 = Point {
  x: 5.0,
  y: 1.0,
};

let point3 = point1.add(point2); //Point { x: 15.0, y: 21.0 }
Enter fullscreen mode Exit fullscreen mode

In the Point implementation the add method receives as parameter self, this is because that method is of instance, if we don’t want to do it of instance it is as simple as removing the self

Traits

The Rust Traits are a collection of methods defined to be implemented by Structs, they are similar to the interfaces of other languages.

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Enter fullscreen mode Exit fullscreen mode

Enums

We can create enums, without value, with value or even have each enum have values of different types.

enum IpAddrKind {
    V4,
    V6,
}

enum IpAddr {
    V4(String),
    V6(String),
}

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

This is where pattern matching and destructuring play a very important role in managing enums

Alias

In Rust we can create our aliases for existing types

type Second = u64;
let seconds: Second = 10;
Enter fullscreen mode Exit fullscreen mode

Adding functionality to Types once they are defined

In some programming languages it is possible to add methods (extension methods) to classes once they have been defined, in Rust this would not be less! First we will create the path with the method that we want to use, then we will implement the path previously created to the type that we want to extend.

pub trait Reversible {
    fn reverse(&self) -> Self;
}

impl Reversible for String {
    fn reverse(&self) -> Self {
        self.chars()
            .rev()
            .collect::<String>()
    }
}

let hello = String::from("hello");
println!("{}",hello.reverse()); // olleh
Enter fullscreen mode Exit fullscreen mode

Operator Overload

Imagine being able to apply arithmetic operations to your own types, following the example before the struct point, being able to add Points with the simple + operator. In Rust this is possible by overloading the operators, which is the same again, implement traits for the types.
Here are all the possible operators to overload

struct Point {
    x: f32,
    y: f32,
}

impl std::ops::Add for Point { // implement Add trait for Point
    type Output = Self;

    fn add(self, other: Self) -> Self { //implement add function
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

let point1 = Point { x: 10.0, y: 20.0};

let point2 = Point {x: 5.0, y: 1.0};

let p = point1 + point2; // apply add trait
Enter fullscreen mode Exit fullscreen mode

Question mark operator

It is very common to see the handling of errors in Rust through the type Result being T the value of OK and E the one of the error, and the handling of this type is done through pattern matching. Let’s
imagine that we want to read the content of a file, for it we will use the function read_to_string of the module fs, the result of this function is a Result.

let content = fs::read_to_string("filename");
match content {
    Ok(file_content) => { println!("{}", file_content) }
    Err(e) => { println!("Error reading file: {}", e) }
}
Enter fullscreen mode Exit fullscreen mode

By means of pattern matching we have dealt with both possible cases of the result. For this example we only wanted to print it by console, but imagine that you want to treat that content, the code becomes a little more complex, for these cases in Rust there is the ? operator with which we directly obtain the value of the Result if it has gone well and if not directly the error will be returned as return of the function. But in order to use the ? operator the method signature has to return a type Result

fn get_content() ->  Result<String, Error>{
    let content = fs::read_to_string("filename")?;
    println!("{}", content);
    Ok(content)
}
Enter fullscreen mode Exit fullscreen mode

If the reading of the file fails, it will automatically return the Error trace, otherwise the code will continue to be executed until it returns the Result ok of the content.

Type Conversion

Another very interesting feature is the type conversion that can be applied in Rust only by implementing the Trait std::convert::From

The clearest use case is a function where you handle different types of errors but the return of your function you want it to be in the case of error a mistake of yours, of your domain, through pattern matching we could go hunting all the results and creating the Results of our type, but it would make our code difficult to maintain and not very readable.

Using the ? operator and type conversion would look like this:

fn get_and_save() -> Result<String, DomainError> {
    let content:  Result<String,HttpError> = get_from_http()?; 
    let result: Result<String,DbError> = save_to_database(content)?; 
    // with ? operator in case of error, the return of the function will be Result of Error
    Ok(result)
}

pub struct DomainError {
    pub error: String,
}

impl std::convert::From<DbError> for DomainError {
    fn from(_: DbError) -> Self {
        DomainError {
            error: (format!("Error connecting with database")),
        }
    }
}

impl std::convert::From<HttpError> for DomainError {
    fn from(_: HttpError) -> Self {
        DomainError {
            error: (format!("Error connecting with http service")),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Cargo & utilities

And if we talk about Rust, we can’t leave out your package manager position, which comes with the Rust installation itself.

With cargo we can create a project from scratch, manage the dependencies, generate the release, launch the tests, generate the documentation, publish the package to the registry…

In addition there is a large list of third party commands available

Resources

Although Rust was created by Mozilla, rust is maintained by the community, it is the community itself that proposes the changes and adapts the language to the needs.

Some of the most interesting links to follow in order to keep up with the news:

rust-lang.slack](rust-lang.slack.com): Slack in which all language issues are discussed with a lot of help for those who are new to the language.

Weekly: Weekly newsletter with news about language changes, interesting articles and conferences/talks.

youtube: official Rust channel where conferences, meetings of the different working groups formed for language development are posted.

Discord: Discord server where most of the working groups are coordinated to maintain the language.

The Rust Book: Documentación oficial sobre Rust, todo lo que debes de saber sobre el lenguaje está en el libro.

And a couple of personal projects with which I’m putting into practice everything I’m reading about Rust

Adh : is a docker client, ported from the original ApiumHub with the commands I use most in my daily life, there are still many features to be added.

Covid-bot: is a telegram bot developed on rust to keep track of covid-19

Originally published at Apiumhub

Top comments (0)