|
25 | 25 | ;; membership — reduce to three map primitives. No special types, |
26 | 26 | ;; no wrapper objects, no case explosion. |
27 | 27 |
|
| 28 | +;; Clojure obviously offers sets as a built-in type — but they are just a thin |
| 29 | +;; wrapper around maps. For the rest of this article we will work directly with |
| 30 | +;; maps, since they are the underlying representation of sets. This more |
| 31 | +;; clearly shows that the same idea applies to any language with associative |
| 32 | +;; data structures. |
| 33 | + |
28 | 34 | ;; ## The Key Insight |
29 | 35 |
|
30 | 36 | ;; A **positive set** is a map where every key maps to itself: |
|
41 | 47 | ;; "everything except a and b" → {:neg :neg, a a, b b} |
42 | 48 | ;; ``` |
43 | 49 |
|
44 | | -;; The sentinel is just data. It flows through map operations like any |
45 | | -;; other key. This is the entire trick. |
| 50 | +;; The sentinel is just data. It flows through map operations like any other |
| 51 | +;; key. This is the entire trick. |
| 52 | + |
| 53 | +;; Note that whichever value is used to denote negative sets is no longer |
| 54 | +;; available as a normal element so choose wisely. |
46 | 55 |
|
47 | 56 | ;; ## Implementation |
48 | 57 |
|
|
62 | 71 | (defn map-remove |
63 | 72 | "Remove A's keys that are also in B." |
64 | 73 | [a b] |
65 | | - (apply dissoc a (keys b))) |
| 74 | + (reduce dissoc a (keys b))) |
66 | 75 |
|
67 | 76 | ;; That's it for primitives. Everything else is built from these three. |
68 | 77 |
|
|
106 | 115 | "Is x a member of set s?" |
107 | 116 | [s x] |
108 | 117 | (if (negative? s) |
109 | | - (nil? (get s x)) ;; negative: member if NOT listed |
110 | | - (some? (get s x)))) ;; positive: member if listed |
| 118 | + (not (contains? s x)) ;; negative: member if NOT listed |
| 119 | + (contains? s x))) |
111 | 120 |
|
112 | 121 | ;; For positive sets, membership is the usual map lookup. For negative |
113 | 122 | ;; sets, the logic inverts — an element is a member if it is *not* |
|
123 | 132 | :member-b (member? vowels :b) |
124 | 133 | :member-z (member? vowels :z)} |
125 | 134 | :consonants {:member-a (member? consonants :a) |
126 | | - :member-b (member? consonants :b) |
127 | | - :member-z (member? consonants :z)}} |
| 135 | + :member-b (member? consonants :b) |
| 136 | + :member-z (member? consonants :z)}} |
128 | 137 |
|
129 | 138 | ;; `vowels` is a positive set containing `:a :e :i :o :u`. |
130 | 139 | ;; `consonants` is a negative set excluding `:a :e :i :o :u` — |
|
147 | 156 |
|
148 | 157 | ;; Verify that complement round-trips: |
149 | 158 |
|
150 | | -^kind/table |
151 | | -{:vowels-complement (= consonants (complement-set vowels)) |
152 | | - :double-complement (= vowels (complement-set (complement-set vowels)))} |
| 159 | +(= consonants (complement-set vowels)) |
| 160 | +(= vowels (complement-set (complement-set vowels))) |
153 | 161 |
|
154 | 162 | ;; ## The Operation Table |
155 | 163 |
|
|
159 | 167 | ;; whether each operand is positive or negative: |
160 | 168 |
|
161 | 169 | ^kind/table |
162 | | -[{:A "pos" :B "pos" :union "merge(A,B)" :intersect "keep(A,B)" :difference "remove(A,B)"} |
163 | | - {:A "neg" :B "neg" :union "keep(A,B)" :intersect "merge(A,B)" :difference "remove(B,A)"} |
164 | | - {:A "pos" :B "neg" :union "remove(B,A)" :intersect "remove(A,B)" :difference "keep(A,B)"} |
165 | | - {:A "neg" :B "pos" :union "remove(A,B)" :intersect "remove(B,A)" :difference "merge(A,B)"}] |
| 170 | +[{:A "+" :B "+" :union "(merge A B)" :intersect "(keep A B)" :difference "(remove A B)"} |
| 171 | + {:A "-" :B "-" :union "(keep A B)" :intersect "(merge A B)" :difference "(remove B A)"} |
| 172 | + {:A "+" :B "-" :union "(remove B A)" :intersect "(remove A B)" :difference "(keep A B)"} |
| 173 | + {:A "-" :B "+" :union "(remove A B)" :intersect "(remove B A)" :difference "(merge A B)"}] |
166 | 174 |
|
167 | 175 | ;; Read each row as: "when A has this polarity and B has that polarity, |
168 | 176 | ;; use this map primitive." |
169 | 177 |
|
170 | | -;; Notice the symmetry. Each of the three primitives appears exactly |
171 | | -;; once in every column — there are no redundant cases and no gaps. |
172 | | -;; The `neg` sentinel key participates in the map operations naturally, |
173 | | -;; so the result automatically has the correct polarity. |
| 178 | +;; Notice the symmetry. merge and keep appear once and remove appears twice in |
| 179 | +;; a complementary pattern in each column. The four cases are exhaustive and |
| 180 | +;; mutually exclusive — every combination of positive and negative sets is |
| 181 | +;; covered. The `neg` sentinel key participates in the map operations |
| 182 | +;; naturally, so the result automatically has the correct polarity. |
174 | 183 |
|
175 | 184 | ;; ### Why Does This Work? |
176 | 185 |
|
|
347 | 356 |
|
348 | 357 | ;; ## Why This Matters |
349 | 358 |
|
350 | | -;; **It's just maps.** No new types, no wrappers, no separate code |
| 359 | +;; **It's just maps.** No new types, wrappers or separate code |
351 | 360 | ;; paths. Any system that can store and manipulate maps can represent |
352 | 361 | ;; both finite and cofinite sets. Serialization, hashing, comparison, |
353 | 362 | ;; indexing — they all come for free from the underlying map. |
|
384 | 393 |
|
385 | 394 | ;; ## Summary |
386 | 395 |
|
| 396 | +;; One sentinel key. Three map primitives. Complete set algebra. |
| 397 | + |
387 | 398 | ;; | Concept | Representation | |
388 | 399 | ;; |---------|----------------| |
389 | 400 | ;; | Positive set `{a, b}` | `{a: a, b: b}` | |
|
393 | 404 | ;; | Complement | Toggle `:neg` key | |
394 | 405 | ;; | Membership | pos: `get ≠ nil` · neg: `get = nil` | |
395 | 406 | ;; | All operations | Three map primitives × four polarity cases | |
396 | | - |
397 | | -;; One sentinel key. Three map primitives. Complete set algebra. |
|
0 commit comments