Learning Rust by Converting Python to Rust
By: Shinichi Okada
Tags: Programming, Rust, Rustlang, Python, Software Development

Learning Rust by Converting Python to Rust

Photo by Le Wagon on Unsplash

[Updated on 2021–02–18. Codes changed to Gist and added links]

Introduction

Rust is a statically and strongly typed systems programming language. Rust is for people who crave speed and stability in a language.

I consider myself a Rust beginner. I started learning Rust after writing this article and I am still learning new things every day. I am at the stage where I can convert simple Python code to Rust and be able to explain what I am coding.

In this article, we are going to use a Python solution of one of the Leetcode problems and convert it to Rust code. You will find not only similarities between the two languages but also learn Rust programming.

You can run the code and also tweak it in the examples in this article.

Leetcode Unique Paths

LeetCode is a very popular website where you can level up your coding skills. One of the problems is called “Unique Paths” where a robot is located at the top-left corner of a m x n grid. You can only move either down or right and you need to find how many unique paths there are between two points.

Image from LeetCode

For example from point A to B in the image below, there are 3 unique paths and from A to C there are 5 unique paths. The following diagram helps you to find the number of unique paths from A to each area. Since you only move down or right, you add the number above and left to find the number of the unique paths to that point.

Image by the author

Python Code

There must be many ways to solve this problem using Python, but we are going to use the following solution.

Solution using Python. Try this Python code online.

The result of Python code on LeetCode.

In this Python code:

We create a class called Solution. We define a method called “uniquePaths”. (It is the starting code.)

It takes self, integer m , and an integer n as parameters and returns an integer. It uses type annotations.

, and an integer as parameters and returns an integer. It uses type annotations. If m or n equals 1 then it returns 1.

or equals 1 then it returns 1. If n is less than m then it switches the positions and runs the same method.

is less than then it switches the positions and runs the same method. dp = [1]*n will create a list with n number of 1. e.g [1]*3 will create [1,1,1] .

will create a list with number of 1. e.g will create . We use a double for-loops and range to add a previous number to the next item. [1,2,3] , [1,3,6] etc. You can check this by changing the return value to return dp .

, etc. You can check this by changing the return value to . We return the last number in the list using [-1] .

We are going to convert this Python code to Rust step by step in this article.

The above result shows that the runtime is 36 ms and the memory usage is 13.6 MB. We will compare this one with the Rust result later.

(When I submitted it again to LeetCode, the result changes and the above one was the best one. You may have a different result.)

First Step in Rust

We are going to start with a basic Rust code.

Step 1 in Rust. Try this Rust lang online.

Rust always runs the main function first when you run the program. We create a function called unique_paths with two parameters m and n , which are i32 types.

In function, we must declare the type of each parameter. Every variable, item, and value in a Rust program has a type.

The type of value defines the interpretation of the memory holding it. i32 is one of the primitive data types and a 32-bit fixed-sized signed integer types. i32 can hold a number between -(2³¹), which is -2147483648, and 2³¹-1, which is 2147483647.

Examples of Primitive Data Types:

More primitive data types are in this link.

By the way, Rust’s default integer type is i32 so if you don't annotate the type of parameters in a closure and if they are integers, Rust will use i32 . Rust's compiler is able to infer the types of the parameters and most variables.

Functions

Functions use the fn keyword to start. Rust code uses a snake case as the conventional style for functions. Functions can return values and we declare their type for the return value using an arrow -> . The unique_paths returns data type of i32 . Rust uses {} for the function body.

Try this Rust lang code online. Function example which returns String.

Statements and Expressions

We return the sum, m + n . Functions' bodies are made of a series of statements and ending in an expression.

Statements are instructions that perform some action and do not return a value. Expressions evaluate to a resulting value. — Function Bodies Contain Statements and Expressions

The expression m + n doesn't include ending semicolons because we want to return it. If you add a semicolon to the end of an expression, you turn it into a statement and it will not return a value.

Variables

In the main() function, we use a keyword let to indicate a variable. Local variables are immutable as default, but you can use mut to make them mutable. For our case, we don’t need to make it mutable.

Try Rust variables example online.

Macros

println!() is one of the standard macros. It parses the format string and converts it into the print format.

pritln! fills up the placeholders in order. You can use the index and name="vale" .

if-else Statement (Step 2)

In the next step, we add a if-else statement. expr == expr (where expr means expression) is the equality comparison. expr || expr is the logical OR.

If either of m or n equals 1 then we return 1. Please note no semicolon after 1 since we want to return 1.

Rust doesn’t use a bracket for the condition in if statement.

Try Step 2 in Rust using if statement online.

if-else if-else has the following form.

Calling a Function

Try Step 3 in Rust online.

This may not be necessary to pass the LeetCode, but this shows you how to call the own function from within.

If n is smaller than m , we swap the variable positions and call the own function.

Range

In Python, we used range as:

for i in range(1,m):

for j in range(1,n):

In Rust, we use .. or ..= .

The above code will output 1 to 9.

The above code will output 1 to 10.

Arrays, Tuples, and Vectors

In Python we used dp = [1]*n . This will create n numbers of [1]. For example when n=3, [1, 1, 1] .

Let’s check which one from arrays, tuples, or vectors in Rust we can use for this purpose.

Arrays

Arrays must have the same type and fixed-length. Arrays are immutable by default and we can’t use mut to change its element.

Try Rust array online.

Output:

one: 1, b: [4, 5, 6], c: [1, 1, 1, 1, 1], d: [1, 1, 1, 1, 1]

We also can’t set an array length dynamically. Because arrays have a length defined at compile time. A variable is not known at compile-time because it can vary. The compiler would not know how much space to allocate on the stack to provide storage for the array.

If you try to print out without the {:?} , you will fail. We can’t format an array with the default formatter. All the Rust data types implement Debug trait, and you can use {:?} to format it and print an array.

So we can’t use an array here.

Tuples

Tuples can have different types and have a fixed length.

let tup: (i32, f64, u8) = (100, 2.32, 4);

To get the individual values out of a tuple, we can use pattern matching to destructure a tuple value.

Try Rust tuple online.

Output:

x = 500, y = 6.2, z = 4

1

Again we can’t use tuples since they must have a fixed length.

Vectors

Rust vectors store more than one value next to each other in memory. Vectors store values of the same type.

A contiguous growable array type with heap-allocated contents. — doc.rust-lang.org

The vec! macro creates a new vector that holds the values you give it.

let v = vec![1, 1, 1];

// this is the same as above

let u = vec![1; 3];

A vector is like a re-sizable array but all elements must be in the same type.

So we will need to use a vector.

dpm = vec![1; m as usize];

dpn = vec![1; n as usize];

as Keyword

The second number, the length, in vec![1; 3] needs to be usize type. We annotated m and n as i32 but we can cast types using the as keyword. We can use the as expression only converting between primitive types.

as is most commonly used to turn primitive types into other primitive types. — doc.rust-lang

vectors has a method len that returns the number of elements in the vector.

Final Code

We use all the above codes to get the number of unique paths.

Try the final step in Rust online.

You may notice that #[allow(unused_variables)] . It will work without it but the compiler gives you a warning:

warning: unused variable: `i`

--> main.rs" data-line="11" data-column="13">main.rs:11:13

|

11 | for i in 1..dpm.len() {

| ^ help: if this is intentional, prefix it with an underscore: `_i`

|

= note: `#[warn(unused_variables)]` on by default



warning: 1 warning emitted

To remove this warning we add #[allow(unused_variables)] .

We will see the final code for LeetCode later. Before that, we are going a step further with this code.

Struct and Impl

We are going to apply struct , impl , and trait to the above solution. Please note that this won’t work on LeetCode.

Structs are used to encapsulate related properties into one unified data type. Structs are different from classes in other languages. For example, Rust’s struct doesn’t support inheritance.

Tuples are convenient but using the index like t.0 and keeping track of the meaning of each part isn't straightforward.

Rust structs contain named fields. We use a keyword struct and set fields with its type within the curly bracket. A structure defines data as a key-value pair.

struct Name_of_struct {

field1:data_type,

field2:data_type,

...

}

Try Rust struct online.

&str and String

You may be wondering why we need to_string() . Rust has two main types of strings: &str and String . &str is called ‘string slices’. A string slice has a fixed size, and cannot be mutated. A String is stored as a vector. String is heap-allocated, growable, and not null-terminated. (More on this in this link.)

"John" is a &str and since we defined the type of first_name as String in the struct , we need to convert to a String using to_string .

We put associated functions into a impl block.

Try Rust impl online.

Method parameter self, &self, and &mut self

We added a method full_name in impl Person . The function takes &self as the first parameter. It can take one of self , &self , or &mut self .

We use &self when we want to read the data in the struct, not write to it.

when we want to read the data in the struct, not write to it. We use self when we want the method to take ownership.

when we want the method to take ownership. We use &mut self when we want the method to write to it.

We use the dot operator for instance methods as in p.full_name() to access the field.

Let’s use struct and impl to our code:

Try Using struct and impl online.

Trait

Traits are similar to interfaces in OOP languages. They are used to define the functionality a type must provide.

Try Using trait to our code online.

We added a trait called UiquePaths with a function unique_paths . We need to update the impl using impl trait-name for struct-name .

We instantiate it in the main function using :: notation.

Now we implemented struct , impl , and trait .

Runtime & Memory Usage

The following solution is adjusted for the LeetCode environment.

impl Solution {

pub fn unique_paths(m: i32, n: i32) -> i32 {

if m == 1 || n == 1 {

1

} else {

let mut dpm = vec![1; m as usize];

let mut dpn = vec![1; n as usize];

for i in 1..dpm.len() {

for j in 1..dpn.len() {

dpn[j] += dpn[j-1];

}

}

*dpn.last().unwrap()

}

}

}

LeetCode result for Rust

Please note Runtime 0 ms. Rust does not have a runtime. Rust applies Zero-cost abstraction. There’s no extra runtime overhead that you pay for certain powerful abstractions or safety features that you do have to pay a runtime cost for in other languages.

… iterators, although a high-level abstraction, get compiled down to roughly the same code as if you’d written the lower-level code yourself. Iterators are one of Rust’s zero-cost abstractions, by which we mean using the abstraction imposes no additional runtime overhead. — Rust Programming Language

Rust’s memory usage was 2.1 MB comparing to 13.8 MB in Python.

Rust has unique ways of managing heap memory and it uses a concept called ownership to manage it. I recommend that you dive into the Rust documentation to learn more about it.

Conclusion

We touched the surfaces of primitive data types, type annotations, functions, statements, expressions, variables, macro, if statement, range, arrays, tuples, vectors, struct , impl , and trait . You can find more information in the official book.

As I stated before I am still learning Rust. I appreciate expert feedback on how to improve the solution.

I hope you learned something and are ready for the next step. There are many key concepts we couldn’t cover in this article. Please stay tuned for the next post.

Newsletter

Get full access to every story on Medium by becoming a member.