Day 1:

student-log
rust
advent-of-code
learning
Getting Santa’s steps in
Author

The MidWit

Published

May 29, 2026

Note

You can find the text of the puzzle at here. I’ve put some fake input that should give me a result of -1 so when you see output ending in -1, that means my implementation is working.

Overview

The aim of this puzzle is to take in a long string of text, somewhat awkwardly, composed of “(” and “)”, and to iterate a counter up or down respectively until we come to a final floor (where Santa ends up after following the instructions).

Concepts

  • File IO
  • Iteration
  • control flow
  • mutability
  • wrapped value (Result)

Initial thoughts

So in python-land I would read in the input string and iterate over the characters with a for loop, checking each char and incrementing/decrementing a counter to get the final number… so that’s where I started.

Part 1

Reading in a text file.

So rather than using context manager to read the text and bind it to a variable, Rust has a function in the standard library: use std::fs::read_to_string;.

#| label: file-io-1

use std::fs::read_to_string;// bring the function into scope

fn main () {
    let input = read_to_string("./input.txt");// read the input
    println!("{}", input)//
}

This was my first hurdle

$ cargo rust
  Compiling day_1 v0.1.0 (/path/to/day_1/main)
error[E0277]: `Result<String, std::io::Error>` doesn't implement `std::fmt::Display`
 --> src/main.rs:5:20
  |
5 |     println!("{}", input)//
  |               --   ^^^^^ `Result<String, std::io::Error>` cannot be formatted with the default formatter
  |               |
  |               required by this formatting parameter
  |
  = help: the trait `std::fmt::Display` is not implemented for `Result<String, std::io::Error>`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

For more information about this error, try `rustc --explain E0277`.
error: could not compile `day_1` (bin "day_1") due to 1 previous error

read_to_string doesn’t just return a String that I can manipulate directly (of course!) it returns a Result<String>, and that creates all sorts of… opportunities for learning.

  • The value I want is inside a Result rather than directly available, so I gotta deal with that
  • The Result doesn’t have the Display Trait so I need to put one of the formatting bugs inside the “{}

1 of those is easy to deal with:

#| label: file-io-2
use std::fs::read_to_string;// bring the function into scope

fn main () {
    let input = read_to_string("./input.txt");// read the input
    println!("{:?}", input)// put the debug syntax into the println! macro
}

which gives me

$cargo run
   Compiling day_1 v0.1.0 (path/to/day_1)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/day_1`
Ok("()(((()()))()((()))())()())\n")

So we can see that the output shows us that my input is wrapped in Ok(), which is one of the variants of the Result enum. Which in some ways is fine, but I want the actual feed of characters, so I have a few options.

  • .unwrap() which will either give me the string directly or Panic
  • .expect() which is basically the same, but I can put an error message in to help me.
  • Properly match on the Result and either get the content or handle the error.
#| label: file-io-expect
use std::fs::read_to_string;// bring the function into scope

fn main () {
    let input = read_to_string("./input.txt");// read the input
    println!("{}", input.expect("no text here my dude"))// trying expect
}
$cargo run
   Compiling day_1 v0.1.0 (path/to/day_1)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/day_1`
()(((()()))()((()))())()())

So that did work, but everything I’ve read says that both unwrap and expect are great tools for rapid iteration without getting bogged down, but that they shouldn’t be used in production code… and yes I’m not writing production code, but if I’m trying to learn more than just the basics, I should probably aim in that direction.

match

A match expression is a way to check the state of an object and control what happens based on what’s there. It’s very similar to the if/else if/else concept, but it can be a lot more ergonomic depending on what you’re checking, and enums are a great usecase for it. Python actually has a similar concept also called a match expression but I hadn’t come across that in my own work until recently so I would have relied on if/elif/else in previous work. I’m trying to practice “idiomatic” rust patterns here so lets match:

#| label: file-io-match
use std::fs::read_to_string;// bring the function into scope

fn main () {
    let input = read_to_string("./input.txt");// read the input
    match input {
        Ok(s) => println!("{}", s),// match unpacks the value for us
        Err(e) => println!("We got an error {}", e),// Printing a custom message and the Error
    }
}

OK so that gives us the actual input string as something I can use, so let me move on to actually tracking floors.

One step up N steps down

So what were trying to do is, starting at 0 (the ground floor) find the floor number that Santa ends up on after taking all the steps indicated in the input: “(” = up 1, “)” = down 1. Rust will let us iterate over the characters in a String by calling .chars(). This gives us back a collection we can iterate over.

Coming from python, my instinct is to reach for a loop and if/else

#| label: immutable-loop
// assume the main function and prelude

let floor = 0;
for c in input.chars() {
    if c == "(" {
        floor += 1 
    } else if c == ")" {
        floor -= 1 
    } else {
        floor
    }
};
println!("{}", floor);

Which brings me to a (drum roll please) RUST ROOKIE MISTAKE : mutability.

  --> src/main.rs:10:21
   |
 7 |             let floor = 0;
   |                 ----- first assignment to `floor`
...
10 |                     floor += 1
   |                     ^^^^^^^^^^ cannot assign twice to immutable variable
   |
help: consider making this binding mutable
   |
 7 |             let mut floor = 0;
   |                 +++

error[E0384]: cannot assign twice to immutable variable `floor`
  --> src/main.rs:12:21
   |
 7 |             let floor = 0;
   |                 ----- first assignment to `floor`
...
12 |                     floor -= 1
   |                     ^^^^^^^^^^ cannot assign twice to immutable variable
   |
help: consider making this binding mutable
   |
 7 |             let mut floor = 0;
   |                 +++

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

Rust, cause it knows I’m a midwit, makes things immutable by default, meaning that once I bind a value to a variable I can’t change it… unless (“unless”) I specifically say that that value is mutable. There’s lots of really good reasons for this, but if you’re reading this post and you haven’t already seen those… then… I mean… welcome, I guess.

Anyway, fortunately this one is easy to fix:

#| label: mutable-loop
// assume the main function and prelude

let mut floor = 0;
for c in input.chars() {
    if c == "(" {
        floor += 1 
    } else if c == ")" {
        floor -= 1 
    } else {
        floor
    }
};
println!("{}", floor);

And… that gave me back the correct answer (for part 1 at least). Boom. I’ve written more than hello world. And yet…

Closures

While the for loop is legitimate rust, an this solution works, it’s not really, mutability and wrapping aside, that different from how I would solve a short problem like this in python, which ain’t really my goal here. So I guess I better try tackling this with a closure.

My main problem with closures isn’t the concept so much as the number of possible options, and there’s no way to learn these except to use them enough times. I really need to write a cheat sheet for them that I can look up later but for now, I’m going to ask my buddy Claude to recommend one1, or more accurately to recommend an iterator adapter which will accept a closure and let me do the thing.

Fold

So again, let’s think about what the puzzle wants:

Starting at 0, iterate over a collection of chars incrementing or decrementing a number (accumulating), producing a count of the number of up or down steps we’ve taken. This looks like a job for .fold().

#| label: closure-fold
use std::fs::read_to_string;// bring the function into scope

fn main () {
    let input = read_to_string("./input.txt");// read the input
    match input { // match on input
        Ok(s) => { // when input is Ok()
        let result = s.chars().fold( // bind the product of the closure to result
                // 0, is the starting number
                // |acc, is the variable name for 0 
                // c| is the variable name for each char in input for each loop
                0, |acc, c| match c { // matching on c
                '(' => acc + 1, // incrementing acc
                ')' => acc -1, // decrementing acc
                _ => acc // using the wildcard so that the match is exhaustive
            });
        println!("{}",result); //printing the result
        },
        Err(e) => println!("We got an error {}", e),// Printing a custom message and the Error
    }
}
#| label: closure-fold-output
$cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/day_1`
-1

So .fold() lets me set a starting value (in the above case it’s 0), then bind that value to acc (standing for accumulator) and then bind the current char to c while looping over input, accumulating a count of steps for each c.

As someone who’s not a programmer, and who writes pretty quick and dirty python, I have to say I really like the closure approach. I can’t really say why but it feels like it reduced some typing for me (for c in chars{ if/else}), but some of that is also just the match implementation as well.

Refactoring

One of the things that I find challenging is reading deeply nested code, or it’s probably more accurate to say that I find it really hard to debug deeply nested code. My imposter-syndrome self likes to tell me that real coders don’t have a problem with this, but my better angels remind me that: - I don’t want to be a real coder so I should just be happy for those that don’t have this problem, and - There’s probably loads of really talented engineers who also struggle with this.

All this to say that I prefer when the code is split up into smaller blocks 2 that I can reason about individually. While this isn’t exactly deeply nested, it is a few layers deep between the 2 match blocks, and so now might be a good time to think about pulling some of this apart so I can put it back together. Especially given that there is a part two to this puzzle, so re-usability might be nice to have…

Taking steps.

The first clear thing I can do is pull the ‘char’ matching out into it’s own function:

Take a char and an i32 and then give back an updated i32

#| label: take-step-definition

//take step function
//this just does what I was doing in the fold but in a function that I can
//use in the .fold() closure
fn take_step(c: char, value: i32) -> i32 {
    match c {
        '(' => value + 1,
        ')' => value -1, 
        _ => value
    }
}

Ok, that might not be the most perfect rust function but it does what I want it to do. I should probably make it return a Result and write some proper Error handling, but baby steps here (this is already much too long).

So now my solution is

#| label: function-fold-1

fn main () {
    let input = read_to_string("./input.txt");// read the input
    match input {
        Ok(s) => {
        let result = s.chars().fold(
                0, |acc, c| take_step(c, acc)
            );
        println!("{}",result);
        },
        Err(e) => println!("We got an error {}", e),// Printing a custom message and the Error
    }
}

This compiles so the refactor works (boom) but also I find it easier to see what’s going on inside the .fold() (second boom).

This also prompted me to think about handling errors and other ways I could refactor to get rid of other nesting in my main function. Specifically I could re-work the file I/O to handle errors and let me drop that outer match expression but I’m going to call that my first sidequest in which our hero write a module to handle reading challenge input that I can use in all the other day’s of AOC. But for now let’s move onto part 2.

Part 2: Don’t go down there! It’s dark..

So for the second part, I have to find the first char that causes Santa to enter the basement (brings the counter below 0).

In python land I would use some kind of while loop here and enumerate over the input string; but I’d like to see if there is a way to do this with a closure?

I know that the .enumerate() method will give me back both the index and the values in a collection so adding that into my adapter chain is a good start, but .fold() isn’t really the move here because we want to stop at a particular point, not take the whole collection.

A little searching tells me that .find() might do what I want, and Claude suggested .scan(). I’m going to try .find() first and see what happens.

#| label: find-1

use std::fs::read_to_string;// bring the function into scope

fn take_step(c: char, value: i32) -> i32 {
// -- snip --
}

fn main () {
    let input = read_to_string("./input.txt");// read the input
    match input {
        Ok(s) => {
// -- snip --
        let floor = 0;
        let basement = s.chars()
            .enumerate()
            .find(|(i,c)|
                take_step(c, floor) == -1
                );

        },
// -- snip --
    }
}

This was my first attempt and it was really just an attempt to see what I get back from .find(). Here’s what happened.

#| label: find-1-output
  --> src/main.rs:22:27
   |
22 |                 take_step(c, floor) == -1
   |                 --------- ^ expected `char`, found `&char`
   |                 |
   |                 arguments to this function are incorrect
   |
note: function defined here
  --> src/main.rs:3:4
   |
 3 | fn take_step(c: char, value: i32) -> i32 {
   |    ^^^^^^^^^ -------
help: consider dereferencing the borrow
   |
22 |                 take_step(*c, floor) == -1
   |                           +

Ok, so what this is telling me is that c is getting passed into the function as a borrow rather than the actual char and so I need to deref. I’m not going to pretend I fully understand why right now, but I do know that I just need to do what the compiler tells me to fix it. And having done so we can see some output.

#| label: find-1-output-2
warning: unused variable: `i`
  --> src/main.rs:21:21
   |
21 |             .find(|(i,c)|
   |                     ^ help: if this is intentional, prefix it with an underscore: `_i`
   |
   = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default

warning: `day_1` (bin "day_1") generated 1 warning (run `cargo fix --bin "day_1" -p day_1` to apply 1 suggestion)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/day_1`
-1
Some((1, ')'))

For now I’m going to ignore the unused_variables warning and focus on the fact that .find() is returning an Option<(i,c)> which is good news, if a little confusing to read. I’m not sure that this value is right though, so I’m going to do a little println! debugging to see what’s happening.

#| label: find-1-println
use std::fs::read_to_string;// bring the function into scope

fn take_step(c: char, value: i32) -> i32 {
// -- snip --
}

fn main () {
    let input = read_to_string("./input.txt");// read the input
    match input {
        Ok(s) => {
// -- snip --
        let floor = 0;
        let basement = s.chars()
            .enumerate()
            .find(|(i,c)| {
                println!("floor = {}", floor);
                println!("index = {}", i);
                println!("char = {}", c);
                take_step(*c, floor) == -1
            });
// -- snip --
    }
}

Which gives me

#| label: find-1-println-output
// --snip --
floor = 0
index = 0
char = (
floor = 0
index = 1
char = )
Some((1, ')'))

I know, you saw the problem already and have been shouting at me for the last few paragraphs, but… “MidWit” remember?

So floor isn’t getting updated at any point which means while I’m actually getting a technically correct answer (which is the best kind of correct) it’s not in the spirit of the puzzle. Really I’m just finding the first character that subtracts 1 from 0… let’s see if I can fix it.

#| label: find-1-update
use std::fs::read_to_string;// bring the function into scope

fn take_step(c: char, value: i32) -> i32 {
// -- snip --
}

fn main () {
    let input = read_to_string("./input.txt");// read the input
    match input {
        Ok(s) => {
// -- snip --
        let floor = 0;
        let basement = s.chars()
            .enumerate()
            .find(|(i,c)| {
                println!("floor = {}", floor);
                println!("index = {}", i);
                println!("char = {}", c);
                floor += take_step(*c, floor); // Updating floor here
                floor == -1
            });
// -- snip --
    }
}
#| label: find-1-update-output
// -- snip --

floor = 2001801
index = 22
char = (
floor = 4003603
index = 23
char = )
floor = 8007205
index = 24
char = (
floor = 16014411
index = 25
char = )
floor = 32028821
index = 26
char = )
floor = 64057641
index = 27
char =

None

Sad Trombone

Ok, so, again you probably saw what I was doing wrong here: I’m not just taking a step each round. I’m adding floor to itself and then the take_step value. So find() doesn’t easily keep track of the value we’re trying to update well. While I could spend ages thinking about how to manage this, it might be time to see if .scan(), which is more similar to .fold(), can do for us.

Scan

Based on the docs, .scan() gives back another iterator and so that will need processing as well to get the actual point at which Santa enters the basement for the first time. I’m going to try to build this chain piece by piece starting with the scan.

#| label: scan-1-start
// -- snip --

fn main () {
    let input = read_to_string("./input.txt");// read the input
    match input {
        Ok(s) => {
// -- snip --
        let basement = s.chars()
            .scan(0, |acc, c| {
                take_step(c, *acc);
                Some(*acc)
            }
            
            );
        println!("{:?}", basement);
#| label: scan-1-start-output
// -- snip --
Scan { iter: Chars(['(', ')', '(', '(', '(', '(', ')', '(', ')', ')', ')', '(', ')', '(', '(', '(', ')', ')', ')', '(', ')', ')',
 '(', ')', '(', ')', ')', '\n']), state: 0 }

Great, we have a Scan object which is a collection of chars and a state… great… I * definitely know what to do with that… def’nitly.

What do I know that can help me figure this out?

  • A lot of closures are ‘lazy’ meaning that I would need to .collect() or otherwise process it.
  • I still need to get a particular index, so .enumerate() is still going to be needed.
  • Speaking of which, .find() is probably going to come into it’s own here.
  • Claude recommended .scan() so I might not be on the right track with .scan() either, but that’s probably not as helpful to think about right now.

Let me start by seeing what happens when I .collect() the .scan().

#| label: scan-1-collect-output
error[E0283]: type annotations needed
  --> src/main.rs:25:35
   |
25 |         println!("{:?}", basement.collect());
   |                                   ^^^^^^^ cannot infer type of the type parameter `B` declared on the method `collect`
   |
   = note: cannot satisfy `_: FromIterator<i32>`
note: required by a bound in `collect`
  --> /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/iter/traits/iterator.rs:2051:4
help: consider specifying the generic argument
   |
25 |         println!("{:?}", basement.collect::<Vec<_>>());
   |                                          ++++++++++

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

Say it with me folks!! Rust. Rookie. Mistake! The compiler needs to know what I’m trying to make so the code needs a turbofish 3 where the type is specified.

#| label: scan-1-turbofish
// -- snip --
    
        let basement = s.chars()
            .scan(0, |acc, c| {
                take_step(c, *acc);
                Some(*acc)
            }
            
            );
        println!("{:?}", basement.collect::<Vec<_>>());
// -- snip --

That gives me back a vector of 0s… because of course it does. I’m not sure what that means yet either, but I’m gonna keep going. We know that we’re looking for an index as I said, so my next thought is to add .enumerate() to the chain, as this will give me a Vec<(index, state)> (I think).

#| label: scan-1-enumerate
// -- snip --
    
        let basement = s.chars()
            .scan(0, |acc, c| {
                take_step(c, *acc);
                Some(*acc)
            }
            
            )
            .enumerate();
        println!("{:?}", basement.collect::<Vec<_>>());
// -- snip --
#| label: scan-1-enumerate-output
// -- snip --
[(0, 0), (1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0), (7, 0), (8, 0), (9, 0), (10, 0), (11, 0), (12, 0), (13, 0), (14, 0), (15
, 0), (16, 0), (17, 0), (18, 0), (19, 0), (20, 0), (21, 0), (22, 0), (23, 0), (24, 0), (25, 0), (26, 0), (27, 0)]
// -- snip --

Yes! Well… half yes. I’m getting the tuple I expect (I think), but the fact that the second item is always 0 leads me to believe that something is going wrong in the way .scan() is interacting with my take_step function. Rather than move onto adding the .find() part of the chain I’m going to unpack the take_step functionality and do it explicitly inside the closure

#| label: scan-1-explicit
// -- snip --
        let basement = s.chars()
            .scan(0, |acc, c| {
                acc += match c {
                    '(' => acc + 1,
                    ')' => acc -1, 
                    _ => acc
                };
                Some(*acc)
            })
            .enumerate();
        println!("{:?}", basement.collect::<Vec<_>>());
// -- snip --

Oh boy, this next one is a doozy

#| label: scan-1-explicit-output
// -- snip --
  --> src/main.rs:21:32
   |
21 |                     '(' => acc + 1,
   |                            --- ^ - {integer}
   |                            |
   |                            &mut {integer}
   |
   = note: an implementation for `&{integer} + {integer}` exists
help: consider reborrowing this side
   |
21 |                     '(' => &*acc + 1,
   |                            ++

error[E0369]: cannot subtract `{integer}` from `&mut {integer}`
  --> src/main.rs:22:32
   |
22 |                     ')' => acc -1,
   |                            --- ^- {integer}
   |                            |
   |                            &mut {integer}
   |
   = note: an implementation for `&{integer} - {integer}` exists
help: consider reborrowing this side
   |
22 |                     ')' => &*acc -1,
   |                            ++
// -- snip --

OK, so, again, I’m not sure if this is accurately diagnosing what the issue was earlier, but it is showing me that acc is mutably borrowed inside the closure, so if I’m understanding correctly then it means I need to deref acc before the match, also if I leave acc in the match arms I’ll end up with the same problem I had with the earlier find: doubling.

#| label: scan-1-deref
// -- snip --
        let basement = s.chars()
            .scan(0, |acc, c| {
                *acc += match c {
                    '(' => 1,
                    ')' => -1, 
                    _ => 0
                };
                Some(*acc)
            })
            .enumerate();
        println!("{:?}", basement.collect::<Vec<_>>());
// -- snip --

We have a third boom.

#| label: scan-1-deref-output
// -- snip --
[(0, 1), (1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 3), (7, 4), (8, 3), (9, 2), (10, 1), (11, 2), (12, 1), (13, 2), (14, 3), (15, 4), (16, 3), (17, 2), (18, 1), (19, 2), (20, 1), (21, 0), (22, 1), (23, 0), (24, 1), (25, 0), (26, -1), (27, -1)]
// -- snip --

So the state is now updating as we run through it, meaning that now I just need to .find() the first time we get a -1 in the state, which we can see is at index 26 in this output.

What we have now is the vec of (i, state) that we need to pass into .find(), and because .find() takes only one parameter I need to pass them in in that format rather than as separate arguments.

#| label: scan-1-find
// -- snip --
        let basement = s.chars()
            .scan(0, |acc, c| {
                *acc += match c {
                    '(' => 1,
                    ')' => -1, 
                    _ => 0
                };
                Some(*acc)
            })
            .enumerate()
            .find(|(i, state)| *state == -1);
        println!("{:?}", basement.collect::<Vec<_>>());
// -- snip --
#| label: scan-1-find-output
// -- snip --
  --> src/main.rs:29:35
   |
29 |         println!("{:?}", basement.collect::<Vec<_>>());
   |                                   ^^^^^^^ method cannot be called on `Option<(usize, {integer})>` due to unsatisfied trait b
ounds
   |
   = note: the following trait bounds were not satisfied:
           `Option<(usize, {integer})>: Iterator`
           which is required by `&mut Option<(usize, {integer})>: Iterator`

For more information about this error, try `rustc --explain E0599`.
// -- snip --

Ok, so .find() returns an Option rather than a collection that needs to be .collect()ed so I need to drop that bit, so long turbofish!

#| label: scan-find-println
// -- snip --
    println!("{}", basement);
// -- snip --

Ladies and Jellyspoons, we have reached the 4th boom.

#| label: scan-find-println-output
// -- snip --
warning: unused variable: `i`
  --> src/main.rs:28:21
   |
28 |             .find(|(i, state)| *state == -1);
   |                     ^ help: if this is intentional, prefix it with an underscore: `_i`
   |
   = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default

warning: `day_1` (bin "day_1") generated 1 warning (run `cargo fix --bin "day_1" -p day_1` to apply 1 suggestion)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/day_1`
-1
Some((26, -1))
// -- snip --

OK, so we have the Some(tuple) that shows us the index and the state we want. So functionally we have the output we want. The eagle eyed amongst you will have spotted that the last char in input is '\n' and so the wildcard in the match is doing what we want as well and leaving acc as it is.

Thus we have out answer to both parts!… except…

As I said up above I’m trying to write towards production code, and while I have my answer, the output is ugly as fu… it doesn’t look great. So, it might be worth it to try to make the output prettier.

Prettifying

This is one of the places .unwrap() may work well because there is a Some that we just want to get the value out of.

#| label: scan-unwrap
// -- snip --
        let basement = s.chars()
            .scan(0, |acc, c| {
                *acc += match c {
                    '(' => 1,
                    ')' => -1, 
                    _ => 0
                };
                Some(*acc)
            })
            .enumerate()
            .find(|(i, state)| *state == -1)
            .unwrap();
// -- snip --

Which gives us back the tuple (26,-1). Since we just want the zeroth item in the tuple we could just destructure it with . syntax as well to just get the 26 back in out output. However, I want to deal with that unused_variables warning now and I think that .map() might let me do that. .map() will let me create a new iterator and apply a new closure to it in line, letting me destructure the tuple I get back from .find().

#| label: pretty-map
// -- snip --
            .enumerate()
            .find(|(i, state)| *state == -1)
            .map(|(i, state)| i as i32)
            .unwrap();
// -- snip --
#| label: pretty-map-output
// -- snip --
warning: unused variable: `state`
  --> src/main.rs:29:23
   |
29 |             .map(|(i, state)| i as i32)
   |                       ^^^^^ help: if this is intentional, prefix it with an underscore: `_state`
   |
   = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default

warning: unused variable: `i`
  --> src/main.rs:28:21
   |
28 |             .find(|(i, state)| *state == -1)
   |                     ^ help: if this is intentional, prefix it with an underscore: `_i`

warning: `day_1` (bin "day_1") generated 2 warnings (run `cargo fix --bin "day_1" -p day_1` to apply 2 suggestions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
     Running \`target/debug/day_1\`
-1
26
// -- snip --

OK, so the compiler is telling me that if I want to get rid of the unused_variables warning I just need to prefix the variable names with “_” and so let me do that one last time and check the output.

Final implementation

#| label: final-implementation
use std::fs::read_to_string;// bring the function into scope

fn take_step(c: char, value: i32) -> i32 {
    match c {
        '(' => value + 1,
        ')' => value -1, 
        _ => value
    }
}
fn main () {
    let input = read_to_string("./input.txt");// read the input
    match input {
        Ok(s) => {
        let result = s.chars().fold(
                0, |acc, c| take_step(c, acc)
            );
        println!("We're on floot {}",result);
        let basement = s.chars()
            .scan(0, |acc, c| {
                *acc += match c {
                    '(' => 1,
                    ')' => -1, 
                    _ => 0
                };
                Some(*acc)
            })
            .enumerate()
            .find(|(_i, state)| *state == -1)
            .map(|(i, _state)| i as i32)
            .unwrap();
        println!("The first time we hit the basement is {}", basement);

        },
        Err(e) => println!("We got an error {}", e),// Printing a custom message and the Error
    }
}
#| label: final-output
$cargo run
   Compiling day_1 v0.1.0 (path/to/day_1)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/day_1`
We're on floor -1
The first time we hit the basement is 26

Ultimate BOOM!.

We have a solution.

It’s definitely not perfect and it spawned at least one sidequest but as a fledgling rust writer I think I can be pretty happy with it.

What do I need to take from this?

  • Closures are great, but there’s a lot of adaptors and, frankly the documentation isn’t that easy to parse.
    • .find() lets us look for a condition
    • .fold() lets us accumulate a value over a collection (at least as far as I understand it)
    • .scan() is like .fold(), except it gives back a collection and a state item that we can track
  • I need to learn more about referencing and dereferencing, there is a principle there around how something gets passed into a closure, but it hasn’t clicked for me yet.
  • The compiler is a good ally, especially if the issue is rudimentary.
  • Walking through the problem is a good idea, even if it’s after the fact.
    • I wrote this up after I had a solved, but I was taking checkpoints as I went so that I could try to consolidate things.
    • I definitely used Claude to help me parse the errors, and as I said above to suggest adaptors that might be useful to me. It would often tell me to just reach for a for loop though and I’d have to do a lot to get it to stick with my learning goals. Working with an LLM definitely saved time, but when my aim is to learn, not to meet KPIs as a developer this isn’t ideal. I have a ‘Rust-coach’ skill which often works well, but I’ve found that it’s tough to keep it from being too helpful and it’s better for me to just not use one when I’m tired: too tempting to just get an answer, which isn’t the same as learning a thing.
  • This write up took way too long. In future it would be better to hit highlights better, but then I don’t really know what qualifies as a highlight this early in the show.
  • Refactoring is good even if it doesn’t actually get reused.
    • I certainly could have updated take_step() to accomodated Part 2, but as a learning excercise it was worth doing even though it just contributed to Part 1.

Anyway, that’s too much for a single post, espeshally a first’un.

God’s speed.

The MidWit

Footnotes

  1. I really don’t like using LLMs for learning, for too many reasons to go into here, but honestly, as a docs reader I do think they are great, and I’m not above saving myself days of reading through docs or reddit to get an answer. If you think I got Claude to give me the whole solution, then you probably think I got Claude to write this whole post, so there’s not really a lot I can do about that. We’re all just tryin’ to figure this out I suppose.↩︎

  2. I’m not qualified to say that this is ‘right’ or ‘wrong’ I’m saying that my brain and eyes prefer it. Maybe it is better to wire everything together in longer chunks, but I’m > 40 years old and I’m trying to learn what I’m trying to learn, so… get off my back!↩︎

  3. If you think for one minute that the fact that it’s called a turbofish isn’t a big reason why I’m trying to learn rust, you’re out of your mind.↩︎