Advent of Code 2025 in Haskell

It’s that time of year again.

You can try out the solutions here .

Day 1

module Main where

import Common (parseFile)
import Control.Applicative ((<|>))
import Data.List (foldl', scanl)
import Data.Text qualified as T
import Data.Void (Void)
import Text.Megaparsec
import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer qualified as Lex
import Text.Printf

data Dir = L | R
  deriving (Show, Eq)

data Rot = Rot Dir Int
  deriving (Show, Eq)

type Parser = Parsec Void T.Text

dirSign :: Dir -> Int
dirSign L = -1
dirSign R = 1

parseRot :: Parser Rot
parseRot = Rot <$> (L <$ char 'L' <|> R <$ char 'R') <*> Lex.decimal

solve1 :: [Rot] -> Int
solve1 rots = length $ filter (== 0) $ scanl update 50 rots
  where
    update pos (Rot d x) = (pos + (dirSign d) * x) `mod` 100

solve2 :: [Rot] -> Int
solve2 rots = snd $ foldl' expand (50, 0) rots
  where
    expand (pos, count) (Rot d x) =
      let steps = [pos + dirSign d * n | n <- [1 .. x]]
       in ((pos + dirSign d * x) `mod` 100, count + length (filter (\p -> p `mod` 100 == 0) steps))

main :: IO ()
main = do
  sampleRots <- parseFile (parseRot `sepEndBy` newline) "input/day01_sample.txt"
  rots <- parseFile (parseRot `sepEndBy` newline) "input/day01.txt"
  printf "Part 1 Sample answer: %s\n" (show $ solve1 sampleRots)
  printf "Part 1 Final answer: %s\n" (show $ solve1 rots)
  printf "Part 2 Sample answer: %s\n" (show $ solve2 sampleRots)
  printf "Part 2 Final answer: %s\n" (show $ solve2 rots)

Yes, I know part 2’s solution is inefficient—it’s prettier this way.

Day 2

module Main where

import Common
import Text.Megaparsec
import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer qualified as Lex
import Text.Printf

parseIds :: Parser [(Int, Int)]
parseIds =
  pair `sepBy` char ',' <* optional eol <* eof
  where
    pair = (,) <$> Lex.decimal <* char '-' <*> Lex.decimal

isInvalid1 :: String -> Bool
isInvalid1 s = (take halfLen s) == (drop halfLen s)
  where
    halfLen = length s `div` 2

isInvalid2 :: String -> Bool
isInvalid2 s =
  any allEqual [1 .. length s `div` 2]
  where
    allEqual k =
      case partition k s of
        [] -> False
        (x : xs) -> all (== x) xs

partition :: Int -> String -> [String]
partition n s
  | n <= 0 = []
  | length s `mod` n /= 0 = []
  | otherwise = go s
  where
    go [] = []
    go xs = take n xs : go (drop n xs)

solve :: (String -> Bool) -> [(Int, Int)] -> Int
solve valid =
  sum
    . filter (valid . show)
    . concatMap (uncurry enumFromTo)

main = do
  sampleIds <- parseFile parseIds "./input/day02_sample.txt"
  ids <- parseFile parseIds "./input/day02.txt"
  printf "Sample Part 1: %d\n" $ solve isInvalid1 sampleIds
  printf "Part 1: %d\n" $ solve isInvalid1 ids
  printf "Sample Part 2: %d\n" $ solve isInvalid2 sampleIds
  printf "Part 2: %d\n" $ solve isInvalid2 ids

Chef’s kiss.

LLM transparency

I used LLMs to fetch documentation and clean up the code structurally. All solutions are conceptually original.