Skip to content

Commit 2585db8

Browse files
committed
Day 2
1 parent 4fb0e82 commit 2585db8

File tree

5 files changed

+1242
-0
lines changed

5 files changed

+1242
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
| Day # | Source | Blog Post |
44
| ----- | ------ | --------- |
55
| 1 | [source](src/advent_2021_clojure/day01.clj) | [blog](docs/day01.md) |
6+
| 2 | [source](src/advent_2021_clojure/day02.clj) | [blog](docs/day02.md) |

docs/day02.md

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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

Comments
 (0)