loading...

Learning Rust via the Advent Of Code - Part 2

brunooliveira profile image Bruno Oliveira ・7 min read

Introduction

Following on my previous post, I'll work on the second part of the first problem of AoC, which states:

During the second Go / No Go poll, the Elf in charge of the Rocket Equation Double-Checker stops the launch sequence. Apparently, you forgot to include additional fuel for the fuel you just added.
Fuel itself requires fuel just like a module - take its mass, divide by three, round down, and subtract 2. However, that fuel also requires fuel, and that fuel requires fuel, and so on. Any mass that would require negative fuel should instead be treated as if it requires zero fuel; the remaining mass, if any, is instead handled by wishing really hard, which has no mass and is outside the scope of this calculation.
So, for each module mass, calculate its fuel and add it to the total. Then, treat the fuel amount you just calculated as the input mass and repeat the process, continuing until a fuel requirement is zero or negative. For example:
A module of mass 14 requires 2 fuel. This fuel requires no further fuel (2 divided by 3 and rounded down is 0, which would call for a negative fuel), so the total fuel required is still just 2.
At first, a module of mass 1969 requires 654 fuel. Then, this fuel requires 216 more fuel (654 / 3 - 2). 216 then requires 70 more fuel, which requires 21 fuel, which requires 5 fuel, which requires no further fuel. So, the total fuel required for a module of mass 1969 is 654 + 216 + 70 + 21 + 5 = 966.
The fuel required by a module of mass 100756 and its fuel is: 33583 + 11192 + 3728 + 1240 + 411 + 135 + 43 + 12 + 2 = 50346.
What is the sum of the fuel requirements for all of the modules on your spacecraft when also taking into account the mass of the added fuel? (Calculate the fuel requirements for each module separately, then add them all up at the end.)

So, it's actually very similar to the first problem in nature:

we need to read an input fie, perform transforming operations on the input, and sum it all together

The main difference here is that the transforming operation needs to be repeated until a certain value is reached.

Mapping the values to the transforming operation

Using the example in the problem statement, if the input is 1969, here's what we need to do:

First input: 1969/3 -2 = 654.

So, using 654, we need to repeat the operation on it, while keeping a running total, until the input is 0 or less. In pseudo-code:

input = 654
total = 0
while input > 0:
  total = total + input
  input = input/3 - 2
return total

So, we need to perform the above operation for each number, and, sum together all the results to get the final answer. Since this is a dedicated computation, it's best to encapsulate it in a function.

Defining functions in Rust - learning with the compiler

So far, the most impressive takeaway I have from Rust, is how helpful the compiler can be in guiding you how to write code, both in terms of syntax as well as style. Coming from a Java or Python background, and, knowing that fn is used to define a function, my first iteration to declare the function body was the following:

fn computeTotalFuel(v: i32) {
   return 5;
}

Obviously, we are missing information about the function return type in this declaration. But, on it's own, it seems pretty reasonable: I use fn as the keyword to declare a function, followed by a function name, and in between parentheses the arguments, with its type specified: we have only the integer value v, representing the amount of fuel as parameter, we use the curly brackets to open a function body (which defines the function scope - everything in between these brackets is local to our function), and, I use the return keyword to return a single integer value.

when compiling this, where's what we get:

error[E0308]: mismatched types
 --> main.rs:5:9
  |
4 | fn computeTotalFuel(v: i32){
  |                            - possibly return type missing here?
5 |     return 5;
  |            ^ expected (), found integer
  |
  = note: expected type `()`
             found type `{integer}`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

This is a huge amount of information from a single compile:

  • we know that the return type probably needs to be placed after the argument list AND before the opening curly bracket, as pointed by the compiler.

  • when returning 5, and when missing to declare a return type, we see: ^ expected (), found integer, which probably means that the () represents the corresponding type to void in Java and C/C++.

So, not only we learnt what we need to do next, we actually learnt new information about the language. In fact, let's keep ourselves totally offline and restricted to the compiler. Let's attempt to use : to declare the return type:

fn computeTotalFuel(v: i32) : i32{
   return 5;
}

The error changed:

error: expected one of `->`, `where`, or `{`, found `:`
 --> main.rs:4:29
  |
4 | fn computeTotalFuel(v: i32) : i32{
  |                             ^ expected one of `->`, `where`, or `{` here

error: aborting due to previous error

And, we see that we can use either -> or where here.

Since we saw earlier that the let clause can be used in more complicated expressions, the where keyword can be used to specify generic contexts for these expressions, so the -> must be used to specify the return type.

Like so, we can now fully define our function:

fn computeTotalFuel(v: i32) -> i32 {
    let mut tot: i32 = 0;
    let mut acc: i32 = v/3 - 2;
    while(acc > 0){
       tot = tot + acc;
           acc = acc/3 -2;
    }
    return tot;
}

This does exactly what we described above, to carry the computation for the extra fuel we need. A small remark here is the use of the mut keyword, after the let, to indicate that these variables are mutable, which means the value bound to them can be reassigned during runtime.

An advantage of Rust is that by making all variables to be immutable by default, it makes it explicit to the programmer and future readers of the code that the values here might change, which in itself, embeds some ideas about the nature of the algorithm. In this case, these accumulator variables will naturally change their values, so we need them to be mutable.

Learning Rust coding style and conventions

Every language has its own identity, and there's usually a philosophy behind its design and intent that the designers usually embed not only in what's semantically possible but also in how the code looks, in terms of coding conventions. There are committees responsible for this in languages like C++, enhancement proposals for Python (the famous PEPs - Python Enhancement Proposals), and usually, IDEs like Eclipse and IntelliJ will have lots of these rules for coding conventions embedded in them, so that we can write idiomatic code, no matter the language we are using.

This is important, because, when in a team, it's great to have an agreed upon coding convention that everyone can follow:

  • on one hand, it reduces friction when reading and modifying code, as all code looks standardized;

  • on the other hand, the team can agree on what is "bad code", so newcomers can get up to speed faster in writing good, idiomatic code.

Let's see the warnings we get when we compile our code now, with the function already implemented:

  src git:(master)  rustc main.rs   
warning: unnecessary parentheses around `while` condition
 --> main.rs:7:7
  |
7 |     while(acc > 0){
  |          ^^^^^^^^^ help: remove these parentheses
  |
  = note: `#[warn(unused_parens)]` on by default

warning: function `computeTotalFuel` should have a snake case name
 --> main.rs:4:4
  |
4 | fn computeTotalFuel(v: i32) -> i32 {
  |    ^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `compute_total_fuel`
  |
  = note: `#[warn(non_snake_case)]` on by default

So, we see that we have unnecessary parentheses around the while condition, and that the function name should be snake case.

Note: these are compiler warnings that are on by default and can be switched off, but, it's always a great way to learn to follow the compiler, especially when it is this informative and useful in getting us up to speed in writing clean code.
Note that I'm learning a lot simply from the compiler on its own, which is something I find very rare to be possible to do. Use the compiler in your learning process.

If we address these warnings, our function will look like:

fn compute_total_fuel(v: i32) -> i32 {
    let mut tot: i32 = 0;
    let mut acc: i32 = v/3 - 2;
    while acc > 0 {
       tot = tot + acc;
           acc = acc/3 -2;
    }
    return tot;
}

Quite different from the original style in which I would write it, more idiomatic, and we get a clean, warning-free compilation.

Wrapping up

Our complete code will look like:

use std::env;
use std::fs;

fn compute_total_fuel(v: i32) -> i32 {
    let mut tot: i32 = 0;
    let mut acc: i32 = v/3 - 2;
    while acc > 0 {
       tot = tot + acc;
           acc = acc/3 -2;
    }
    return tot;
}

fn main() {
     let args: Vec<String> = env::args().collect();
     let filename = &args[1];
     println!("In file: {}", filename);
     let contents = fs::read_to_string(filename)
        .expect("Something went wrong reading the file");
     let v: Vec<i32> = contents.split("\n").filter_map(|w| w.parse().ok()).map(|x: i32| compute_total_fuel(x)).collect();
     let tot: i32 = v.iter().sum();
     println!("{:?}", tot);
}

The main difference, is that in the map operation, we are now calling the function we defined above on each number, to perform the desired computation, and, in the end, in a functional way, we are summing up all the values to get the answer we need.

Conclusion

We learnt how to define functions in Rust, how to compose them with map operations, how to define mutable variables with the mut keyword, and more importantly:

We program using the rust compiler as our guide. We can shape our coding style to Rust, we can use the compiler warnings and errors as guidance in our learning process, and, coupling that with some online official documentation searches, we have everything we need

Posted on by:

Discussion

markdown guide
 

A good way to close Day 1 of AOC ...looking forward to Day 2.
I am currently reading it and it sounds like much more fun.

 

Also defnitely harder for me :)

I do have some ideas on how to check for the intersections of the wires, but none really satisfy me (and are actually hard to implement)