
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
enummethods andmatchergonomics (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.