DEV Community

Cover image for Getting familiar with Rust's Syntax
Francesco Ciulla
Francesco Ciulla

Posted on • Updated on

Getting familiar with Rust's Syntax

So, you've decided to learn Rust.

Good choice! Rust is an awesome language that combines the power of systems programming with modern language features, and it can be used for Web Development and blockchain.

However, when learning Rust, one of the blockers is getting familiar with its Syntax.

In this article, I'll do my best to provide examples that will make you feel comfortable with them.

Getting Started: Variables and Types

Let's start with the basics: variables.

By default, Rust variables are immutable. This might sound weird if you're used to languages like Python or JavaScript, which allow variables to change.

fn main() {
    let x = 5; // x is immutable by default
    // x = 6; // Uncommenting this will throw a compiler error

    let mut y = 5; // y is mutable
    y = 6; // No problem here
}
Enter fullscreen mode Exit fullscreen mode

Notice the let keyword? That's how you declare variables in Rust. If you want to change a variable, make it mutable with the mut keyword.

Type Annotations

Rust has great type inference: the compiler usually knows your variables' type.

But sometimes, you'll need to specify the type yourself:

// Here, we're explicitly saying that z is a 32-bit integer
let z: i32 = 10; 
Enter fullscreen mode Exit fullscreen mode

Rust's type system is one of its great advantages, so it’s worth getting comfortable with it early on.

Functions

Functions in Rust look pretty familiar if you've worked with other languages. But there are some syntax quirks to watch out for.

fn add(a: i32, b: i32) -> i32 {
    a + b // No semicolon means this is the return value
}
Enter fullscreen mode Exit fullscreen mode

Notice that we’re using -> to define the function's return type. Also, there's no return keyword here; Rust returns the last expression by default if you omit the semicolon.

It’s nice once you get used to it.

Ownership and Borrowing

Alright, here’s where things get interesting. Rust’s ownership model makes it stand out but can be tricky at first.

Let’s see another example

Ownership

In Rust, each value has a variable, which is its owner.

When the owner goes out of scope, the value is dropped. This is how Rust avoids memory leaks.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // Ownership of the String is moved to s2, s1 is now invalid

    // println!("{}", s1); // This would cause a compile-time error
}
Enter fullscreen mode Exit fullscreen mode

Here, s1 no longer owns the String after it’s moved to s2.

If you try to use s1 after that, Rust won’t let you. It’s like Rust says: "Hey, that’s not yours anymore."

Borrowing

But what if you want to use a value without taking ownership of it?

That’s where borrowing comes in. You can borrow a value by using references:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // We're borrowing s1 here

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}
Enter fullscreen mode Exit fullscreen mode

In this example, &s1 is a reference to s1. The calculate_length function temporarily borrows s1 without taking ownership. After the function is done, s1 is still valid. That's pretty cool.

Lifetimes

Lifetimes are how Rust keeps track of how long references are valid.

They can be confusing initially, but they’re crucial for safe memory management.

Let's see a very basic example, to get familiar with it.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, 'a is a lifetime parameter. It means the references x and y must live at least as long as the return value. This ensures that we don’t return a reference to something that’s already been dropped.

Pattern Matching

Rust's match statement is like a switch on steroids. It’s one of my favorite parts of the language because it’s so powerful and expressive.

fn main() {
    let number = 7;

    match number {
        1 => println!("One!"),
        2 => println!("Two!"),
        3 | 4 | 5 => println!("Three, Four, or Five!"),
        6..=10 => println!("Between Six and Ten!"),
        _ => println!("Anything else!"),
    }
}
Enter fullscreen mode Exit fullscreen mode

The match statement checks a value against multiple patterns and runs the code for the first matching pattern. The _ is a catch-all pattern, which is useful when you want to handle anything you haven’t explicitly matched.

Destructuring with Pattern Matching

You can also use match to destructure complex data types like tuples or enums.

fn main() {
    let pair = (2, 5);

    match pair {
        (0, y) => println!("First is zero and y is {}", y),
        (x, 0) => println!("x is {} and second is zero", x),
        _ => println!("No zeroes here!"),
    }
}
Enter fullscreen mode Exit fullscreen mode

This is just scratching the surface.

Match can do much more, but this should give you a solid foundation.

Error Handling

Rust doesn’t have exceptions. Instead, it uses the Result and Option types for error handling. It might feel a bit verbose initially, but it’s much safer than unchecked exceptions.

fn main() {
    let result = divide(10, 2);
    match result {
        Ok(v) => println!("Result is {}", v),
        Err(e) => println!("Error: {}", e),
    }
}

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Division by zero"))
    } else {
        Ok(a / b)
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, Result is a type that can be either Ok (success) or Err (error). This forces you to handle success and failure cases, which is great for writing robust code.

The ? Operator

To make error handling a bit more ergonomic, Rust provides the ? Operator. It’s a shorthand for propagating errors.

fn main() -> Result<(), String> {
    let result = divide(10, 0)?; // If divide returns Err, it returns from the function immediately
    println!("Result is {}", result);
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

This is Rust's saying, “If there’s an error, just return it.”

Advanced Syntax: Traits, Generics, and More

Now that we've got the basics down, let's dive into more advanced topics.

Traits: Interfaces (Kinda)

Traits are kind of like interfaces in other languages. They define shared behavior that different types can implement.

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

struct Article {
    title: String,
    content: String,
}

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

Here, we define and implement a Summary trait for the Article struct. Now, any Article can be summarized. Traits are super powerful for writing generic and reusable code.

Generics: Writing Flexible Code

Generics let you write functions and types that work with any data type.

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}
Enter fullscreen mode Exit fullscreen mode

This function works with any type T that can be compared. The PartialOrd part is trait-bound, meaning that T must implement the PartialOrd trait, which allows for ordering comparisons.

Practical Tips: Writing Idiomatic Rust

  • Use rustfmt: Rust has a built-in formatter that keeps your code looking sharp. Just run cargo fmt in your project directory.

  • Use Rust Analyzer: This powerful IDE extension provides code completion, refactoring, and more. It’s like having an assistant that knows Rust inside and out.

  • Clippy: This is a linter for Rust that catches common mistakes and suggests improvements. Run cargo clippy to see what it finds.

Conclusion

This quick article is to get a bit more familiar with Rust.

I have a series of free videos about these specific topics.

You can check it out here

Top comments (19)

Collapse
 
silvesterwali profile image
silvesterwali

thank you for sharing ...simple and easy to understand

Collapse
 
francescoxx profile image
Francesco Ciulla

That was the goal of the article: simplifying the rust syntax. Of course, it's not exhaustive, but it was more about lowering the complexity barrier. Thank you

Collapse
 
dung_truong_9dc5b43380474 profile image
Dung Truong

:)

Collapse
 
ezpieco profile image
Ezpie • Edited

I was going to learn rust, this is a good starting point. Especially the ownership and borrowing part, I can assure you that I have missed up a lot of C code... mostly skill issues as always.

Collapse
 
francescoxx profile image
Francesco Ciulla

glad it helped

Collapse
 
igor_vasilchikov_d1f4b6a5 profile image
Igor Vasilchikov

Beautiful, thank you . It feels like rust is Somewhat similar to scala and golang ...

Collapse
 
kerkg profile image
kerkg

Gonna implememt life times To my language, thanks

Collapse
 
francescoxx profile image
Francesco Ciulla

lifetimes is a concept specific to the rust programming language

Collapse
 
kerkg profile image
kerkg

Im sure they wont mind me using as well

Thread Thread
 
francescoxx profile image
Francesco Ciulla

gl

Collapse
 
jim_dunstan_5e3d97186c0a7 profile image
Jim Dunstan

What a great article.. Rusts pattern matching is really clever. I'm bored to death with C#, and Rust seems to inspire me again

Collapse
 
nikunjbhatt profile image
Nikunj Bhatt

Very inconsistent, irregular, weird and illogical systax. 🤦

Collapse
 
ken-preston profile image
Ken • Edited

It really is, no wonder Rust programmers make $30k/year more than us "lowly" C++ programmers at my company, no one can tell if what they're doing is right or not🤦🏼‍♀️ But here I am learning it since it's the only way I'll make over $200k without scoring a senior role...

Collapse
 
francescoxx profile image
Francesco Ciulla

interesting take, it seems the opposite to me. Can you elaborate on each point?

Collapse
 
nikunjbhatt profile image
Nikunj Bhatt
  1. Weird: In Rust, Variables are not variables, they are constants by default! And one has to use mut to define a variable variable! While most of the programming languages require to define constants by some other specific way.
  2. Irregular/Inconsistent: In Rust, a function's statement without semicolon is considered a return statement. It would be considered an error in other programming languages as the statement is missing a semicolon.
  3. Weird: Why there are "ownership" and "borrowing"!? (Or I think the example provided here for "borrowing" is not enough to understand "borrowing".)
  4. Illogical: In the Pattern Matching, there is 6..=10. Is .. one operator or ..=? If .. is an operator then other match/switch cases should also be prefixed with =, for example: =1 => println("One!"). Otherwise = in ..= seems extra.
  5. Weird: Error handling is also weird, or I am not able to / this post has failed to make me understand it.

Plus, I have some questions/doubts:

  1. The example of lifetimes is unclear. What are the meanings of single quote and "a"?
  2. The pattern matching example has a comma after each statement, why not a semicolon? Why the last default _ statement has a comma too?
  3. From the error handling and the ? operator examples, it looks like the code is returning Ok or Err. What does returning Ok or Err means?

It's fine if you don't make any changes to the post to make me understand or explain anything here. I don't know Python language but I have read its documentation for beginners and I liked its syntax. Python seemed most logical than most of the programming languages.

Thread Thread
 
dotsilas profile image
Silas Duarte

Immutable variables are not constants, since their values ​​are not known at compile time, constants are values ​​written to the binary during compilation; immutable variables can be assigned during program execution and immutable from there. There are several compression errors like this in all your criticisms, but I think the main one is that you think Rust has to look like the languages ​​you know and there is absolutely no reason for that. All languages ​​are different!

Thread Thread
 
thiagomg profile image
Thiago Massari Guedes

@nikunjbhatt I think you are complaining that Rust does not look like some languages you know.
Many of those weirdness you cited come from SML. If you are curious, see how to implement binary tree in SML. It's super easy. The pattern matching and algebraic datatypes came straight from there (or some other language of the SML family)

Collapse
 
thiagomg profile image
Thiago Massari Guedes

Why? I am curious now

Collapse
 
sairamthedev profile image
Sairam Mangeshkar

Indeed!