AoC 2021 Day 02: Dive!

Dated Feb 19, 2022; last modified on Sat, 19 Feb 2022

Day 2 - Advent of Code 2021. adventofcode.com . Accessed Feb 19, 2022.

Problem Statement

Part One

Now, you need to figure out how to pilot this thing.

It seems like the submarine can take a series of commands like forward 1, down 2, or up 3:

  • forward X increases the horizontal position by X units.
  • down X increases the depth by X units.
  • up X decreases the depth by X units.

Can’t submarines move backwards? Why is there no backward X?

Note that since you’re on a submarine, down and up affect your depth, and so they have the opposite result of what you might expect.

Calculate the horizontal position and depth you would have after the following planned course . What do you get if you multiply your final horizontal position by your final depth?

Not sure why we’re interested in \(h \cdot d\) rather than in \(\sqrt{h^2 + d^2}\), which represents the distance. Part II might have this. Update: Part II doesn’t address this.

Another extension would be taking into account the effects of water current. If a submarine is not actively moving, it seems like it should drift depending on where the currents' magnitude and direction. Update: Part II doesn’t address this.

Part Two

Based on your calculations, the planned course doesn’t seem to make any sense. You find the submarine manual and discover that the process is actually slightly more complicated.

In addition to horizontal position and depth, you’ll also need to track a third value, aim, which also starts at 0. The commands also mean something entirely different than you first thought:

  • down X increases your aim by X units.
  • up X decreases your aim by X units.
  • forward X does two things:
    • It increases your horizontal position by X units.
    • It increases your depth by your aim multiplied by X.

Again note that since you’re on a submarine, down and up do the opposite of what you might expect: “down” means aiming in the positive direction.

Using this new interpretation of the commands, calculate the horizontal position and depth you would have after following the planned course. What do you get if you multiply your final horizontal position by your final depth?

My Solution

{-# OPTIONS_GHC -Wall #-}

module Dive.Dive
  ( productOfFinalPosition,
    productOfFinalPositionWithNewIntepretation,
  )
where

-- One consideration in Haskell is the third-party library that I should use,
-- given that standard GHC seems quite lean. [1] should help in picking up
-- libraries that have lots of usage. For example, [2] helped me pick [3], which
-- uses Posix regex, and is fast, native, stable and lazy.
--
-- [1]: https://wiki.haskell.org/Category:Libraries
-- [2]: https://wiki.haskell.org/Regular_expressions
-- [3]: https://hackage.haskell.org/package/regex-tdfa

import Data.Maybe (fromJust, isJust, mapMaybe)
import Text.Read (readMaybe)
import Text.Regex.TDFA ( (=~) )

-- [1] suggests that I should be fine without deriving from the `Enum` data
-- type. Deriving from `Enum` helps me when I care about the mapping to an
-- underlying type, e.g. Int. [2]
--
-- [1]: https://www.schoolofhaskell.com/school/starting-with-haskell/basics-of-haskell/5-tokenizer-data-types#enumerated-data-types
-- [2]: https://stackoverflow.com/questions/6000511/better-way-to-define-an-enum-in-haskell/6000520
-- [3]: https://www.schoolofhaskell.com/school/starting-with-haskell/introduction-to-haskell/2-algebraic-data-types
data DiveDirection = Up | Down | Forward deriving (Eq, Show)

diveDirection :: String -> Maybe DiveDirection
diveDirection "forward" = Just Forward
diveDirection "up" = Just Up
diveDirection "down" = Just Down
diveDirection _ = Nothing

parseSubMatches :: [String] -> Maybe (DiveDirection, Int)
parseSubMatches [dir, mag] =
  let direction = diveDirection dir
      magnitude = readMaybe mag :: Maybe Int
      parsedDirAndMag =
        if isJust direction && isJust magnitude
          then Just (fromJust direction, fromJust magnitude)
          else Nothing
   in parsedDirAndMag
parseSubMatches _ = Nothing

-- Surprised that defining the constant as `STEP_REGEX` does not compile:
--
--    Not in scope: data constructor ‘FORWARD’typecheck
--
-- Variable names must start with a lowercase letter, and anything that is
-- uppercase is interpreted as a Data Constructor. [1]
--
-- [1]: https://stackoverflow.com/a/28381111/7812406
stepRegex :: [Char]
stepRegex = "([a-z]+) ([0-9]+)" -- regex-tdfa doesn't have \d for digits

directionAndMagnitude :: String -> Maybe (DiveDirection, Int)
directionAndMagnitude s =
  -- The format is (beforeMatch, firstMatch, afterMatch, [subMatches])
  let (_, _, _, subMatches) = s =~ stepRegex :: (String, String, String, [String])
   in -- Haskell lists are ordinary single-linked lists. Except operations on the
      -- first element (e.g. prepend, get, remove), the rest of the operations
      -- (including getting the length and indexing) are linear-time.
      --
      -- [1]: https://wiki.haskell.org/How_to_work_on_lists
      parseSubMatches subMatches

applySign :: (DiveDirection, Int) -> Int
applySign (Up, i) = - i
applySign (Down, i) = i
applySign (Forward, i) = i

-- It's common in Haskell to use the apostrophe to signify a relationship to a
-- previously defined item. Similar to how apostrophes are used in math.
--
-- [1]: https://stackoverflow.com/a/5673954/7812406
applySign' :: Maybe (DiveDirection, Int) -> Int
-- Hlint saved me here. My initial implementation was:
--
--    if isJust m then applySign $ fromJust m else 0
--
-- ... and Hlint suggested:
--
--    maybe 0 applySign m
--
-- ... and further suggested dropping the `m` citing Eta reduction. This
-- reduction leads to the cleaner Pointfree style which omits the names of the
-- variables. Pointfree style puts the spotlight on composing functions (high
-- level), rather than shuffling data (low level). [1] [2].
--
-- [1]: https://wiki.haskell.org/Eta_conversion
-- [2]: https://wiki.haskell.org/Pointfree
-- [3]: https://www.schoolofhaskell.com/user/school/starting-with-haskell/introduction-to-haskell/4-higher-order-programming-and-type-inference#wholemeal-programming
applySign' = maybe 0 applySign

isForward :: (DiveDirection, Int) -> Bool
isForward (Forward, _) = True
isForward _ = False

cumulativeSums :: [Int] -> [Int]
cumulativeSums [] = []
cumulativeSums [x] = [x]
cumulativeSums [x, y] = [x, x + y]
cumulativeSums (x : y : zs) = x : cumulativeSums (x + y : zs)

productOfFinalPosition :: [String] -> Int
productOfFinalPosition steps =
  -- In general, use `where` for locally-defined functions, and `let` for
  -- intermediate values. [1]
  --
  -- I don't fully get the nuance in [2], but it might come in handy later as
  -- I get more experience with Haskell. A broad stroke would be `let` places
  -- fewer restrictions, and `where` is more readable, but `where` may obscure
  -- inefficient code that redefines local functions.
  --
  -- [1]: http://www.cse.unsw.edu.au/~cs3161/14s2/StyleGuide.html#sec-5-1-1
  -- [2]: https://wiki.haskell.org/Let_vs._Where
  let parsedSteps = mapMaybe directionAndMagnitude steps

      -- `fst` and `snd` are utility functions for the first and second members
      -- of a pair, respectively. [1]
      --
      -- [1]: https://en.wikibooks.org/wiki/Haskell/Lists_and_tuples

      -- How do I compute `horizontalPos` and `verticalPos` while iterating
      -- through `parsedSteps` once? From an imperative programming background,
      -- this looks pretty inefficient! Update: [1] suggests the `foldl`
      -- package.
      --
      -- [1]: https://hackage.haskell.org/package/base-4.16.0.0/docs/Data-Foldable.html#g:18
      finalHorizontalPos = sum $ map applySign $ filter isForward parsedSteps

      -- The function composition operator is handy when inverting the filter
      -- predicate.
      --
      -- [1]: https://techoverflow.net/2014/01/03/haskell-invert-filter-predicate/
      finalVerticalPos = sum $ map applySign $ filter (not . isForward) parsedSteps
   in finalHorizontalPos * finalVerticalPos

productOfFinalPositionWithNewIntepretation :: [String] -> Int
productOfFinalPositionWithNewIntepretation steps =
  let parsedSteps = mapMaybe directionAndMagnitude steps
      finalHorizontalPos = sum $ map applySign $ filter isForward parsedSteps
      -- Hlint suggestion on using map once was illuminating. I had code like:
      --
      --    l = ([1..10] :: [Int])
      --    map even $ map negate l
      --
      -- ... for which Hlint suggested:
      --
      --    map (even . negate) l
      --
      -- ... and that makes sense. Composition strikes again!
      --
      -- That said, the (.) can be unwieldy because we normally read from left
      -- to right, but (.) composition is read from right to left. For extra
      -- readability, we can use the (>>>) operator from `Control.Arrow`, e.g.
      --
      --    map (negate >>> even) l
      --
      -- ... as it flips the (.) operator. [1]
      --
      -- [1]: https://byorgey.wordpress.com/2019/04/24/competitive-programming-in-haskell-basic-setup/
      aimDeltas =
        map
          (applySign' . (\x -> if (not . isForward) x then Just x else Nothing))
          parsedSteps

      -- How do I do cumulative sums, i.e. [1, 2, 3, 4] -> [1, 3, 6, 10]? The end
      -- result is a list, so maybe something to do with `map`, but how do I
      -- reference what came before? Maybe a standalone function with pattern
      -- matching?
      cumulativeAims = cumulativeSums aimDeltas

      forwardDeltas =
        map
          (applySign' . (\x -> if isForward x then Just x else Nothing))
          parsedSteps

      -- In other languages, moving from a list to a single value is referred to
      -- as a reduction. Haskell uses the term "folding". On lists, one can
      -- either recursively combine the 1st element with the result of combining
      -- the rest (right fold), or recursively combine the results of combining
      -- all but the last element with the last element (left fold). Some
      -- initial value is provided that is combined with the first item in the
      -- list. There are some nuances:
      --
      --   `foldr` lazily evaluates the recursive case of folding over the rest
      --   of the list. This allows it to handle computations on infinite lists
      --   that either produce some result without referencing the recursive
      --   case, or short-circuit (e.g. `||` short-circuits on the first
      --   `True`).
      --
      --
      --   `foldl` immediately calls itself with new params until it reaches the
      --   end of the list (and thus can't handle infinite loops). However, its
      --   tail recursiveness (the final result of the recursive call is the
      --   final result of the function itself) can be efficiently compiled as a
      --   as a loop.
      --
      --
      --   `foldl` does not evaluate the initial parameter before the recursive
      --   call is made. At the end of the list, we may end up with a gigantic
      --   expression that causes stack overflow. Haskell provides the `foldl'`
      --   function that forces evaluation of the initial parameter before
      --   making the recursive call.
      --
      -- From [1] [2]. [3] and [4] goes into way more detail, and are worth a
      -- closer read.
      --
      -- That said, I still don't know how to summarize `(zip forwardDeltas
      -- aimDeltas)`. `foldl` and `foldr` accept operators, but there's no
      -- operator for "increase depth value by aim multiplied by X". Seems like
      -- `foldMap` is the place to be, but WHAT IS A MONOID?
      --
      -- Update: Misread the docs. `fold` and `foldMap` need monoids, but
      -- `foldl` and `foldr` have examples with lambdas. Nice!
      --
      -- [1]: https://wiki.haskell.org/Fold
      -- [2]: https://wiki.haskell.org/Tail_recursion
      -- [3]: https://hackage.haskell.org/package/base-4.16.0.0/docs/Data-Foldable.html#overview
      -- [4]: https://www.schoolofhaskell.com/user/school/starting-with-haskell/introduction-to-haskell/6-laziness

      -- My version had:
      --
      --    (fst p * snd p)
      --
      -- ... and Hlint proposed the use of uncurry, which converts a curried
      -- function to a function on pairs. That's pretty neat.
      --
      -- [1]: https://hackage.haskell.org/package/base-4.16.0.0/docs/Prelude.html#v:uncurry

      -- Lambda abstractions can also have multiple arguments. For example:
      --
      --    \x y z -> [x, 2 * y, 3 * z]
      --
      -- ... is an anonymous function that takes 3 arguments.
      --
      -- [1]: https://www.schoolofhaskell.com/user/school/starting-with-haskell/introduction-to-haskell/4-higher-order-programming-and-type-inference#anonymous-functions
      finalDepth =
        foldr
          (\p depthSoFar -> depthSoFar + uncurry (*) p)
          0
          (zip forwardDeltas cumulativeAims)
   in finalHorizontalPos * finalDepth

Learning from Others' Solutions

Once again, I find myself admiring ’s solution, which has fewer moving parts.

data Instruction = GoForward Int | GoUp Int | GoDown Int deriving (Show)
type Pos = (Int, Int)

moveA :: Pos -> Instruction -> Pos
moveA (x, y) (GoForward dx) = (x + dx, y)
moveA (x, y) (GoUp dy)      = (x, y + dy)
moveA (x, y) (GoDown dy)    = (x, y - dy)

productOfFinalPosition :: [Instruction] -> Int
productOfFinalPosition instructions = x * y
  where (x, y) = foldl moveA (0, 0) instructions

… compared to mine:

data DiveDirection = Up | Down | Forward deriving (Eq, Show)
directionAndMagnitude :: String -> Maybe (DiveDirection, Int)
applySign :: (DiveDirection, Int) -> Int
isForward :: (DiveDirection, Int) -> Bool

-- The fact that I'm repeatedly using `(DiveDirection, Int)` is a sign
-- that something is wrong with my abstraction that only considers
-- `DiveDirection`. My major thought at the time was that, "Convert
-- string inputs to enum values at parsing time," and once I achieved
-- that, I patted myself on the back for applying a best practice. I did
-- not stop to consider what else I could do to improve the abstraction.

productOfFinalPosition :: [String] -> Int
productOfFinalPosition steps =
  let parsedSteps = mapMaybe directionAndMagnitude steps
      finalHorizontalPos = sum $ map applySign $ filter isForward parsedSteps
      finalVerticalPos = sum $ map applySign $ filter (not . isForward) parsedSteps
  in finalHorizontalPos * finalVerticalPos

-- Granted, during this time, I hadn't known about folding.

… really shows the code clarity that can be obtained from creating the proper abstractions.

“sum $ map applySign $ filter (not . isForward) parsedSteps”,
colorized, Feb 2022.

“sum $ map applySign $ filter (not . isForward) parsedSteps”, colorized, Feb 2022.

Copilot suggested product . foldl' moveA (0, 0), but product (3, 3) = 3 while product [3, 3] = 9. Color me surprised.

’s solution for part two contains syntax that I am not familiar with:

-- `Navigation` is using the record syntax. The compiler will generate
-- accessor functions and update functions for the fields. Note that the
-- records are still immutable, so the update syntax `y { x = z}`
-- creates a new independent value. Also note that the compiler does not
-- enforce that all fields are set. Accessing unspecified fields results
-- in a runtime error. [1]
--
-- [1]: https://en.wikibooks.org/wiki/Haskell/More_on_datatypes#Named_Fields_(Record_Syntax)
data Navigation = Navigation {depth :: Int, aim :: Int, x :: Int}

moveB :: Navigation -> Instruction -> Navigation
-- The `..` syntax is enabled by the RecordWildCards GHC language
-- extension. Each elided field `f` is replaced by the pattern `f = f`.
-- [1].
--
-- The `@` syntax that is preceded by whitespace is enabled by the
-- TypeApplications GHC language extension [2].
--
-- When the `@` is not preceded by whitespace, it is being
-- used as an as-pattern, which names a pattern for use of the RHS of an
-- equation, e.g. `f s@(x:xs) = x:s` [3].
--
-- [1]: https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/record_wildcards.html
-- [2]: https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/type_applications.html
-- [3]: https://www.haskell.org/tutorial/patterns.html
moveB n@Navigation{ .. } (GoForward dx) = n{ x = x + dx
                                           , depth = depth + aim * dx}
moveB n@Navigation{ .. } (GoUp dAim)      = n{ aim = aim - dAim }
moveB n@Navigation{ .. } (GoDown dAim)    = n{ aim = aim + dAim }

productOfFinalPositionWithNewIntepretation :: [Instruction] -> Int
productOfFinalPositionWithNewIntepretation instructions = x * depth
  where Navigation{ .. } = foldl moveB (Navigation 0 0 0) instructions

The fact that we can do n{x = x + dx} confused me because I didn’t think x = x + dx is valid Haskell syntax, given it is not a generally true mathematical equation. However, the first x refers to the x field in n, while the second x refers to the one that was autogenerated by the RecordWildCards extension.

Le’s solution starts with, “Day 2 has a satisfying “unified” solution for both parts that can be derived from group theory!” and I’m not about that pure math life right now. ┐( ˘ 、 ˘ )┌

It does mention an #adventofcode channel in libera IRC, and that’s something worth checking out and possibly joining.

References

  1. Advent of Code 2021: Day 2: Dive! jhidding.github.io . Accessed Feb 22, 2022.