Miguel Vilá

Advent Of Code 2020 en Rust. Día 1

Dec 07 2020

Estoy intentando resolver problemas del Advent of Code de este año usando Rust y así aprender el lenguaje. La verdad creo que hace 4 años había intentado aprenderlo pero la verdad no recuerdo mucho. Recuerdo no haber entendido por completo el sistema de borrowing y otro montón de cosas. Pero esta vez quiero usar los ejercicios de Advent of Code como excusa para aprender.

El problema del primer día consistía en, dada una lista de números, encontrar un par de ellos que sumados den 2020 y multiplicarlos. La segunda parte era hacer lo mismo pero encontrando 3 de ellos. Mi solución fue:

use std::collections::HashSet;
mod file_reading;

fn find_pair(nums: &Vec<i32>, target_sum: i32) -> Option<(i32, i32)> {
    let mut diffs: HashSet<i32> = HashSet::new();
    for n in nums {
        diffs.insert(*n);
        let diff: i32 = target_sum - n;
        if diffs.contains(&diff) {
            return Some((diff, *n));
        };
    }
    None
}

fn find_three(mut nums: Vec<i32>, target_sum: i32) -> Option<(i32, i32, i32)> {
    nums.sort();
    for start in 0..(nums.len() - 2) {
        let mut mid = start + 1;
        let mut end = nums.len() - 1;
        while mid < end {
            let total = nums[start] + nums[mid] + nums[end];
            if total == target_sum {
                return Some((nums[start], nums[mid], nums[end]));
            } else if total > target_sum {
                end -= 1
            } else {
                mid += 1
            }
        }
    }
    None
}

fn main() {
    if let Ok(numbers) = file_reading::read_numbers("./input1.txt") {
        let target_sum = 2020;
        if let Some((p1, p2)) = find_pair(&numbers, target_sum) {
            let product = p1 * p2;
            println!("Part 1: {}", product);
        }
        if let Some((p1, p2, p3)) = find_three(numbers, target_sum) {
            let product = p1 * p2 * p3;
            println!("Part 2: {}", product);
        }
    }
}

La solución de la primera parte funciona con un Hashset que almacena los números vistos y permite preguntar rápido si ya vimos el otro número que sumado al actual permite sumar 2020. La segunda parte funciona de otra forma. Primero ordena los números y después hace una busqueda anidada, pero un poco inteligente.

Pero mi objetivo no es hablar tanto de los algoritmos sino de Rust. En su mayoría tuve problemas poniendo los tipos y entendiendo los errores de compilación.

Por ejemplo, para la primera parte:

fn find_pair(nums: &Vec<i32>, target_sum: i32) -> Option<(i32, i32)>

La segunda parte:

fn find_three(mut nums: Vec<i32>, target_sum: i32) -> Option<(i32, i32, i32)>

Dado que necesito ordenar los números hice la variable mutable. Los otros tipos tienen una justificación similar a la primera parte.

Por último creo que vale la pena hablar del módulo que escribí para leer input de archivos:

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

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

#[derive(Debug)]
pub enum ReadingError {
    IOError(io::Error),
    ParsingError(ParseIntError),
}

pub fn read_numbers<P: AsRef<Path>>(filename: P) -> Result<Vec<i32>, ReadingError> {
    let mut vec = Vec::new();
    let lines = read_lines(filename).map_err(ReadingError::IOError)?;
    for line_it in lines {
        let line = line_it.map_err(ReadingError::IOError)?;
        let n: i32 = line.parse().map_err(ReadingError::ParsingError)?;
        vec.push(n);
    }
    Ok(vec)
}

Siendo honesto la primera función fue copiada de Stackoverflow y no entiendo la firma por completo. Sé que está haciendo uso de un generic y una restrición de trait pero los tipos que no entiendo son:

Sin embargo tuve que implementar la otra función para leer un vector de números a partir de un archivo de entrada. Creo que hice un poco de manejo paranoíco de errores (no es como si alguno de esos errores fuera a suceder intentando resolver estos problemas), pero quería aprender a manejar y unificar errores en Rust. Podría haber resuelto el problema usando .unwrap pero esa no era la idea.

Lo más curioso que puedo notar es el operador ?, que sirve para “extraer” el valor de un Result. Cualquiera que venga de un lenguaje funcional reconece que esto es muy similar a como funcionan las monadas. Curiosamente creo que go-lang es famoso por tener un problema similar pero, hasta donde sé, no tienen alguna ayuda sintáctica como Rust.

Conclusión

Eso es todo, creo que tengo que entender mucho mejor algunas cosas. El sistema de borrowing se me hace un poco complicado de interiorizar y desafortunadamente estoy acostumbrado a simplemente no pensar en eso. Es bueno entender cuál es la firma más general que necesita una función, y por qué. Por ejemplo si requiere borrowing o no, si es una referencia a una variable mutable o no, o si simplemente es una variable mutable. Hay cosas que me gustan, por ejemplo las cosas semi-funcionales como Result o ? o los enums.