Rust for the Novice Programmer/Basic Maths Testing Program/Traits and Displaying

Traits and Displaying

edit

Very little in Rust is magic. A lot of the stuff we've learned and used earlier could be reimplemented in our code. What we would like to do in our program is to use "{}" to display the operator directly.

What we want to write in our print_question() is this:

fn print_question(num1: i32, num2: i32, operator: Operator) {
    println!("What is {} {} {}?", num1, operator, num2);
}

However, if we try to run this, we get the message:

error[E0277]: `Operator` doesn't implement `std::fmt::Display`

The way things like println!() works in Rust is that when it sees "{}", it checks whether the corresponding object after the comma implements a 'Trait' called 'Display'. A trait is like a marker on a type, indicating that it has certain methods that can be used.

If we look at the docs for std::fmt::Display we see that the trait has:

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

We do have to remember this is all under std::fmt so we have to put this before each type. A lot of this will look quite strange but essentially we have to write a function that matches this, we can use the macro write!() to make things easier like follows:

impl std::fmt::Display for Operator {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "+")
    }
}

The std::fmt::Result on its own is a shorthand for the Result<(), Error> above. Also the std::fmt:: before everything is a little tiresome. What we can do is put

use std::fmt;

at the top and change our implementation of the Display trait as follows:

impl fmt::Display for Operator {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "+")
    }
}

The use statement means whenever the code encounters fmt, it will assume it is std::fmt. This is helpful in reducing the amount of code we have to write but can also be dangerous if we have our instance of fmt since it won't be able to be distinguished. That is unlikely to be the case for us so we can be comfortable making this change. We don't have to put it at the top but generally we will want to put all of our use statements in the same place. That being written, if we run the program now we get:

What is 35 + 23?

Oops, we didn't check which operator it is, let's fix that now:

impl fmt::Display for Operator {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Operator::Addition => write!(f, "+"),
            Operator::Subtraction => write!(f, "-"),
        }
    }
}

Now if we run the program, we get:

What is 35 - 23?

which is what we wanted.

All this may seem a little convoluted but there's a few advantages of doing things like this:

  • Reuse, we can now easily print out the operator in a different place easily
  • Breaking up the problem, we don't want everything in the same function, or the same place. In this way the Operator type and functionality is separated out from the other parts of the program.
  • Easier to edit, since everything is in the same place we just have to change the logic in the Operator type and related functions and all uses of it in other places will be correct.

Next: we look at how to take in user input