The day 2 puzzle, “Dive!”, is another of these “follow an instruction sequence using two different interpretations”. This post is literate Haskell, so let’s get the imports out of the way.
import Control.Arrow ((&&&),(***))
import Control.Lens (makeLenses,Getter,(&),(+~),to,view)
import Control.Lens.Unsound (lensProduct)
import Data.Semigroup (Dual(..),Endo(..))
The code defines three instructions, but two of them (up
and down
) behave symmetrically, so I’ll merge their
internal representation.
data Command = Horiz Int | Vert Int
parse :: String -> Command
words -> [cmd,read -> i]) = case cmd of
parse ("forward" -> Horiz i
"down" -> Vert i
"up" -> Vert (-i)
Every time I try to use ViewPatterns, I’m disappointed with the post-hoc readability. Oh well.
In part 1, the commands’ interpretation is straightforward, summing bidimensional moves.
data State1 = S1 { _horiz :: !Int, _depth :: !Int }
'State1
makeLenses '
pos1 :: State1
= S1 0 0
pos1
part1 :: Command -> Endo State1
= Endo . \case Horiz i -> horiz +~ i
part1 Vert i -> depth +~ i
display :: Getter State1 Int
= lensProduct horiz depth . to (uncurry (*)) display
Feels weird to reach for Lens.Unsound for a simple getter product. That’s what I get for using lenses once per year. Improvements welcome.
In part 2, the interpretation gets weird. We still have a position, but are invited to keep track of an additional value: the aim.
data State2 = S2 { _pos :: !State1, _aim :: !Int }
'State2
makeLenses '
pos2 :: State2
= S2 pos1 0 pos2
It’s still implemented as a simple case match. You’ll note I use the
Dual
monoid adaptor: Endo
composes in the
usual mathematical direction, resulting in functions being applied right
to left. It didn’t matter for part 1 where they commuted, but here we
really have to apply them left to right.
part2 :: Command -> Dual (Endo State2)
= Dual . Endo . \case Vert i -> aim +~ i
part2 Horiz i -> depthChange i . (pos.horiz +~ i)
where depthChange i st = st & pos.depth +~ view aim st * i
That depthChange
function is also a bit disappointing: I
was hoping to write it without making st
explicit.
Improvements welcome.
Anyway, a wrapper to run the whole of it and we’re done.
main :: IO ()
= interact $ show .
main *** view (pos.display)) .
(view display `appEndo` pos1) *** (`appEndo` pos2) . getDual) .
((foldMap (part1 &&& part2) .
map parse . lines
This concludes today’s solution. See you tomorrow!