Skip to content

Commit 9604bd6

Browse files
committed
Update readme
1 parent bbfd05c commit 9604bd6

File tree

1 file changed

+231
-0
lines changed

1 file changed

+231
-0
lines changed

README.md

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,234 @@
1+
# Thinking and Sleeping Better With Clojure
2+
3+
To prioritize a list of todo-items, as in my Prioritization-app I recently launched, I needed to get a list of the unique combinations for the user to choose from:
4+
5+
It goes a little bit like this:
6+
7+
> Which is more important: **A** or **B**?
8+
> Which is more important: **A** or **C**?
9+
> Which is more important: **B** or **C**?
10+
11+
I wrote a version in Ruby for the app and then a sketch in Clojure to compare readability and expressiveness.
12+
13+
In my opinion, the Clojure version is much more readable and was far easier to reason about.
14+
15+
Probably because I'm not good to solve problems in Ruby but there might be more than meets the eye here.
16+
17+
## Creating Comparisons
18+
19+
Let's say I have a todo-list of `A, B, C and D`.
20+
21+
The list of comparisons I want is `A ↔ B, A ↔ C, A ↔ D, B ↔ C, B ↔ D and C ↔ D` to use later in my app.
22+
23+
From the matrix below, you can see, I want only the _unique_ comparisons (`A ↔ B` and `B ↔ A` are interchangeable for my purposes) and skip dupes that compare an element with itself (`A ↔ A, B ↔ B, C ↔ C, D ↔ D`)
24+
25+
To demonstrate, I only want the top-right triangle of comparisons:
26+
27+
| × | A | B | C | D |
28+
|:---:|:---|---|---|---|
29+
| A | _skip (dupe)_ | **A ↔ B** | **A ↔ C** | **A ↔ D** |
30+
| B | _skip_ | _skip (dupe)_ | **B ↔ C** | **B ↔ D** |
31+
| C | _skip_ | _skip_ | _skip (dupe)_ | **C ↔ D** |
32+
| D | _skip_ | _skip_ | _skip_ | _skip (dupe)_ |
33+
34+
### Thinking, Testing and Implementing in Ruby
35+
36+
Now, since I started the app in Ruby, I translated the behavior above as follows:
37+
38+
```ruby
39+
describe '.unique_combinations' do
40+
context 'input list is empty' do
41+
it 'returns an empty list' do
42+
result = Combinator.unique_combinations([])
43+
expect(result).to eq([])
44+
end
45+
end
46+
47+
context 'input list only has one element' do
48+
it 'returns an empty list' do
49+
result = Combinator.unique_combinations([1])
50+
expect(result).to eq([])
51+
end
52+
end
53+
54+
context 'input list only has two elements' do
55+
it 'returns a list with combination a' do
56+
result = Combinator.unique_combinations([1, 2])
57+
expect(result).to eq([[1,2]])
58+
end
59+
60+
it 'returns a list with combination b' do
61+
result = Combinator.unique_combinations([:foo, :bar])
62+
expect(result).to eq([[:foo, :bar]])
63+
end
64+
end
65+
66+
context 'input list only has three elements' do
67+
it 'returns a list with 3 combinations' do
68+
result = Combinator.unique_combinations([1, 2, 3])
69+
expect(result).to eq([[1, 2], [1, 3], [2, 3]])
70+
end
71+
end
72+
73+
context 'input list only has four elements' do
74+
it 'returns a list with 6 combinations' do
75+
result = Combinator.unique_combinations([1, 2, 3, 4])
76+
expect(result).to eq([[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]])
77+
end
78+
end
79+
End
80+
end
81+
```
82+
83+
...and wrote, after several failed attempts and one sleepless night, the following, working but not very elegant solution. I felt weird about it, because even looking at it now, I can't fully explain it well. It works, it has reasonable test-coverage. But I'm not proud of it.
84+
85+
```ruby
86+
def self.unique_combinations(ids)
87+
return [] if ids.length < 2
88+
89+
all_combinations = []
90+
91+
for i1 in ids
92+
for i2 in ids
93+
if i1 != i2
94+
contains = false
95+
for c in all_combinations
96+
if c[1] == i1 and c[0] == i2
97+
contains = true
98+
end
99+
end
100+
unless contains
101+
all_combinations << [i1, i2]
102+
end
103+
end
104+
end
105+
end
106+
107+
return all_combinations
108+
end
109+
```
110+
111+
Okay, fine I thought. How would I solve this issue in Lisp?
112+
113+
### Thinking, Testing and Implementing in Clojure
114+
115+
I don't know why but thinking in terms of lists and recursion instead of iteration made it simpler for me.
116+
117+
The top-level function works as follows:
118+
119+
```clojure
120+
(final-combinations '(A B C D))
121+
;; => '((A B) (A C) (A D) (B C) (B D) (C D))
122+
```
123+
124+
Yup, that's the list I want.
125+
Thinking about it I had an idea to break the problem further down:
126+
127+
The complete list of unique comparisons, the one I want, is...
128+
129+
1. the **union** of each row of one element combined with all the others. That's `A` combined with `B, C and D`
130+
2. Each row has an increasing **offset** from the left that starts at 0 for row 1 and increases by 1 for each following row.
131+
3. I have to subtract the **dupes** such as `A ↔ A` because I don't need them.
132+
133+
For illustration purposes, here is the table again:
134+
135+
| × | A | B | C | D |
136+
|:---:|:---|---|---|---|
137+
| A | _skip (dupe)_ | **A ↔ B** | **A ↔ C** | **A ↔ D** |
138+
| B | _skip_ | _skip (dupe)_ | **B ↔ C** | **B ↔ D** |
139+
| C | _skip_ | _skip_ | _skip (dupe)_ | **C ↔ D** |
140+
| D | _skip_ | _skip_ | _skip_ | _skip (dupe)_ |
141+
142+
Given the above 3 steps, I basically just wrote them out as functions.
143+
144+
I derived the first function, `combine` which takes `x` and `l` and returns a list of all possible combinations, including the ones we'll later skip.
145+
146+
```clojure
147+
(with-test
148+
(defn combine
149+
([x l]
150+
(combine x l '()))
151+
([x l acc]
152+
(cond (empty? l) (reverse acc)
153+
:else (recur x (rest l) (conj acc (list x (first l)))) )))
154+
155+
(is (= (combine 1 '()) '()))
156+
(is (= (combine 1 '(1)) '((1 1))))
157+
(is (= (combine 1 '(2)) '((1 2))))
158+
(is (= (combine 1 '(1 2)) '((1 1) (1 2))))
159+
(is (= (combine 1 '(1 2 3)) '((1 1) (1 2) (1 3)))))
160+
```
161+
162+
Then, I use `combine` to create basically the table above but with nothing yet removed.
163+
164+
```clojure
165+
(with-test
166+
(defn combine-lists
167+
([l1 l2]
168+
(combine-lists l1 l2 '()))
169+
([l1 l2 acc]
170+
(cond (empty? l1) (reverse acc)
171+
:else (recur (rest l1) l2 (cons (combine (first l1) l2) acc)) )))
172+
173+
(is (= (combine-lists '() '()) '()))
174+
(is (= (combine-lists '(1) '(1)) '(((1 1)))))
175+
(is (= (combine-lists '(1 2) '(1 2)) '(((1 1) (1 2))
176+
((2 1) (2 2))))))
177+
```
178+
179+
Then I introduce the idea of an offset. To only take those elements in a list that are after a certain offset.
180+
181+
```clojure
182+
(with-test
183+
(defn take-rest
184+
([offset l]
185+
(cond (= offset 0) l
186+
:else (recur (dec offset) (rest l)))))
187+
188+
(is (= (take-rest 0 '()) '()))
189+
(is (= (take-rest 1 '()) '()))
190+
(is (= (take-rest 0 '(1)) '(1)))
191+
(is (= (take-rest 0 '(1 2)) '(1 2)))
192+
(is (= (take-rest 1 '(1 2)) '(2)))
193+
(is (= (take-rest 1 '(1 2 3)) '(2 3)))
194+
(is (= (take-rest 2 '(1 2 3)) '(3)))
195+
(is (= (take-rest 2 '(1 2 3 4)) '(3 4))))
196+
```
197+
198+
Now I can use `take-rest` and apply it to each row in my matrix...
199+
200+
```clojure
201+
(defn unique-combinations
202+
([offset ll]
203+
(unique-combinations offset ll '()))
204+
([offset ll acc]
205+
(cond (empty? ll) (reverse acc)
206+
:else (recur (inc offset) (rest ll) (cons (take-rest offset (first ll)) acc)))))
207+
```
208+
209+
And finally remove the identity items such as `A ↔ A`
210+
211+
```clojure
212+
(with-test
213+
(defn final-combinations
214+
[l]
215+
(filter #((= (first %) (second %)))) (partition 2 (flatten (unique-combinations 1 (combine-lists l l)))))
216+
217+
(is (= (final-combinations '()) '()))
218+
(is (= (final-combinations '(1)) '()))
219+
(is (= (final-combinations '(1 2)) '((1 2))))
220+
(is (= (final-combinations '(1 2 3)) '((1 2) (1 3) (2 3))))
221+
(is (= (final-combinations '(1 2 3 4)) '((1 2) (1 3) (1 4) (2 3) (2 4) (3 4)))))
222+
```
223+
224+
Granted, this code is not yet refactored and in total more lines than the Ruby version, but here is the rub:
225+
226+
It took me **20 minutes** to think up and implement the solution in Clojure and 2 days including a sleepless night to do it in Ruby.
227+
228+
I don't know what that means but I'll probably use a Lisp like Clojure for problems like this in the future. Just to make sure I get some sleep.
229+
230+
*
231+
1232
# clojure-scribbles
2233

3234
A few experiments learning Clojure, mostly recursive functions inspired by "The Little Schemer."

0 commit comments

Comments
 (0)