nidomiro

Software developer stories
en de

Rust-03: Improving the simple program

In the last post we created a simple program that would accept an input like 5+5 and calculates the result. In this post we will improve this program.

To prepare for adding more operators we need to improve the input-parsing. Currently, we just split the input-string at the plus-sign. If we want to know the operator, we have to choose another approach.

In this case I chose regex:

#[derive(Debug)]
struct Operation {
    left: i32,
    operator: char,
    right: i32,
}

impl Operation {
    fn from_string(input: &String) -> Option<Operation> {
        let regex = Regex::new(r"^\s*(\d+)\s*([+])\s*(\d+)\s*$").unwrap();

        regex.captures(&input).map(|capture| Operation {
            left: (&capture[1]).parse().unwrap(),
            operator: (&capture[2]).parse().unwrap(),
            right: (&capture[3]).parse().unwrap(),
        })
    }
}

As you can see we have a new thing called struct in the code. If you are coming from a language like Java or Typescript, a struct can be seen as a class without any methods. This struct is called Operation and as the name suggests it holds an operation. The fields left and right are both typed as an i32 or in other words a “signed 32-bit integer”. The field operator is a char since we only use one character for an operator. In rust strings and chars are utf-8, therefore a char can contain one unicode codepoint, not just a byte.

The other new thing is a impl called Operation. An impl is the method-part equivalent of a class. Here you can define methods, that are available to call on the struct with the same name. In this case we have one method called from_string. This method is our new parser. It takes a string as an input and returns an Operation wrapped by an Option. Rust does not have null, so every time when there is the possibility that a value could be non-existent an Option is used.

If you read the code carefully, you may have noticed that the last statement of the function does not have a semicolon. That is on purpose. If the last statement of a function has a semicolon, it returns () (equivalent to void), if it isn’t preceded by the keyword return. You can omit the keyword return by omitting the semicolon on the last statement of a function. Then, the value of the last statement is used as the return value.

Now we have a new method of parsing, so lets look at the new evaluator:

let operation = Operation::from_string(&input).expect("Your input is invalid");

let result = match operation {
    Operation { left, operator: '+', right } => Ok(left + right),
    x => Err(format!("{} is not a valid operator", x.operator)),
};


println!("The result is: {:?}", result.unwrap());

Here we can see another example of a powerful feature of Rust: Pattern matching. With pattern matching, we can save a lot of else-if-statements. In this simple case it wouldn’t make a big difference, but we want to expand the code.

So what happens inside the match operation {... statement? Each line contains a Pattern, followed by => and a statement or block. The pattern Operation { left, operator: '+', right } matches every Operation with + as its operator. left and right have no value assigned in the pattern, so all values match. You can use left and right as variables in your following statement, as seen in the code. x => ... matches all cases, it’s therefore the default case.

After these changes, we are at state: 24affc71

In the next post we will add more operators to the calculator and more.


This post is part of a series: