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 {
.insert(*n);
diffslet 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)> {
.sort();
numsfor 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 {
-= 1
end } else {
+= 1
mid }
}
}
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)>
nums: &Vec<i32>
: El primer argumento, es una referencia prestada e inmutable. Para propositos de la función podría ser simplemente una variable inmutable pero tuve que hacerla una referencia para que la siguiente llamada de la segunda parte, que va a compartir el mismo vector pero lo va a mutar. Pero para lo que necesita hacer la función esto no es un problema y se puede convertir en una referencia prestada. Tal vez ese sea el tipo más general que eltarget_sum: i32
: Este es la suma objetivo y no necesita ser cambiado a lo largo del programa. Ahora que lo pienso podría haver sido una variable con un lifetime estático pero tal vez eso es demasiado.Option<(i32, i32)>
: El resultado es una posible pareja de números. Si los parámetros de entrada hicieran imposible encontrar una pareja entonces retornariamosNone
.
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 {
io::Error),
IOError(,
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)?;
.push(n);
vec}
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:
AsRef
Path
Lines
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.