Review of two small programs written by newbie

Hi, newbie Rust programmer, self-studying with the book. At the uni I used to program in C and I sort of could handle Java but for most of my working life ABAP has been my bread and butter.

I just finished writing the recommended examples at the end of chapter 3, the temperature conversion and the Fibonacci generator.

I struggled a bit with parsing the temperature, because I was attempting to be more general than strictly required for the example and ended up in deeper waters than my limited knowledge of the language can deal with. So I went for a fixed format and it works.

use std::io;

fn main() {
    loop {
        println!("Please enter temperature to be converted:");
        let mut input = String::new();
        io::stdin()
            .read_line(&mut input)
            .expect("Failed to read line");

        let input = input.trim();
        if input.is_empty() {
            break;
        };
        let (temp, scale) = input.split_at(input.len() - 1);

        let temperature: f64 = match temp.parse() {
            Ok(temp) => temp,
            Err(_) => continue,
        };

        match scale {
            "C" => println!("{}C is {}F", temperature, (temperature * 9.0 / 5.0 + 32.0)),
            "F" => println!("{}F is {}C", temperature, (temperature - 32.0) * 5.0 / 9.0),
            _ => println!("No unit"),
        }
    }
}

I had considerably less difficulty with the Fibonacci generator:

use std::io;

fn fibonacci(index: i32) -> i32 {
    match index {
        0 => 0,
        1 => 1,
        _ => fibonacci(index - 1) + fibonacci(index - 2),
    }
}

fn main() {
    loop {
        println!("Which N-th Fibonacci number?");
        let mut input = String::new();
        io::stdin()
            .read_line(&mut input)
            .expect("Failed to read line");

        let input = input.trim();
        if input.is_empty() {
            break;
        };
        let fib_index: i32 = match input.parse() {
            Ok(fib_index) => fib_index,
            Err(_) => continue,
        };
        println!(
            "The {fib_index}° Fibonacci number is {}",
            fibonacci(fib_index)
        );
    }
}

Given that both programs work, what could still be improved? Any bad non-Rusty habits to break?
(the formatting is automatically applied when saving, because I am that lazy :smiley: )

Thanks in advance,
Andrea.

1 Like

These simple exercises look pretty good. You may want to continue by outsourcing common code into a library. As you might have noticed, you wrote te code for reading input from lines twice. You could outsource this into a generic function.

Also, regarding temperature conversion, you might not want to perform the calculation when printing the output, but put it inside a separate function.

I don't think creating a library just to outsource as simplistic of an operation (that's, moreover, utilized in that simplistic of a context, and not so much) would be the way to go.

Definitely one if we're talking about something without the "simplistic" part, though.

1 Like

I think it is a very good excercise for a beginner to learn about writing libraries to bundle common tasks, including dependencies and starting with generics, but ymmv.

2 Likes

First of all, thanks to all for the feedback.

@conqp on the issue of libraries I tend to agree with @Miiao : not much to gain by creating a library function for such a basic operation (after all, it's one line to call the standard I/O function and one line still for my wrapper, no gains) plus creating a library (a crate?) is not something I can do now.

Your other suggestion is more interesting, I'd like to discuss it: what are in your opinion the advantages of calculating the conversion in a separate function instead of inlining it? Are you thinking of "result" handling or something along those lines?

To be clear, I am more interested in the specifics on Rust than in the general topic of refactoring, since that is something that in other languages I can already decide on my own when and if that makes sense.

Your code looks good! The only possible red flag I see, regarding Rust usage, is that you're using recursion in fibonacci. This is natural and may be fine for now. But be aware that this could blow the stack (cause a stack overflow), so it would better be done as a loop that accumulates values, or if you're motivated to learn about them, using iterators.

Also note that Rust doesn't guarantee proper tail recursion like some functional languages. In this example tail recursion doesn't apply anyway. But because of this fact, you'll only see recursion used in Rust in cases where the number of recursions has a fixed maximum that is a small number. In the case of fibonacci of course, the index parameter determines the number of recursions, so there is in general no maximum.

3 Likes

I'm not @conqp, but here are some reasons that you might want to put your calculations in functions that are separate from the IO parts of your program:

  • They allow you to easily write tests that verify the correctness of the calculation.
  • Separating the two concerns, the application’s functionality (converting temperature) and the application’s user interface (read_line, println), makes it easier to understand or modify one without the other.
  • They let you use the same functionality from multiple places. For example, in a GUI program that writes output files, there might be an on-screen preview of part of what it would write. This is (usually) easier to implement and more efficient if it does this by running the computation and displaying it, rather than writing to a temporary file and reading that file again for display.
7 Likes

Easier than you think -

cargo init --lib mylib

then put your main(s) in src/bin - then run with

cargo run --bin main1

Many times I start with a bin, then change to a lib and add several mains...

No issues in the code. I'd write both programs pretty much the same after a long time using Rust. In shared/production code, I'd follow what @kpreid and @conqp have said about separating the code out, but I wouldn't do that for tiny learning programs while you're still at the beginning of the rust book. You should expect to encounter more differences in approach when you start dealing with structured data and lifetimes as there's a lot more space for choice there.

In your temperature program, once you parse temp, you can shadow the original temp with a new let temp: f64 = ... as you no longer need the original after parsing. This would be good because it would make it clear that the original temp will no longer be used after parsing. I'm stating this because in C/C++ this is something the compiler will throw a warning about if you use -Wshadow, but in Rust this is a very common pattern because it allows you to reuse variable names.

It's common to do stuff like:

fn new(path: impl AsRef<Path>) -> Self {
  let path = path.as_ref(); // &Path
  // use path reference
  let path = path.to_path_buf(); // PathBuf
  // store path value:
  Self {
    path
  }
}

Chapters 5, 6 & 10 (structures, enums, traits) would also greatly change how these two simple programs look. The note about shadowing is something that's mentioned in Chapter 3.

3 Likes

Hi. Your code examples are pretty basic (and this is honestly a good thing when you are just starting), so there is no much to comment here. But one piece of advice I could give would be this. Try extending it. When I am learning a language I like to create something really basic, and the gradually add more and more features, that teach me about other parts of the language. You can implement something manually, and then use already existing libraries. For example with your temperature converter you could try the following things:

Add support for CLI

Instead of interactive prompt you can get user input from the command line arguments. You can parse CLI arguments manually using std::env::args, or use phenomenal crate clap.

Add support for configuration files

Maybe your users mostly wants to do only one kinds of conversion (for example only from F to C). Your program could read configuration file it it is present and use user-provided default conversion path (with ability to explicitly overwrite). You can use crate serde (another fantastic crate), to automatically parse configuration file in any format you would like and crate xdg to discover those files according to XDG Base Directory Specification.

Full blown interactive

If you really like interactive prompts you could use crate inquire, which provides nice API that automatically crates different kinds of prompts. Or you can create full blown TUI with a crate like ratatui.

Network protocol

You can create a really simple network protocol, that will allow you to receive temperature conversions requests over UDP or TCP. You can start by manually serializing and deserializing and using standard library's network primitives, but further extensions are endless. This could be your gateway into async (I highly recommend tokio). You could do parsing using a parsing-combinator library like nom or do a "zerocopy" deserialization with something like rkyv .

FFI

You could create C-ABI interface for your library and invoke temperature conversions from other functions.

Time to deal with the latest round of feedbacks, thanks again to all who replied.

jumpnbrownweasel: my experience with functional languages is just theoretical, I understand your point but here I went for recursion just because it was more intuitive. Saving intermediate results in a static table would be another optimisation, for example. Anyway, I tried with index 46 (yielding almost the maximum for a 32 bit integer) and it still worked.

Kevin: right, good point. I'll try to keep this in mind once I get to that point where I can write tests, this is still a few chapters away.

Jesper: I'm following the book and slowly, too slowly, moving forward. The book has not talked about it at the point I am at, therefore I do not "know" how to do it, however easy it might be. For example, temperature conversion is also a library function but I deliberately avoided using it for the sake of the exercise.

Tin: thanks, your suggestion has been adopted.

Aleksander: oh, ideas for expansion are not what I am missing the most, that would be time.

Alright, moving on now :slight_smile:

P.S.: hit the new user limit for mentions, replacing them all with names.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.