Learning Rust by Converting Python to Rust | by Shinichi Okada | Towards Data Science

[Updated on 2021–02–18. Codes changed to Gist and added links]
Table of ContentsIntroduction🦀 Leetcode Unique Paths🦀 Python Code🦀 First Step in Rust🦀 Examples of Primitive Data Types:🦀 Functions🦀 Statements and Expressions🦀 Variables🦀 Macros🦀 if-else Statement (Step 2)🦀 Calling a Function🦀 Range🦀 Arrays, Tuples, and Vectors🦀 as Keyword🦀 Final Code🦀 Struct and Impl🦀 Trait🦀 Runtime & Memory UsageConclusion
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.
towardsdatascience.com
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.
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.
There must be many ways to solve this problem using Python, but we are going to use the following solution.
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.
If m or n equals 1 then it returns 1.
If n is less than m 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].
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.
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.)
We are going to start with a basic Rust code.
Rust always runs the main function first when you run the program. We create a function called unique_pathswith two parameters mand 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 -(231), which is -2147483648, and 231-1, which is 2147483647.
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 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.
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.
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.
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".
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.
if-else if-else has the following form.
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.
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.
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.
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.
Output:
x = 500, y = 6.2, z = 41
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 abovelet 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];
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 theas 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.
We use all the above codes to get the number of unique paths.
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 defaultwarning: 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.
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,   ...}
&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.
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.
We use self 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:
Traits are similar to interfaces in OOP languages. They are used to define the functionality a type must provide.
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.
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()        }       }}
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.
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.
Get full access to every story on Medium by becoming a member.