Skip to content

Commit e641fe7

Browse files
committed
Day 1, rewritten and with explanations.
1 parent cf62a77 commit e641fe7

File tree

3 files changed

+264
-19
lines changed

3 files changed

+264
-19
lines changed

README.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# Advent of Code 2020
22

3-
Having fun with [Advent of Code](https://adventofcode.com/), as this is now my third year doing it all in Clojure. This page will include lessons learned,
4-
neat things implemented, etc.
3+
Having fun with [Advent of Code](https://adventofcode.com/), as this is now my third year doing it all in Clojure.
4+
I'm going to try and make little blog of each day, explaning how I came to my solutions and how to read them.
5+
Originally I thought of fully explaining this to non-Clojurists, but other folks can explain the language better
6+
than I. So I'll assume the reader can follow along with a basic understanding of Lisp/Clojure, and I'll try to
7+
fill in the blanks when I'm able.
8+
9+
_Warning:_ I write long explanations. So... yeah.

docs/day1.md

+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# Day One: Report Repair
2+
3+
* [Problem statement](https://adventofcode.com/2020/day/1)
4+
* [Solution code](https://github.com/abyala/advent-2020-clojure/blob/master/src/advent_2020_clojure/day01.clj)
5+
6+
---
7+
8+
## Part 1
9+
10+
This problem requires reading a list of integers, finding the two that add up
11+
to the value 2020, and then returning the product of those numbers. Pretty
12+
straightforward problem.
13+
14+
My initial solution (foreshadowing!!) was to calculate this as a single function.
15+
As a convention for AoC, I tend to name the solution to each part `part1` and
16+
`part2`, so it's easy to know what to look at. Clojure namespace files require
17+
compilation in order, so if function `foo` depends on function `bar` and
18+
they are in the same file, `bar` must appear first. This is quite a change
19+
from my Java or Kotlin code, where I like to put the most business-relevant,
20+
low-granulairty methods at the top and then the implementations below.
21+
22+
The first thing I want to do is parse the input. In this case, I'm expecting a
23+
single string of the integers, separated by new-lines. For my test data, I
24+
just copy-pasted the data from the website, and for the puzzle data, I stored the
25+
data in a file and use the `slurp` function to read it all in as a single string.
26+
27+
To parse the data, I use the wonderful `thread-last` macro `->>`, which takes
28+
in a list of expressions, and supplies the result of each expression as the final
29+
value of the next expression. So if I wanted to take a numeric value `x`,
30+
square it, triple the result, and then add 1000, this is much easier to read with
31+
pipelining/threadng. Being able to read left-to-right, or top-to-bottom instead
32+
of inside-out makes Lisp much easier to work with:
33+
34+
```
35+
; Without threading
36+
(+ 1000 (* 3 (* x x)))
37+
38+
; or
39+
(+ 1000
40+
(* 3
41+
(* x x)))
42+
43+
44+
; With threading
45+
(->> (* x x)
46+
(* 3)
47+
(+ 1000)))
48+
```
49+
50+
So to parse the data, I use `str/split-lines` to break the string into a sequence
51+
of strings, separated by the newline character. Then I use the `map` function
52+
to apply the `Integer/parseInt` function to each value in the sequence, resulting
53+
in a sequence of integers. Note that this is _the_ `Integer/parseInt` static Java
54+
method, so you can see the interop is pretty slick. I take the results and bind
55+
the result to the `expenses` symbol, which is lexically scoped. This is Clojure's
56+
equivalent of making a local variable within a method.
57+
58+
```clojure
59+
(let [expenses (->> (str/split-lines input)
60+
(map #(Integer/parseInt %)))]
61+
OTHER STUFF HERE)
62+
```
63+
64+
Next, I want to find all possible pairs of non-equal values, such that they add
65+
up to 2020. Once found, return them as a vector, or a two-element tuple. For this,
66+
we use Clojure's `for` function, which like the `let` macro, takes in a vector
67+
of bindings and an expression. Here I bind both `x` and `y` to the integer sequence
68+
created above, so this is pretty close to nested loops. The `:when` keyword
69+
allows filtering of the `for` bindings, so in this case I use it to restrict the
70+
pairs to those where x is smaller than y (to avoid duplicates) and where they
71+
add up to 2020. The evaluated value is just the vector `[x y]`.
72+
73+
```clojure
74+
(for [x expenses
75+
y expenses
76+
:when (and (< x y)
77+
(= 2020 (+ x y)))]
78+
[x y])
79+
```
80+
81+
We're almost done. The `for` function returns a sequence of values, and in this
82+
case I know we only expect one value, so I wrap the results in the `first` function.
83+
That returns a single `[x y]` vector of integers, so all I need to do is multiply
84+
the values with each other. The `apply` function takes in 2 or more arguments,
85+
where the first is the function to apply, and the second is the container of values.
86+
So my function is just `*` for multiplication, and the expression is everything else
87+
I just computed. Put all together, the function looks like this:
88+
89+
```clojure
90+
(defn part1 [input]
91+
(let [expenses (->> (str/split-lines input)
92+
(map #(Integer/parseInt %)))]
93+
(apply * (first (for [x expenses
94+
y expenses
95+
:when (and (< x y)
96+
(= 2020 (+ x y)))]
97+
[x y])))))
98+
```
99+
100+
But wait -- that last section looks all scary and nested. Can't we pipeline it?
101+
Sure we can!
102+
103+
```clojure
104+
(defn part1 [input]
105+
(let [expenses (->> (str/split-lines input)
106+
(map #(Integer/parseInt %)))]
107+
(->> (for [x expenses
108+
y expenses
109+
:when (and (< x y)
110+
(= 2020 (+ x y)))]
111+
[x y])
112+
first
113+
(apply *))))
114+
```
115+
116+
## Part 2
117+
118+
This is essentially the same algorithm as part 1, except that we need three
119+
numbers to add up to 2020, instead of two. The easiest way to solve this is with
120+
a copy-paste job, adding in the third binding `z` go to along with `x` and `y`.
121+
122+
```clojure
123+
(defn part2 [input]
124+
(let [expenses (->> (str/split-lines input)
125+
(map #(Integer/parseInt %)))]
126+
(->> (for [x expenses
127+
y expenses
128+
z expenses
129+
:when (and (< x y z)
130+
(= 2020 (+ x y z)))]
131+
[x y z])
132+
first
133+
(apply *))))
134+
```
135+
136+
## Rewrite
137+
138+
What I like to do with AoC problems, when feasible, is to refactor part 1 such
139+
that I get as much reuse as possible between parts 1 and 2. In this case, we can
140+
see that the only real difference is the desired length of the vectors of ints
141+
that add up to 2020 -- two for part 1, and three for part 2. Also, I want to be
142+
a good little functional programmer and pretend that the logic of adding up to
143+
2020, or multiplying the result, could potentially have business meaning. So
144+
let's do some decomposition.
145+
146+
The first step is to create a function called `permutations` that takes in a
147+
desired sequence length and a sequence of data, and provide all permutations of
148+
data. So for `(permutations 2 [3 4])` we should get back `((3 3) (3 4) (4 3) (4 4))`.
149+
In theory, if part 3 of this problem asked for the 15 input values that add up to
150+
2020, we shouldn't have to make a structural change to the design.
151+
152+
For this to work, we use the `iterate` function, which often can take the place of
153+
a `reduce` function. It takes in a function to apply and some initialization
154+
data, and it returns an infinite sequence of applying the function to the
155+
output of the previous iteration. Let's start with the initial data first -
156+
we use `(map list data)` which takes each value in the `data` sequence and
157+
turns it into a single element list. So `(map list [1 2 3])` should return
158+
`((1) (2) (3))`. This is a working base case; if I wanted all single-element
159+
tuples in a list, I would expect a list of single-element lists. Plus,
160+
`(map list data)` sounds like I'm confused about data types, but here `map` is
161+
the mapping function and `list` is a function to create a list. No confusion!
162+
163+
Then the `iterate` function needs to flatmap each input data value onto each of
164+
these lists. The Clojure equivalent of flatmap is `mapcat`, since we map a
165+
function to the input data, and then concatenate the values back into a list.
166+
In this case, the function uses the `for` function only once, since we want to add
167+
a single value to each incoming list, and we use the `cons` function to add an
168+
element to the front of another list. Note that since we don't care about ordering,
169+
let's favor the Clojure list instead of the vector, which means that the mapping
170+
function will add the new data to the head of the list.
171+
172+
The last piece is to take the `nth` value of the infinite sequence. Calling
173+
`(nth f 0)` would provide the input data, in this case a list of length 1, so
174+
we actually want to call `(nth f (dec target-length))`.
175+
176+
```clojure
177+
(defn permutations
178+
"Creates a list of lists, containing all permutations of the incoming data, each with
179+
the intended length."
180+
[length data]
181+
(nth (iterate
182+
(fn [d] (mapcat #(for [x data] (cons x %))
183+
d))
184+
(map list data))
185+
(dec length)))
186+
```
187+
188+
Next, we'll make three tiny functions that explain the target functionality
189+
without getting bogged down in implementation. `all-increasing?` will make sure
190+
that all elements in a list are strictly increasing, so that we can keep values
191+
`(10 2010)` and throw out `(2010 10)`. `adds-to-2020?` makes sure the values...
192+
add up to 2020. And `product-of-all` multiplies all values together.
193+
194+
What I like about these functions is the use of `apply`. Remember that `apply`
195+
applies a function to all following values. So where in Java you might need
196+
to say `if ((x < y) && (y < z))`, in Clojure you can say `(if (< x y z))`.
197+
Again, if we needed to look at a tuple of 15 values, the function doesn't change.
198+
199+
```clojure
200+
(defn all-increasing? [v] (apply < v))
201+
(defn adds-to-2020? [v] (= 2020 (apply + v)))
202+
(defn product-of-all [v] (apply * v))
203+
```
204+
205+
Then we get to the key objective -- a nice `solve` function that we can share
206+
between parts 1 and 2. Let's put the pieces together.
207+
Using a thread-last pipeline, we split the input String by line, map each String
208+
value to an Integer, and calculate all permutations of those values given a target
209+
length. The `keep` function says to apply a mapping function and throw away the
210+
nulls, like Kotlin's `mapNotNull`. So here we say when we have a matching tuple,
211+
such that the values are all increasing and add to zero, return the product.
212+
Finally, returning the first (and only?) value calculated.
213+
214+
```clojure
215+
(defn solve [length input]
216+
(->> (str/split-lines input)
217+
(map #(Integer/parseInt %))
218+
(permutations length)
219+
(keep #(when (and (all-increasing? %)
220+
(adds-to-2020? %))
221+
(product-of-all %)))
222+
first))
223+
```
224+
225+
We'll know we have a good solution if the `part1` and `part2` functions look
226+
pretty. Let's see if that's the case:
227+
228+
```clojure
229+
(defn part1 [input] (solve 2 input))
230+
(defn part2 [input] (solve 3 input))
231+
```
232+
233+
I think they won a freaking beauty contest. Day 1 complete with Clojure!

src/advent_2020_clojure/day01.clj

+24-17
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
(ns advent-2020-clojure.day01
22
(:require [clojure.string :as str]))
33

4-
(defn part1 [input]
5-
(let [expenses (->> (str/split-lines input)
6-
(map #(Integer/parseInt %)))]
7-
(apply * (first (for [x expenses
8-
y expenses
9-
:when (and (< x y)
10-
(= 2020 (+ x y)))]
11-
[x y])))))
4+
(defn permutations
5+
"Creates a list of lists, containing all permutations of the incoming data, each with
6+
the intended length."
7+
[length data]
8+
(nth (iterate
9+
(fn [d] (mapcat #(for [x data] (cons x %))
10+
d))
11+
(map list data))
12+
(dec length)))
1213

13-
(defn part2 [input]
14-
(let [expenses (->> (str/split-lines input)
15-
(map #(Integer/parseInt %)))]
16-
(apply * (first (for [x expenses
17-
y expenses
18-
z expenses
19-
:when (and (< x y z)
20-
(= 2020 (+ x y z)))]
21-
[x y z])))))
14+
(defn all-increasing? [v] (apply < v))
15+
(defn adds-to-2020? [v] (= 2020 (apply + v)))
16+
(defn product-of-all [v] (apply * v))
17+
18+
(defn solve [length input]
19+
(->> (str/split-lines input)
20+
(map #(Integer/parseInt %))
21+
(permutations length)
22+
(keep #(when (and (all-increasing? %)
23+
(adds-to-2020? %))
24+
(product-of-all %)))
25+
first))
26+
27+
(defn part1 [input] (solve 2 input))
28+
(defn part2 [input] (solve 3 input))

0 commit comments

Comments
 (0)