Advent of Code: A Learning Journey in F# Day 2
Explore the second day of Advent of Code challenges using F#. This article delves into functional programming techniques to solve algorithmic puzzles.
4 décembre 2024
Published
Hugo Mufraggi
Author

A Learning Journey In F # Day 2
As our Advent of Code adventure continues, the challenge of Day 2 beckons, offering another opportunity to dive deeper into the functional programming paradigm with F#. Each puzzle is a new landscape of computational problem-solving waiting to be explored and conquered.
Following the excitement of Day 1, where we navigated list manipulations and algorithmic thinking, today’s challenge brings a fresh set of puzzles to test our growing skills in functional programming. As a developer still learning the intricacies of F#, I see each problem as a stepping stone — not just a coding exercise but a chance to expand my understanding and push the boundaries of my programming capabilities.
My journey remains unchanged: solve the problem, document the process, and share the insights gained along the way. This isn’t just about finding a solution; it’s about the learning process — the thought patterns, the challenges overcome, and the incremental growth that comes with each line of code.
Join me as we unravel another algorithmic mystery, transforming complexity into elegant functional solutions, one problem at a time!
Day 1
You can retrieve the subject of Day 1 here.
Day 2
Part 1
Subject
7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9
This example data contains six reports, each consisting of five levels.
The engineers are trying to figure out which reports are safe. The Red-Nosed reactor safety systems can only tolerate levels that are either gradually increasing or gradually decreasing. A report is considered safe if both of the following conditions are true:
The levels are either all increasing or all decreasing.
Any two adjacent levels differ by at least 1 and at most 3.
For example:
7 6 4 2 1: Safe because the levels are all decreasing by 1 or 2.
1 2 7 8 9: Unsafe because 2 → 7 is an increase of 5.
9 7 6 2 1: Unsafe because 6 → 2 is a decrease of 4.
1 3 2 4 5: Unsafe because 1 → 3 is increasing but 3 → 2 is decreasing.
8 6 4 4 1: Unsafe because 4 → 4 is neither an increase nor a decrease.
1 3 6 7 9: Safe because the levels are all increasing by 1, 2, or 3.
In this example, 2 reports are safe.
Implementation
The input file contains a list of numbers separated by spaces. Here’s the process:
- Read the input file.
- Split each line by spaces and convert the values into integers.
- Create three functions for the checks.
- Count the number of reports that pass all checks.
Functions: isSup and isInf
To determine if the levels are all increasing (isSup) or all decreasing (isInf), we use Array.windowed 2, which creates pairs of consecutive elements.
let isSup (line: int array) =
line
|> Array.windowed 2
|> Array.map (fun [|a; b|] -> a < b)
|> Array.forall id
let isInf (line: int array) =
line
|> Array.windowed 2
|> Array.map (fun [|a; b|] -> a > b)
|> Array.forall id
This function checks if the differences between adjacent levels are valid (1, 2, or 3):
let isSafe (line: int array) =
line
|> Array.windowed 2
|> Array.map (fun [|a; b|] -> abs (a - b) >= 1 && abs (a - b) <= 3)
|> Array.forall id
Line Validation
Using pattern matching, we combine the three checks:
let checkLine (isInf: bool) (isSup: bool) (isSafe: bool) =
match (isInf, isSup, isSafe) with
| (true, false, true) -> true
| (false, true, true) -> true
| _ -> false
We apply these checks to each line of the input:
let checkValidity (lines: int array array) =
lines
|> Array.map (fun line -> checkLine (isInf line) (isSup line) (isSafe line))
Finally, the main function computes the result:
[<EntryPoint>]
let main argv =
let res =
@"/path/to/input.txt"
|> readFile
|> clean
|> checkValidity
|> Array.filter id
|> Array.length
printfn "Number of safe reports: %d" res
0
Part 2
Updated Subject
Now, engineers can remove a single level from a report to make it safe. The goal is to determine how many reports are safe with this adjustment.
New Function: tryRemoveSingleLevel
This Function tests the removal of one level at a time:
let tryRemoveSingleLevel (line: int array) =
[0..line.Length - 1]
|> List.exists (fun removeIndex ->
let modifiedLine =
line
|> Array.indexed
|> Array.filter (fun (i, _) -> i <> removeIndex)
|> Array.map snd
(isSup modifiedLine || isInf modifiedLine) && isSafe modifiedLine
)
Adjusted Validation
We incorporate the ability to retry after removing one level:
let checkLineDampener (line: int array) =
let originalSafe = (isSup line || isInf line) && isSafe line
if originalSafe then true else tryRemoveSingleLevel line
The main function for Part 2:
let resPart2 =
@"/path/to/input.txt"
|> readFile
|> clean
|> checkValidityDampener
|> Array.filter id
|> Array.length
printfn "Number of safe reports with dampener: %d" resPart2
Conclusion
Day 2 of Advent of Code pushed my understanding of F# to new heights. From implementing core functional logic to managing edge cases, this challenge highlighted the power and elegance of functional programming. Though frustrating at times, the experience deepened my appreciation for F#.
If you’ve enjoyed this walkthrough, subscribe to my newsletter for more insights into F#, problem-solving, and daily learning. See you on Day 3!