|
| 1 | +# Day Two: Dive! |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2021/day/2) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2021-clojure/blob/master/src/advent_2021_clojure/day02.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## Part 1 |
| 9 | + |
| 10 | +Ahoy, mateys! It be time to take th' ol' sub ta sea! |
| 11 | + |
| 12 | +Today we're taking our submarine out for a ride. We're given a list of instructions, which either moves our submarine |
| 13 | +forward (increasing its position), down (increasing its depth), or up (decreasing its depth). We will need to multiply |
| 14 | +its final position by its depth to give our answer. Pretty straightforward. |
| 15 | + |
| 16 | +As always, let's start with parsing. Assuming we'll be splitting the input by each line, we want to turn an individual |
| 17 | +line of `"forward 5"` to `[:forward 5]`. Clojurians would _never_ use Strings when keywords convey the intention better, |
| 18 | +and the String 5 needs to become numeric. So we split the line into the words before and after the space, converting |
| 19 | +the former into a keyword and the later into an int. |
| 20 | + |
| 21 | +```clojure |
| 22 | +(defn parse-instruction [s] |
| 23 | + (let [[a b] (str/split s #" ")] |
| 24 | + [(keyword a) (Integer/parseInt b)])) |
| 25 | +``` |
| 26 | + |
| 27 | +Now let's briefly think about how to represent the submarine. The easiest option is a simple map with keys of |
| 28 | +`:pos` and `:depth`; to feel good about that decision, we'll define the initial submarine state. And while we're at |
| 29 | +it, let's quickly define the function `final-position` that multiplies together the `:pos` and `:depth` of the sub. |
| 30 | + |
| 31 | +```clojure |
| 32 | +(def initial-submarine {:pos 0 :depth 0}) |
| 33 | + |
| 34 | +(defn final-position [{:keys [pos depth]}] |
| 35 | + (* pos depth)) |
| 36 | +``` |
| 37 | + |
| 38 | +As we move line by line through the input, we'll need update the submarine based on the instruction found. Of course, |
| 39 | +I'm using the Clojure definition of "update," which doesn't mutate the original sub but rather creates a new one based |
| 40 | +on changed data values. While one could write, for the `:forward` instruction, `(update submarine :pos #(+ % amount))`, |
| 41 | +we can simplify the expression by providing `update`'s function and arguments inline as |
| 42 | +`(update submarine :pos + amount)` instead. Combine that with a `case` macro, and we have a simple `move-sub` function. |
| 43 | +Note that I'm destructuring the instruction into its `dir` and `amount` components within the function argument list |
| 44 | +since it's clear in this namespace what an instruction looks like. |
| 45 | + |
| 46 | +```clojure |
| 47 | +(defn move-sub [submarine [dir amount]] |
| 48 | + (case dir |
| 49 | + :forward (update submarine :pos + amount) |
| 50 | + :down (update submarine :depth + amount) |
| 51 | + :up (update submarine :depth - amount))) |
| 52 | +``` |
| 53 | + |
| 54 | +Finally, we're ready to put it all together into the `part1` function. All we need to do is split the input string |
| 55 | +into its lines, map each line to its `[dir amount]` vector with the `parse instruction` function, reduce each such |
| 56 | +instruction with the `move-sub` function and the `initial-submarine` definition, and then calculate the |
| 57 | +`final-position`. This right here is what I love about Clojure - from three simple functions, we get a very |
| 58 | +easy-to-read coordination function that reads cleanly without a lot of syntax clutter. |
| 59 | + |
| 60 | +```clojure |
| 61 | +(defn part1 [input] |
| 62 | + (->> (str/split-lines input) |
| 63 | + (map parse-instruction) |
| 64 | + (reduce move-sub initial-submarine) |
| 65 | + final-position)) |
| 66 | +``` |
| 67 | + |
| 68 | +Looking good! Let's move on to Part 2. |
| 69 | + |
| 70 | +--- |
| 71 | + |
| 72 | +## Part 2 |
| 73 | + |
| 74 | +Hmm. This looks very much the same as Part 1, except that we'll have different interpretations of each of the three |
| 75 | +instructions. We'll start with the obvious solution first, and then refactor it later. |
| 76 | + |
| 77 | +First off, a submarine is now defined by three properties instead of just two -- the position, depth, and aim. So |
| 78 | +let's revise the `initial-submarine` function. Note that this shouldn't impact `part1` at all. |
| 79 | + |
| 80 | +```clojure |
| 81 | +(def initial-submarine {:pos 0 :depth 0 :aim 0}) |
| 82 | +``` |
| 83 | + |
| 84 | +Now we'll make a `move-sub2` function that's very similar to `move-sub`, except that each instruction changes the sub |
| 85 | +differently. The `:down` and `:up` instructions just move the aim instead of the depth, so they're easy enough. |
| 86 | +The `:forward` instruction now becomes two separate `update` calls, where the latter needs to extract out the current |
| 87 | +`aim` in order to multiply it by the `amount` to get to the right answer. |
| 88 | + |
| 89 | +```clojure |
| 90 | +(defn move-sub2 [submarine [dir amount]] |
| 91 | + (case dir |
| 92 | + :forward (-> (update submarine :pos + amount) |
| 93 | + (update :depth + (* (:aim submarine) amount))) |
| 94 | + :down (update submarine :aim + amount) |
| 95 | + :up (update submarine :aim - amount))) |
| 96 | +``` |
| 97 | + |
| 98 | +Finally, `part2` is the same as `part1`, except that its reducing function is `move-sub2` instead of `move-sub`. |
| 99 | + |
| 100 | +```clojure |
| 101 | +(defn part2 [input] |
| 102 | + (->> (str/split-lines input) |
| 103 | + (map parse-instruction) |
| 104 | + (reduce move-sub2 initial-submarine) |
| 105 | + final-position)) |
| 106 | +``` |
| 107 | + |
| 108 | +I mean, it works, but if you're actually reading this text, you should know by now that I'm not going to keep that |
| 109 | +code lying around. That means it's time to refactor! |
| 110 | + |
| 111 | +--- |
| 112 | + |
| 113 | +## Cleanup |
| 114 | + |
| 115 | +It's clear that parts 1 and 2 only differ in how the submarine moves based on each instruction. I'd like to abstract |
| 116 | +that away by defining a function called `create-mover`, which will take in the three functions to apply to a submarine |
| 117 | +(forward, down, and up), and return a new function that will call the right function when invoked with a submarine and |
| 118 | +an instruction. |
| 119 | + |
| 120 | +It sounds much more complicated that it looks. Within the `defn`, we define an anonymous function with the same |
| 121 | +signature as the previous `move-sub` and `move-sub2` functions - it takes in a submarine and an instruction |
| 122 | +(destructured into its direction and amount), and it returns an updated submarine. To avoid a case statement, I create |
| 123 | +a map of the three directional keywords (`:forward`, `:down`, and `:up`) to their respective functions, and I call |
| 124 | +`(dir {map})`. Once we know which operation/function to call, we simply call `(op submarine amount)`. |
| 125 | + |
| 126 | +```clojure |
| 127 | +(defn create-mover [forward-fn down-fn up-fn] |
| 128 | + (fn [submarine [dir amount]] |
| 129 | + (let [op (dir {:forward forward-fn, :down down-fn, :up up-fn})] |
| 130 | + (op submarine amount)))) |
| 131 | +``` |
| 132 | + |
| 133 | +Now let's create our two mover functions. Each calls `create-mover` with three functions, which I've chosen to |
| 134 | +represent as anonymous functions since they're so small. The first function for part 2 (move forward) is a little more |
| 135 | +verbose since it performs two updates, but this time I destructure the `aim` out of the submarine in the function |
| 136 | +argument declaration. This is one of the awesome features of Clojure destructuring - we can define an argument of |
| 137 | +`{aim :aim :as submarine}` to mean "given an associative argument (a map), pull the `:aim` property into a |
| 138 | +local binding of `aim`, but still bind the entire argument to the binding `submarine`." Without this capability, we |
| 139 | +would have needed a `let` binding lower down, but this is much more concise and expressive. |
| 140 | + |
| 141 | +```clojure |
| 142 | +(def part1-mover (create-mover (fn [submarine amount] (update submarine :pos + amount)) |
| 143 | + (fn [submarine amount] (update submarine :depth + amount)) |
| 144 | + (fn [submarine amount] (update submarine :depth - amount)))) |
| 145 | +(def part2-mover (create-mover (fn [{aim :aim :as submarine} amount] (-> (update submarine :pos + amount) |
| 146 | + (update :depth + (* aim amount)))) |
| 147 | + (fn [submarine amount] (update submarine :aim + amount)) |
| 148 | + (fn [submarine amount] (update submarine :aim - amount)))) |
| 149 | +``` |
| 150 | + |
| 151 | +We could have gone farther and made everything anonymous, but that would turn my beautiful Clojure code into something |
| 152 | +that looks like Perl, and nobody wants that! |
| 153 | + |
| 154 | +```clojure |
| 155 | +; This is awful. Don't do this. These functions deserve to have named arguments, so let's honor them as shown above. |
| 156 | +(def part1-mover (create-mover #(update %1 :pos + %2) |
| 157 | + #(update %1 :depth + %2) |
| 158 | + #(update %1 :depth - %2))) |
| 159 | +(def part2-mover (create-mover #(-> (update %1 :pos + %2) |
| 160 | + (update :depth + (* (:aim %1) %2))) |
| 161 | + #(update %1 :aim + %2) |
| 162 | + #(update %1 :aim - %2))) |
| 163 | +``` |
| 164 | + |
| 165 | +Now that we have defined two movers, we can make the unified `solve` function. This function takes in both the mover |
| 166 | +and the input string, and then it reduces each instruction using that `mover` function. |
| 167 | + |
| 168 | +```clojure |
| 169 | +(defn solve [mover input] |
| 170 | + (->> (str/split-lines input) |
| 171 | + (map parse-instruction) |
| 172 | + (reduce mover initial-submarine) |
| 173 | + final-position)) |
| 174 | +``` |
| 175 | + |
| 176 | +Finally, we redefine the `part1` and `part2` functions. Now I could use `def` and partial functions, since I used |
| 177 | +`def` for the movers, but I think that's a little harder to understand when we're not using the functions as arguments |
| 178 | +into other functions. So instead, I'll use the normal `defn` that calls the `solve` function with the correct mover |
| 179 | +and the input String. |
| 180 | + |
| 181 | +```clojure |
| 182 | +; Using defs for the movers were fine, but I don't like it here. |
| 183 | +(def part1 (partial solve part1-mover)) |
| 184 | +(def part2 (partial solve part2-mover)) |
| 185 | + |
| 186 | +; For a tiny bit of repetition, I think these definitions are clearer. |
| 187 | +(defn part1 [input] (solve part1-mover input)) |
| 188 | +(defn part2 [input] (solve part2-mover input)) |
| 189 | +``` |
| 190 | + |
| 191 | +There, now that's a handsome submarine processing algorithm! I really love the use of higher order functions in this |
| 192 | +solution. |
0 commit comments