< up >
2023-01-03

Hello rust

I finally checked out Rust and solved day one of last year’s advent of code.

tl;dr:

use std::fs::File;
use std::io::{self, prelude::*, BufReader};

// https://stackoverflow.com/a/45882510
fn main() -> io::Result<()> {
    // https://doc.rust-lang.org/std/vec/struct.Vec.html
    let mut calories = Vec::new();
    let mut sum: i32 = 0;

    // https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#propagating-errors
    let file = match File::open("input"){
        Err(e) => return Err(e),
        Ok(file) => file,
    };
    for line in  BufReader::new(file).lines() {
        let data = match line {
            Err(e) => return Err(e),
            Ok(data) => data,
        };

        if data == "" {
            calories.push(sum);
            sum = 0;
            continue
        }
        // https://doc.rust-lang.org/std/primitive.str.html#method.parse
        sum += data.parse::<i32>().unwrap();
    }
    
    calories.push(sum);
    calories.sort();
    
    println!("part1: {}", calories.iter().rev().take(1).sum::<i32>());
    println!("part2: {}", calories.iter().rev().take(3).sum::<i32>());
    
    Ok(())
}

Contents:

Motivation

As a C fanboy, I’m interested in languages that have a similar low level scope. Since 2019, Go is my language of choice where I made myself comfortable.

The claims and news brought me to Rust and today my curiousity about them took over:

Starting over

To dive right into the language, I skipped all tutorials and exampls and started with day 1 of the last year’s advent of code right away. Rust is already installed, so I started over by asking the internet for Rust line by line and Rust max value, and here you are:

use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;
use std::cmp; 

// based on https://doc.rust-lang.org/stable/rust-by-example/std_misc/file/read_lines.html
fn main() {
    let mut max: i32 = 0;
    let mut sum: i32 = 0;
    if let Ok(lines) = read_lines("input") {
        for line in lines {
            if let Ok(data) = line {
                if data == "" {
                    max = cmp::max(max, sum);
                    sum = 0;
                    continue
                }
                sum += data.parse::<i32>().unwrap();
            }
        }
    }

    max = cmp::max(max, sum);
    println!("{}", max);
}

fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>, {
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}

Looking more closely over the copy-pasta code, some constructs are standing out1:

Don’t forget the elephant in the room at the end. The last block seemed too complicated for simply reading a file line-by-line. Let’s address this one first, as it’s the greatest question mark for me.

SO vs. official docs

Well, rather than addressing it, I asked the internet again as the code from the codes looked too optimized. This SO post was more reasonable than the read_lines section from the official documentation:

use std::fs::File;
use std::io::{self, prelude::*, BufReader};
use std::cmp; 

// based on https://doc.rust-lang.org/stable/rust-by-example/std_misc/file/read_lines.html
fn main() -> io::Result<()> {
    let mut max: i32 = 0;
    let mut sum: i32 = 0;
    let file = File::open("input")?;
    let reader = BufReader::new(file);
    for line in reader.lines() {
        if let Ok(data) = line {
            if data == "" {
                max = cmp::max(max, sum);
                sum = 0;
                continue
            }
            sum += data.parse::<i32>().unwrap();
        }
    }
    
    max = cmp::max(max, sum);
    println!("{}", max);

    Ok(())
}

Investigate

Let’s get into all the remaining findings from starting over.

Immutable first

Variables are immutable/constant by default. A classical variables needs to be declared with mut for beeing mutable:

let mut max: i32 = 0;

Error handling

Error handling works by using Results containing the wanted data on success or an error. They can be assumed via Ok and Err, providing the wanted variable if it applies. In case of an error, the condition is false and never enters the branch:

if let Ok(lines) = read_lines("input") {

Alternatively by using ? aka the question mark operator, the error can implicitly be returned from the current function:

let file = File::open("input")?;

This can be converted into a more explicit approach using match where the success and failure case has its own arm. An arm is kind of a lambda <pattern> => <expression>. The first pattern match gets selected and the corresponding expression executed. The Err and Ok do some kind of type-assertion including declaration of variables e and file:

let file = match File::open("input"){
    Err(e) => return Err(e),
    Ok(file) => file,
};

String to int

Converting a string into an integer needs some parsing. The ::<> aka turbofish operator is used to tell parse about the target type:

data.parse::<i32>().unwrap()

Expand the shortcuts

After expanding the error handling shortcuts, the code gets more transparent:

use std::fs::File;
use std::io::{self, prelude::*, BufReader};
use std::cmp; 

// https://stackoverflow.com/a/45882510
fn main() -> io::Result<()> {
    let mut max: i32 = 0;
    let mut sum: i32 = 0;

    // https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#propagating-errors
    let file = match File::open("input"){
        Err(e) => return Err(e),
        Ok(file) => file,
    };
    let reader = BufReader::new(file);
    for line in reader.lines() {
        let data = match line {
            Err(e) => return Err(e),
            Ok(data) => data,
        };

        if data == "" {
            max = cmp::max(max, sum);
            sum = 0;
            continue
        }
        // https://doc.rust-lang.org/std/primitive.str.html#method.parse
        sum += data.parse::<i32>().unwrap();
    }
    
    max = cmp::max(max, sum);
    println!("{}", max);

    Ok(())
}

Reaching for the second star

Finally I used a vector to solve the second aoc part:

use std::fs::File;
use std::io::{self, prelude::*, BufReader};

// https://stackoverflow.com/a/45882510
fn main() -> io::Result<()> {
    // https://doc.rust-lang.org/std/vec/struct.Vec.html
    let mut calories = Vec::new();
    let mut sum: i32 = 0;

    // https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#propagating-errors
    let file = match File::open("input"){
        Err(e) => return Err(e),
        Ok(file) => file,
    };
    for line in  BufReader::new(file).lines() {
        let data = match line {
            Err(e) => return Err(e),
            Ok(data) => data,
        };

        if data == "" {
            calories.push(sum);
            sum = 0;
            continue
        }
        // https://doc.rust-lang.org/std/primitive.str.html#method.parse
        sum += data.parse::<i32>().unwrap();
    }
    
    calories.push(sum);
    calories.sort();
    
    println!("part1: {}", calories.iter().rev().take(1).sum::<i32>());
    println!("part2: {}", calories.iter().rev().take(3).sum::<i32>());
    
    Ok(())
}

While the error handling takes a few lines, summing up the n-th last items of the vector are pretty ruby-alike short:

calories.iter().rev().take(1).sum::<i32>());

Thoughts

It was refreshing to checkout a new language including its resources and toolchain. The error handling with the Ok and Err construct is a mixture of the two error handling concepts I saw in other languages so far: try-catch and error value. Although this looks promising for writing readable and maintainable code, I’m far from judging.

The rustc compiler is colorful and includes some hints which supports my learning-by-doing approach.

Finally I’m hyped and want to learn more about the language. Let’s do some more days!


  1. In context to Golang and C.