Allow output types to vary for different parts of same day

For now this applies to Haskell only, and it may turn out to be tricky for the Rust implementation.

In practice, the limitation hasn't turned out to be important, and we could even go the other way and use `Integer` everywhere. This does however at least help with debugging, as well as just being conceptually right.

The `nil` and `(/\)` functions are intended to be overloaded to work for other list-like things in a later commit, and from there we will investigate using `OverloadedLists` and `RebindableSyntax` to recover standard list syntax, although there are probably limitations due to `(:)` being special.
This commit is contained in:
George Thomas 2025-12-16 16:15:11 +00:00
parent 6ca7b4eac8
commit 415055dcc2
13 changed files with 151 additions and 92 deletions

View File

@ -45,7 +45,7 @@ main =
in in
withResource (parseFile $ "../inputs/" <> t <> "/" <> pt) mempty \input -> withResource (parseFile $ "../inputs/" <> t <> "/" <> pt) mempty \input ->
testGroup pt $ testGroup pt $
( zip (map show [1 :: Int ..]) parts <&> \(n, pp) -> ( flip mapWithIndexOutputParameterisedFunctionList parts \(show . succ -> n) pp ->
goldenVsStringDiff n diffCommand ("../outputs/" <> t <> "/" <> pt <> "/" <> n) $ goldenVsStringDiff n diffCommand ("../outputs/" <> t <> "/" <> pt <> "/" <> n) $
TL.encodeUtf8 . (<> "\n") . TL.show . pp <$> input TL.encodeUtf8 . (<> "\n") . TL.show . pp <$> input
) )

View File

@ -42,6 +42,11 @@ module Pre (
adjacentPairs, adjacentPairs,
sortPair, sortPair,
diffCommand, diffCommand,
OutputParameterisedFunctionList,
mapOutputParameterisedFunctionList,
mapWithIndexOutputParameterisedFunctionList,
(/\),
nil,
) )
where where
@ -69,7 +74,8 @@ import Data.Foldable hiding (foldl1, foldr1, maximum, maximumBy, minimum, minimu
import Data.Foldable1 import Data.Foldable1
import Data.Function import Data.Function
import Data.Functor import Data.Functor
import Data.List (sortOn, transpose) import Data.Kind (Constraint, Type)
import Data.List (List, sortOn, transpose)
import Data.List.Extra (dropEnd, enumerate, firstJust, notNull, splitOn) import Data.List.Extra (dropEnd, enumerate, firstJust, notNull, splitOn)
import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty, some1, tail, tails) import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty, some1, tail, tails)
import Data.Maybe import Data.Maybe
@ -91,10 +97,10 @@ import Text.Megaparsec hiding (Pos, State, Stream, many, some)
import Text.Megaparsec.Char import Text.Megaparsec.Char
import Text.Megaparsec.Char.Lexer (decimal) import Text.Megaparsec.Char.Lexer (decimal)
data Puzzle = forall input output. (Show output) => Puzzle data Puzzle = forall input outputs. Puzzle
{ number :: Word { number :: Word
, parser :: Bool -> Parsec Void Text input , parser :: Bool -> Parsec Void Text input
, parts :: [input -> output] , parts :: OutputParameterisedFunctionList Show input outputs
, extraTests :: Bool -> FilePath -> IO input -> [TestTree] , extraTests :: Bool -> FilePath -> IO input -> [TestTree]
} }
@ -125,3 +131,35 @@ sortPair (a, b) = if a <= b then (a, b) else (b, a)
diffCommand :: FilePath -> FilePath -> [String] diffCommand :: FilePath -> FilePath -> [String]
diffCommand a b = ["diff", "--color=always", a, b] diffCommand a b = ["diff", "--color=always", a, b]
infixr 9 /\
(/\) :: (c output) => (input -> output) -> OutputParameterisedFunctionList c input outputs -> OutputParameterisedFunctionList c input (output : outputs)
(/\) = OutputParameterisedFunctionListCons
nil :: OutputParameterisedFunctionList c input '[]
nil = OutputParameterisedFunctionListNil
data OutputParameterisedFunctionList (c :: Type -> Constraint) (input :: Type) (outputs :: List Type) :: Type where
OutputParameterisedFunctionListNil :: OutputParameterisedFunctionList c input '[]
OutputParameterisedFunctionListCons ::
(c output) =>
(input -> output) ->
OutputParameterisedFunctionList c input outputs ->
OutputParameterisedFunctionList c input (output ': outputs)
mapOutputParameterisedFunctionList ::
(forall output. (c output) => (input -> output) -> a) ->
OutputParameterisedFunctionList c input outputs ->
[a]
mapOutputParameterisedFunctionList f = \case
OutputParameterisedFunctionListNil -> []
OutputParameterisedFunctionListCons x xs -> f x : mapOutputParameterisedFunctionList f xs
mapWithIndexOutputParameterisedFunctionList ::
forall c input outputs a.
(forall output. (c output) => Int -> (input -> output) -> a) ->
OutputParameterisedFunctionList c input outputs ->
[a]
mapWithIndexOutputParameterisedFunctionList f = go 0
where
go :: Int -> OutputParameterisedFunctionList c input outputs' -> [a]
go i = \case
OutputParameterisedFunctionListNil -> []
OutputParameterisedFunctionListCons x xs -> f i x : go (i + 1) xs

View File

@ -8,13 +8,15 @@ puzzle =
{ number = 1 { number = 1
, parser = const $ ((,) <$> ((char 'L' $> L) <|> (char 'R' $> R)) <*> (Inc <$> decimal)) `sepEndBy` newline , parser = const $ ((,) <$> ((char 'L' $> L) <|> (char 'R' $> R)) <*> (Inc <$> decimal)) `sepEndBy` newline
, parts = , parts =
[ sum ( sum
. flip evalState 50 . ( flip evalState 50
. traverse \(d, i) -> do . traverse \(d, i) -> do
modify $ snd . step i d modify $ snd . step i d
p' <- get p' <- get
pure $ Count if p' == 0 then 1 else 0 pure $ Count if p' == 0 then 1 else 0
, sum )
)
/\ ( sum
. flip evalState 50 . flip evalState 50
. traverse \(d, i) -> do . traverse \(d, i) -> do
p <- get p <- get
@ -27,7 +29,8 @@ puzzle =
| p == 0 -> abs c - 1 | p == 0 -> abs c - 1
| p' == 0 -> abs c + 1 | p' == 0 -> abs c + 1
| otherwise -> abs c | otherwise -> abs c
] )
/\ nil
, extraTests = mempty , extraTests = mempty
} }

View File

@ -23,12 +23,13 @@ puzzle =
void $ single '}' void $ single '}'
pure (Lights $ IM.fromList $ zip [0 ..] lights, switches) pure (Lights $ IM.fromList $ zip [0 ..] lights, switches)
, parts = , parts =
[ sum . map \(lights, switches) -> ( sum . map \(lights, switches) ->
maybe (error "no solution") length maybe (error "no solution") length
. firstJust (firstJust (\(s, ls) -> guard (allOff ls) $> s)) . firstJust (firstJust (\(s, ls) -> guard (allOff ls) $> s))
$ flip iterate [([], lights)] \ls -> $ flip iterate [([], lights)] \ls ->
concatMap (\s -> map (\(ss, l) -> (s : ss, applySwitch s l)) ls) switches concatMap (\s -> map (\(ss, l) -> (s : ss, applySwitch s l)) ls) switches
] )
/\ nil
, extraTests = mempty , extraTests = mempty
} }

View File

@ -10,13 +10,15 @@ puzzle =
{ number = 2 { number = 2
, parser = const $ (<* newline) $ ((,) <$> (decimal <* char '-') <*> decimal) `sepBy` (char ',') , parser = const $ (<* newline) $ ((,) <$> (decimal <* char '-') <*> decimal) `sepBy` (char ',')
, parts = , parts =
[ sum ( sum
. concatMap . concatMap
(mapMaybe (\n -> guard (isRepetition2 n) $> n) . uncurry enumFromTo) (mapMaybe (\n -> guard (isRepetition2 n) $> n) . uncurry enumFromTo)
, sum )
/\ ( sum
. concatMap . concatMap
(mapMaybe (\n -> guard (isRepetitionN n) $> n) . uncurry enumFromTo) (mapMaybe (\n -> guard (isRepetitionN n) $> n) . uncurry enumFromTo)
] )
/\ nil
, extraTests = mempty , extraTests = mempty
} }

View File

@ -10,11 +10,13 @@ puzzle =
{ number = 3 { number = 3
, parser = const $ (Bank <$> some1 digit) `sepEndBy` newline , parser = const $ (Bank <$> some1 digit) `sepEndBy` newline
, parts = , parts =
[ sum ( sum
. map (digitsToInt . fromMaybe (error "battery list too short") . maxBatteries 2) . map (digitsToInt . fromMaybe (error "battery list too short") . maxBatteries 2)
, sum )
/\ ( sum
. map (digitsToInt . fromMaybe (error "battery list too short") . maxBatteries 12) . map (digitsToInt . fromMaybe (error "battery list too short") . maxBatteries 12)
] )
/\ nil
, extraTests = mempty , extraTests = mempty
} }

View File

@ -14,11 +14,13 @@ puzzle =
{ number = 4 { number = 4
, parser = const $ (some $ asum $ enumerate <&> \t -> char (inToChar t) $> t) `sepEndBy` newline , parser = const $ (some $ asum $ enumerate <&> \t -> char (inToChar t) $> t) `sepEndBy` newline
, parts = , parts =
[ (\g -> countRolls g - countRolls (removeAccessibleRolls $ findAccessible g)) ( (\g -> countRolls g - countRolls (removeAccessibleRolls $ findAccessible g))
. mkGrid . mkGrid
, (\g -> countRolls g - countRolls (fst $ S.head $ S.filter (noneAccessible . snd) $ generateFrames g)) )
/\ ( (\g -> countRolls g - countRolls (fst $ S.head $ S.filter (noneAccessible . snd) $ generateFrames g))
. mkGrid . mkGrid
] )
/\ nil
, extraTests = \isRealData path input -> , extraTests = \isRealData path input ->
[ testCase "round trip" do [ testCase "round trip" do
t <- TL.readFile if isRealData then "../inputs/real/4" else "../inputs/examples/4" t <- TL.readFile if isRealData then "../inputs/real/4" else "../inputs/examples/4"

View File

@ -12,16 +12,18 @@ puzzle =
vals <- decimal `sepEndBy` newline vals <- decimal `sepEndBy` newline
pure (ranges, vals) pure (ranges, vals)
, parts = , parts =
[ \(ranges, vals) -> ( \(ranges, vals) ->
length length
. filter (flip any ranges . isInRange) . filter (flip any ranges . isInRange)
$ vals $ vals
, sum )
/\ ( sum
. map rangeLength . map rangeLength
. foldr addInterval [] . foldr addInterval []
. sortOn (Down . (.lower)) . sortOn (Down . (.lower))
. fst . fst
] )
/\ nil
, extraTests = mempty , extraTests = mempty
} }

View File

@ -12,10 +12,11 @@ puzzle =
void newline void newline
pure (ops, ints) pure (ops, ints)
, parts = , parts =
[ sum ( sum
. uncurry (zipWith applyToList) . uncurry (zipWith applyToList)
. second (transpose . map (map (digitsToInt @Int . catMaybes) . filter notNull . splitOn [Nothing])) . second (transpose . map (map (digitsToInt @Int . catMaybes) . filter notNull . splitOn [Nothing]))
, sum )
/\ ( sum
. uncurry (zipWith applyToList) . uncurry (zipWith applyToList)
. second . second
( map catMaybes ( map catMaybes
@ -23,7 +24,8 @@ puzzle =
. map (\l -> if all isNothing l then Nothing else Just $ digitsToInt @Int $ catMaybes l) . map (\l -> if all isNothing l then Nothing else Just $ digitsToInt @Int $ catMaybes l)
. transpose . transpose
) )
] )
/\ nil
, extraTests = mempty , extraTests = mempty
} }

View File

@ -17,7 +17,7 @@ puzzle =
let splitters = map (IS.fromList . map fst . filter snd . zip [0 ..]) rows let splitters = map (IS.fromList . map fst . filter snd . zip [0 ..]) rows
pure (start, splitters) pure (start, splitters)
, parts = , parts =
[ uncurry \start -> ( uncurry \start ->
flip execState (0 :: Int) flip execState (0 :: Int)
. foldlM . foldlM
( \beams splitters -> ( \beams splitters ->
@ -27,7 +27,8 @@ puzzle =
pure if hit then [x - 1, x + 1] else [x] pure if hit then [x - 1, x + 1] else [x]
) )
(IS.singleton start) (IS.singleton start)
, uncurry \start -> )
/\ ( uncurry \start ->
sum sum
. map snd . map snd
. IM.toList . IM.toList
@ -38,6 +39,7 @@ puzzle =
zip (if hit then [x - 1, x + 1] else [x]) (repeat n) zip (if hit then [x - 1, x + 1] else [x]) (repeat n)
) )
(IM.singleton start (1 :: Int)) (IM.singleton start (1 :: Int))
] )
/\ nil
, extraTests = mempty , extraTests = mempty
} }

View File

@ -15,7 +15,7 @@ puzzle =
(if isRealData then 1000 else 10,) (if isRealData then 1000 else 10,)
<$> (V3 <$> decimal <* single ',' <*> decimal <* single ',' <*> decimal) `sepEndBy` newline <$> (V3 <$> decimal <* single ',' <*> decimal <* single ',' <*> decimal) `sepEndBy` newline
, parts = , parts =
[ uncurry \n -> ( uncurry \n ->
product product
. take 3 . take 3
. sortOn Down . sortOn Down
@ -24,13 +24,15 @@ puzzle =
. maybe (error "not enough boxes") snd . maybe (error "not enough boxes") snd
. listIndex n . listIndex n
. connectBoxes . connectBoxes
, uncurry . const $ )
/\ ( uncurry . const $
uncurry ((*) `on` view _x) uncurry ((*) `on` view _x)
. maybe (error "sets never unified") fst . maybe (error "sets never unified") fst
. lastMay . lastMay
. takeWhile ((> 1) . DS.sets . snd) . takeWhile ((> 1) . DS.sets . snd)
. connectBoxes . connectBoxes
] )
/\ nil
, extraTests = mempty , extraTests = mempty
} }

View File

@ -8,12 +8,13 @@ puzzle =
{ number = 9 { number = 9
, parser = const $ (V2 <$> decimal <* single ',' <*> decimal) `sepEndBy1` newline , parser = const $ (V2 <$> decimal <* single ',' <*> decimal) `sepEndBy1` newline
, parts = , parts =
[ maximum ( maximum
. fmap (squareSize . uncurry Rectangle) . fmap (squareSize . uncurry Rectangle)
. fromMaybe (error "input too small") . fromMaybe (error "input too small")
. nonEmpty . nonEmpty
. allUnorderedPairs False . allUnorderedPairs False
, \points -> )
/\ ( \points ->
let path = let path =
fromMaybe (error "malformed line") fromMaybe (error "malformed line")
. traverse mkLine . traverse mkLine
@ -26,7 +27,8 @@ puzzle =
. sortOn (Down . snd) . sortOn (Down . snd)
. fmap ((id &&& squareSize) . uncurry Rectangle) . fmap ((id &&& squareSize) . uncurry Rectangle)
$ allUnorderedPairs False points $ allUnorderedPairs False points
] )
/\ nil
, extraTests = mempty , extraTests = mempty
} }

View File

@ -24,6 +24,7 @@ executable aoc
default-extensions: default-extensions:
BlockArguments BlockArguments
DuplicateRecordFields DuplicateRecordFields
ImpredicativeTypes
LexicalNegation LexicalNegation
MultiWayIf MultiWayIf
NamedFieldPuns NamedFieldPuns