DEV Community

Cover image for Rust Tutorial 5: Let's Build a Simple Calculator! (Part 2)
Khair Alanam
Khair Alanam

Posted on

Rust Tutorial 5: Let's Build a Simple Calculator! (Part 2)

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!

Semicolon and Code meme


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:

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Now that looks good. Let's get back to our four functions again.

Remember this block of code from a previous tutorial?

fn main() {
    let some_num: i32 = {
        let x: i32 = 45;
        let y: i32 = 34;
        x + y
    };

    println!("{}", some_num);
}

Enter fullscreen mode Exit fullscreen mode

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:

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
}
Enter fullscreen mode Exit fullscreen mode

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:

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;
        }
    }
Enter fullscreen mode Exit fullscreen mode

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.

    let my_tup = (1, true, "Hello");
Enter fullscreen mode Exit fullscreen mode

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:

    let my_tup: (i32, bool, &str) = (1, true, "Hello");
Enter fullscreen mode Exit fullscreen mode

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.

    let my_arr = [1, 2, 3, 4, 5];
Enter fullscreen mode Exit fullscreen mode

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:

    let my_arr: [i32; 5] = [1, 2, 3, 4, 5];
Enter fullscreen mode Exit fullscreen mode

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:

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
}
Enter fullscreen mode Exit fullscreen mode

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:

    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);
Enter fullscreen mode Exit fullscreen mode

Now let's get back to our project.


Let's create an array of length four to enclose all the four operations:

let ops = [add, subtract, multiply, divide];
Enter fullscreen mode Exit fullscreen mode

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:

let ops: [fn(f64, f64) -> f64; 4] = [add, subtract, multiply, divide];
Enter fullscreen mode Exit fullscreen mode

Let's break down the function type:

  • Firstly, we have the fn keyword to signify that the values given inside the ops array are functions.
  • Then we have two f64s enclosed inside the fn 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 of f64 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 type f64.

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:

result = ops[op - 1];
Enter fullscreen mode Exit fullscreen mode

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:

result = ops[(op - 1) as usize];
Enter fullscreen mode Exit fullscreen mode

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:

result = ops[(op - 1) as usize](x, y);
Enter fullscreen mode Exit fullscreen mode

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.

if op > 4 || op < 1 {
    println!("Invalid Selection!");
    return;
}

result = ops[(op - 1) as usize](x, y);
Enter fullscreen mode Exit fullscreen mode

Now, the final code will look like this:

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
}
Enter fullscreen mode Exit fullscreen mode

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:

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here's what I did:

  • I declared result as mutable using mut.
  • 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 for y_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 the trim() 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 the trim() 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/

Top comments (0)