Today’s Advent of Code puzzle, “Distress Signal”, is one of those classic define-a-bespoke-ordering-on-a-data-structure problems.
We’ll tackle it by writing as little code as possble, which translates to a largeish import list. Reproduced right from the start, as this is literate Haskell.
import Control.Applicative ((<|>),liftA2)
import Data.Aeson (FromJSON,eitherDecodeStrict',parseJSON,withArray,withScientific)
import Data.List (findIndices,sort)
import Data.Foldable (toList)
import Data.Functor ((<&>))
import Data.Scientific (floatingOrInteger)
import qualified Data.ByteString.Char8 as BS
A packet is either an integer or a list of recursive packets.1
data Packet = I Int | L [Packet]
A packet as given in the puzzle input happens to be valid JSON, so let’s parse it from there and spare futile head-scratching.
instance FromJSON Packet where
= liftA2 (<|>) parseInt parseList
parseJSON where
= withScientific "integer" $
parseInt >>>
floatingOrInteger either (const (fail "Not an integer")) pure >>>
fmap I
= withArray "list" $
parseList >>>
toList traverse parseJSON >>>
fmap L
To parse globally, I’ll assume the input is well-formed and ignore the explicit pairings by skipping blank lines and deferring the re-pairing to part 1 which is the only one to make use of it.
main :: IO ()
= do
main Right packets <-
<&>
BS.getContents >>>
BS.lines filter (not . BS.null) >>>
traverse eitherDecodeStrict'
print (part1 packets)
print (part2 packets)
Now the puzzle revolves entirely around being able to sort packets.
Comparing two integer values is straightforward, I’ll delegate it to
the underlying Int
s:
comparePackets :: Packet -> Packet -> Ordering
I a) (I b) = compare a b comparePackets (
Comparing two list values follows the same lexicographical ordering
rules as standard Haskell lists, provided their elements have a proper
ordering. Which is not the case for us here yet, as there’s no
Ord
instance on Packet
s. But assuming we could
define one, we could delegate all the same:
L as) (L bs) = compare as bs comparePackets (
Last, comparing an integer to a list works as if promoting the integer to a single-element list.
@I{} l = comparePackets (L [a]) l
comparePackets a@I{} = comparePackets l (L [b]) comparePackets l b
So we need to define an Ord
instance on
Packet
s. How would we do that? Well, all we need to do is
call our comparePackets
function!
instance Eq Packet where (==) = ((== EQ) .) . comparePackets
instance Ord Packet where compare = comparePackets
It’s worth pondering why we needed both an Ord
instance
and an external comparison function. The reason is that sneaky
Eq
instance above. It’s needed to declare an
Ord
instance. And though what I’ll be doing probably
doesn’t need to call it, I don’t want to be liable for breakage when
that changes later on.2
In part 1, we’re only looking to identify the correctly ordered pairs.
part2 :: [Packet] -> Int
part1,= sum . map succ . findIndices (uncurry (<)) . pairs part1
In part 2, we’re looking for the sorted positions of two special markers called “dividers”.
= let dividers = [L[L[I 2]],L[L[I 6]]] in
part2 product . map succ . findIndices (`elem` dividers) . sort . (dividers ++)
And that’s all there is to it!
Well, except for a little bit of boring support code.3
pairs :: [a] -> [(a,a)]
:b:xs) = (a,b) : pairs xs
pairs (a= []
pairs [] (>>>) :: (a -> b) -> (b -> c) -> a -> c
>>>) = flip (.)
(infixr 2 >>>
This concludes today’s solution. See you tomorrow!
If you’ve read the problem, you’ll know this is factually wrong: a packet there is necessarily a list. My extension doesn’t result in further inconsistency down the road, so I’ll go with it as it gives the datastructure better regularity, with no need to single out a separate “value” type.↩︎
And while we explicitly only call
(<)
in part 1, thesort
function we use in part 2 is much more opaque and liable to freedom.↩︎(>>>)
is already present in the standard library, but with a fixity that’s not perfect for our use here. And it’s shorter to redefine it than to import it altering fixity, so…↩︎