Rust Tutorial 5: Let's Build a Simple Calculator! (Part 2)
Khair Alanam
Posted on December 28, 2023
Reading Time: 15 minutes
Welcome back to the Rust Tutorial Series!
We will be continuing the simple calculator project, and on the way learn new concepts like tuples, arrays, etc.
So, let's get started!
More functions!
Going back to the code, notice that for the operations we do, we are doing them in our main code and not refactored into separate functions.
Now for this project, our functions are all one-liners. But it's a good practice to keep operations as separate functions so that we can maintain the readability of our main program code.
So, let's write functions for each of the operations:
rust
fn add(x: f64, y: f64) -> f64 {
return x + y;
}
fn subtract(x: f64, y: f64) -> f64 {
return x - y;
}
fn multiply(x: f64, y: f64) -> f64 {
return x * y;
}
fn divide(x: f64, y: f64) -> f64 {
return x / y;
}
Now, let's replace each of the operations in the match
statement op
with these functions:
use std::io;
fn main() {
// rest of the code
match op {
1 => result = add(x, y),
2 => result = subtract(x, y),
3 => result = multiply(x, y),
4 => result = divide(x, y),
_ => {
println!("Invalid selection");
return;
}
}
println!("The result is: {}", result);
}
Now that looks good. Let's get back to our four functions again.
Remember this block of code from a previous tutorial?
rust
fn main() {
let some_num: i32 = {
let x: i32 = 45;
let y: i32 = 34;
x + y
};
println!("{}", some_num);
}
And remember that odd-looking x + y
line of code that returns itself to the some_num
variable? Well, we can do that in functions too!
In our four functions, we can remove the return
keyword and the semi-colon to just do the same thing:
rust
fn add(x: f64, y: f64) -> f64 {
x + y
}
fn subtract(x: f64, y: f64) -> f64 {
x - y
}
fn multiply(x: f64, y: f64) -> f64 {
x * y
}
fn divide(x: f64, y: f64) -> f64 {
x / y
}
These are still valid functions! These "naked returns" can be tricky to someone who isn't used to Rust. But just know that Rust can do this :D
Let's take a look at this part of the code we wrote:
rust
match op {
1 => result = add(x, y),
2 => result = subtract(x, y),
3 => result = multiply(x, y),
4 => result = divide(x, y),
_ => {
println!("Invalid selection");
return;
}
}
Notice that the numbers are not random and they are ordered?
Wouldn't it be nice to keep them in an ordered sequence in such a way that I can just access them via "indices", and get rid of the entire match
statement completely?
That's where tuples and arrays come in!
Tuples
Tuples are compound data structures that can hold different types of data. These data are enclosed in parentheses ()
. They are fixed in length.
rust
let my_tup = (1, true, "Hello");
To add types for a tuple, you have to specify the type for each data inside the tuple in the exact order, enclosed in parentheses like this:
rust
let my_tup: (i32, bool, &str) = (1, true, "Hello");
We will get to know more about tuples in the later sections.
Arrays
Arrays are just like tuples in many ways, except unlike tuples, they can only hold values of the same data type. These data are enclosed in square brackets []
. They are also fixed in length.
rust
let my_arr = [1, 2, 3, 4, 5];
To add types for an array, you have to specify two parameters enclosed in square brackets; the first is the data type for each value in the array, and the second is the length of the array. These parameters are separated by a semi-colon. Here's what it looks like:
rust
let my_arr: [i32; 5] = [1, 2, 3, 4, 5];
We will get to know more about arrays in the later sections.
Accessing values in tuples and arrays
If you ever come across accessing values in arrays or lists in other programming languages, then it is almost identical here in Rust. Rust follows zero-indexing, that is the first element is in the 0th index, the second element is in the 1st index, and so on.
However, The syntax to access values in arrays is very different from that of tuples. Let me show you:
rust
fn main() {
let my_arr: [i32; 5] = [1, 2, 3, 4, 5];
let my_tup: (i32, f64, bool) = (23, 16.4, false);
println!("{}", my_arr[0]); // prints 1
println!("{}", my_tup.1); // prints 16.4
}
If you notice, we use square brackets to access any element in an array based on the given index. However, in tuples, we use dot notation.
Also, just a quick note, if you want to print the entire tuple or array in one print statement. We have to use the {:?}
formatting instead of {}
to do the same:
rust
let my_arr: [i32; 5] = [1, 2, 3, 4, 5];
let my_tup: (i32, f64, bool) = (23, 16.4, false);
println!("{:?}", my_arr);
println!("{:?}", my_tup);
Now let's get back to our project.
Let's create an array of length four to enclose all the four operations:
rust
let ops = [add, subtract, multiply, divide];
Remember when adding the function names in the array or tuple, you should not use parentheses along with the function name.
Now you might be thinking, what will be the data type of a function?
It's not necessary to mention types during declaration as Rust infers the type from the given value. However, here's what it looks like:
rust
let ops: [fn(f64, f64) -> f64; 4] = [add, subtract, multiply, divide];
Let's break down the function type:
- Firstly, we have the
fn
keyword to signify that the values given inside theops
array are functions. - Then we have two
f64
s enclosed inside thefn
keyword. This is to signify the data types of parameters of these four functions. If you remember, all these four functions take two decimals as arguments which are off64
type. - Lastly, we have the
f64
after an arrow->
. This is the return type of the function. As you can notice, every function in the given array returns a decimal of typef64
.
That's pretty much the breakdown. To summarise this bit, if you are including the functions in your array, make sure they have these checked:
- All the values are of the
fn
type - The number of parameters AND the datatypes of each of the parameters IN the exact order are the same for every function.
- Finally, the return type for each function has to be the same.
Now that we have our operations array, we can completely remove the match
case statement and just use the desired operation based on the selection given:
So let's write this one line of code by replacing the match
case statement:
rust
result = ops[op - 1];
You will get an error saying that you cannot use an i32
as an index. So for indexing, you have to use the type casting to convert op - 1
's type of i32
to something called usize
which is the type defined for those variables that act as pointers to the array elements.
It will look like this:
rust
result = ops[(op - 1) as usize];
Later, you will get a mismatched type error. This is because the result
variable is of type f64
but you are assigning an array element of type fn
, hence the mismatched type.
So to fix this, you have to "call" the operation along with arguments x
and y
using parentheses.
If the above sounds confusing, here's what it looks like:
rust
result = ops[(op - 1) as usize](x, y);
Finally, we have to put an if
check to see if the op
lies in the range of 1 to 4 before assigning the result to the result
variable, or else we will get the "out of range" error.
rust
if op > 4 || op < 1 {
println!("Invalid Selection!");
return;
}
result = ops[(op - 1) as usize](x, y);
Now, the final code will look like this:
rust
use std::io;
fn main() {
let result: f64;
let ops: [fn(f64, f64) -> f64; 4] = [add, subtract, multiply, divide];
println!("Enter the first number: ");
let x: f64 = input_parser();
if f64::is_nan(x) {
println!("Invalid input!");
return;
}
println!("Enter the second number: ");
let y: f64 = input_parser();
if f64::is_nan(y) {
println!("Invalid input!");
return;
}
println!("List of operators:");
println!("(1) Add");
println!("(2) Subtract");
println!("(3) Multiply");
println!("(4) Divide");
println!("Select the number associated with the desired operation: ");
let op: f64 = input_parser();
if f64::is_nan(op) {
println!("Invalid input!");
return;
}
let op: i32 = op as i32;
if op > 4 || op < 1 {
println!("Invalid Selection!");
return;
}
result = ops[(op - 1) as usize](x, y);
println!("The result is: {}", result);
}
fn input_parser() -> f64 {
let mut x: String = String::new();
io::stdin().read_line(&mut x).expect("Invalid Input");
let x: f64 = match x.trim().parse() {
Ok(num) => num,
Err(_) => {
return f64::NAN;
}
};
return x;
}
fn add(x: f64, y: f64) -> f64 {
x + y
}
fn subtract(x: f64, y: f64) -> f64 {
x - y
}
fn multiply(x: f64, y: f64) -> f64 {
x * y
}
fn divide(x: f64, y: f64) -> f64 {
x / y
}
Now let's make this calculator work as much as we want to by wrapping the logic inside a loop
statement.
Here's the final code:
rust
use std::io;
fn main() {
let mut result: f64;
let mut y_or_n: String = String::new();
let ops: [fn(f64, f64) -> f64; 4] = [add, subtract, multiply, divide];
loop {
println!("Enter the first number: ");
let x: f64 = input_parser();
if f64::is_nan(x) {
println!("Invalid input!");
return;
}
println!("Enter the second number: ");
let y: f64 = input_parser();
if f64::is_nan(y) {
println!("Invalid input!");
return;
}
println!("List of operators:");
println!("(1) Add");
println!("(2) Subtract");
println!("(3) Multiply");
println!("(4) Divide");
println!("Select the number associated with the desired operation: ");
let op: f64 = input_parser();
if f64::is_nan(op) {
println!("Invalid input!");
return;
}
let op: i32 = op as i32;
if op > 4 || op < 1 {
println!("Invalid Selection!");
return;
}
result = ops[(op - 1) as usize](x, y);
println!("The result is: {}", result);
println!("Continue? (y/n)");
io::stdin().read_line(&mut y_or_n).expect("Invalid Input");
if y_or_n.trim() == "n" {
break;
}
}
}
Here's what I did:
- I declared
result
as mutable usingmut
. - I declared a new variable
y_or_n
to check for loop condition whether to continue or not. - Then I wrapped the entire calculation logic inside a
loop
statement, except the first three declarations. - After the
result
printing, I take the user input fory_or_n
. Since we want a string, we don't have to parse it for integer input. - Finally I check for the condition comparing the input with
"n"
. I used thetrim()
function because when you press Enter key to finish the input, that key press is also recorded in the input (as\n
). So to remove that\n
, we use thetrim()
function.
That's pretty much it! Now you should probably be good with the Rust basics with just this simple calculator project!
In the next tutorial, we will explore one of the most important concepts of Rust and that is the concept of ownership and how memory management works in Rust.
Until then, have a great day ahead!
GitHub Repo: https://github.com/khairalanam/rust-calculator
If you like whatever I write here, follow me on Devto and check out my socials:
LinkedIn: https://www.linkedin.com/in/khair-alanam-b27b69221/
Twitter: https://www.twitter.com/khair_alanam
GitHub: https://github.com/khairalanam
I also have a new portfolio! I am a web developer as well as a web designer with 3+ years of experience designing creative web experiences for everyone.
Check it out: https://khairalanam.carrd.co/
Posted on December 28, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.