alt text

We are at the halfway mark of this year’s shortened 12-day Advent of Code. As I wrote three weeks ago, I decided to run an experiment: I abandoned my usual comfortable tool (Python) and my previous “challenge” tool (Rust) to solve everything personal in F#.

In 2023 and 2024, I solved AoC in Rust. I treated it as a software engineering exercise: structured projects, cargo run --example dayXX, and strict memory discipline.

This year, looking at my F# solutions for Days 1-6 alongside my Rust code from last year, the difference in “Dev Experience” is stark.

It comes down to one realization: F# let me skip the “Pre-coding” phase.


The Missing “Pre-Rust” Stage

When I looked back at my process for Rust (AoC 2023-2024), I realized I had a hidden step. Before writing a single line of .rs code, I often had a “Pre-Rust” stage.

I would sketch/run the algorithm in Python, or scribble strict pseudocode on paper, to make sure the logic was sound. Why? Because refactoring in Rust is expensive. If I went down the wrong path in Rust—say, choosing a struct design that ended up fighting the borrow checker—rewriting it was painful. I had to be sure of the what before I committed to the how.

With F# (AoC 2025), that stage is gone. I just… write F#.

Because the syntax is lightweight and the type inference is so strong, I can “sketch” directly in the language. If I decide to change a List to a Seq, or wrap a result in an Option, the refactor is trivial. The code I write to explore the problem often becomes the code that solves the problem.

For example, on Day 3 (2025), I needed a monotonic stack. I didn’t plan it out on paper. I just wrote a recursive function, realized I needed to track “drops allowed,” added an argument, and the compiler instantly aligned everything.


The “Borrow Checker” Tax

Looking back at my Day 11 (2024) solution (Plutonian Pebbles), I implemented a memoized recursion to count stones. Look at the function signature required to make the compiler happy:

1
2
3
4
5
6
// Rust 2024: Day 11
fn count_stones_after_blinks(
  stone: u64,
  blinks_remaining: usize,
  memo: &mut HashMap<(u64, usize), u64>, // <--- This is the "tax"
) -> u64 { ... }

In Rust, I have to pass &mut memo explicitly. I have to think about lifetimes. I have to ensure I’m not borrowing memo immutably elsewhere while I try to mutate it here.

In F# (2025 Day 3), when I needed a stack to solve the “Lobby” problem, I didn’t worry about ownership. I just passed the state forward in the recursion:

1
2
3
4
5
6
7
// F# 2025: Day 3
let rec buildStack input stack dropsLeft =
  match input with
  | [] -> stack
  | d :: rest ->
      let (newStack, newDrops) = pop stack dropsLeft
      buildStack rest (d :: newStack) newDrops

There is no &mut. There is no lifetime annotation. The Garbage Collector handles the memory, so I can focus entirely on the logic.

For systems programming, Rust’s manual memory management might be a superpower. For parsing text files and counting abstract pebbles? It’s just friction.


Tooling: Trust vs. Power

This was a subtle but important surprise. rust-analyzer is widely considered the gold standard of LSP (Language Server Protocol) implementations. It is powerful and fast.

But I occasionally experienced a “correctness gap.”

In Rust, I would sometimes clear all the red squiggles in the IDE, only to have rustc fail when I actually ran the code. rust-analyzer is technically separate from the compiler, and in complex edge cases (e.g., nested closures or weird trait bounds), they can disagree.

This year, I haven’t seen it once. If Ionide says it’s good, it compiles.

Maybe this is because F# is easier to analyze statically (global inference + no borrow checker), or maybe the tooling is just that tightly integrated. But the result is a higher degree of trust. When I see no errors, I know the logic is the only thing left to debug.


Complexity vs. Composition

Comparing my Day 6 (2024) solution to my Day 6 (2025) solution highlights a shift in mindset.

In Rust, I modeled the “Guard Gallivant” problem with classic OOP-style structs and methods:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Rust 2024: Day 6
struct GuardSimulator {
  grid: Grid,
  guard_start_pos: Position,
  guard_start_dir: Direction,
}

impl GuardSimulator {
  fn simulate_patrol(&self) -> HashSet<Position> { ... }
  fn simulate_with_obstruction(&self, pos: Position) -> bool { ... }
}

It’s verbose. It requires defining state containers (GuardSimulator) just to run a loop.

In F# (2025), I found myself using Function Composition to decouple parsing from logic. For the “Trash Compactor” problem (Day 6), I wrote a generic parser that took a strategy function as an argument:

1
2
// F# 2025: Day 6
let parseInput (lines: string array) (extractor: char[][] -> int -> int -> MathProblem)

I didn’t create a GridManager struct. I just passed the logic extractor into the parser. This functional composition is possible in Rust (using Fn traits), but it is unergonomic enough that I rarely reached for it. In F#, it is the default way of thinking.


The Verdict at the Halfway Mark

I am moving faster in F# than I did in Rust, but I am not breaking things like I did in Python.

  • To the Rustacean in me: I miss enum methods and match ergonomics (Rust’s pattern matching is slightly richer).
  • To the Data Scientist in me: I love that I can model a domain in 3 lines of code.

But mostly, I love that I don’t need a “Pre-F#” phase. I can just think in F#.

Six days remain in this year’s calendar. If the second half (likely much more challenging) is as smooth as the first, F# might just become my permanent home for recreational programming.