Skip to content

Commit 13c5a59

Browse files
author
Gorm
committed
Add article: Negative Sets as Data
Cofinite sets with one sentinel key — all set operations reduce to three map primitives (merge, keep, remove). No special types, no wrapper objects, no case explosion. Includes working Clojure implementation, verification of all four polarity combinations, and proofs of De Morgan's laws, identity elements, and self-complement properties.
1 parent 7931535 commit 13c5a59

1 file changed

Lines changed: 397 additions & 0 deletions

File tree

src/math/sets/negative_sets.clj

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
^{:kindly/hide-code true
2+
:clay {:title "Negative Sets as Data"
3+
:quarto {:type :post
4+
:author [:jclaggett]
5+
:date "2026-03-21"
6+
:description "Cofinite sets with one sentinel key: all set operations reduce to three map primitives."
7+
:category :math
8+
:tags [:sets :data-structures :maps]
9+
:keywords [:cofinite :negative-sets :set-operations :maps :complement]}}}
10+
(ns math.sets.negative-sets
11+
(:require [scicloj.kindly.v4.kind :as kind]))
12+
13+
;; Finite sets are everywhere in programming. But sometimes you need
14+
;; the *complement* of a set — "everything except these elements."
15+
;; This comes up in access control, type systems, query filters,
16+
;; permission models, and anywhere you work with open-world assumptions.
17+
18+
;; The usual approach is to introduce a separate type: a tagged union,
19+
;; a wrapper class, or a special algebra with case analysis at every
20+
;; operation. This adds complexity and makes composition harder.
21+
22+
;; This article presents a simpler approach: represent both finite and
23+
;; cofinite sets as plain maps, distinguished by a single sentinel key.
24+
;; All set operations — union, intersection, difference, complement,
25+
;; membership — reduce to three map primitives. No special types,
26+
;; no wrapper objects, no case explosion.
27+
28+
;; ## The Key Insight
29+
30+
;; A **positive set** is a map where every key maps to itself:
31+
32+
;; ```
33+
;; #{a b c} → {a a, b b, c c}
34+
;; ```
35+
36+
;; A **negative set** (cofinite set) represents "everything except these
37+
;; elements." It is the same map structure, but with one additional entry:
38+
;; a distinguished sentinel key `:neg` that maps to itself:
39+
40+
;; ```
41+
;; "everything except a and b" → {:neg :neg, a a, b b}
42+
;; ```
43+
44+
;; The sentinel is just data. It flows through map operations like any
45+
;; other key. This is the entire trick.
46+
47+
;; ## Implementation
48+
49+
;; We need exactly three map primitives. These are standard operations
50+
;; available in any language with associative data structures:
51+
52+
(defn map-merge
53+
"Add B's keys not already in A."
54+
[a b]
55+
(merge b a))
56+
57+
(defn map-keep
58+
"Keep only A's keys that are also in B."
59+
[a b]
60+
(select-keys a (keys b)))
61+
62+
(defn map-remove
63+
"Remove A's keys that are also in B."
64+
[a b]
65+
(apply dissoc a (keys b)))
66+
67+
;; That's it for primitives. Everything else is built from these three.
68+
69+
;; ### Constructing Sets
70+
71+
;; A positive set maps each element to itself:
72+
73+
(defn pos-set
74+
"Create a positive set from elements."
75+
[& elements]
76+
(zipmap elements elements))
77+
78+
;; A negative set is the same, but with the `:neg` sentinel:
79+
80+
(defn neg-set
81+
"Create a negative set (complement) from excluded elements."
82+
[& excluded]
83+
(assoc (apply pos-set excluded) :neg :neg))
84+
85+
;; Some useful constants:
86+
87+
(def empty-set
88+
"The empty set (positive, no elements)."
89+
{})
90+
91+
(def universal-set
92+
"The universal set (negative, no exclusions)."
93+
{:neg :neg})
94+
95+
;; Note: the empty set is just an empty map. The universal set is
96+
;; `{:neg :neg}` — "everything except nothing."
97+
98+
;; ### Predicates
99+
100+
(defn negative?
101+
"Is this a negative (cofinite) set?"
102+
[s]
103+
(contains? s :neg))
104+
105+
(defn member?
106+
"Is x a member of set s?"
107+
[s x]
108+
(if (negative? s)
109+
(nil? (get s x)) ;; negative: member if NOT listed
110+
(some? (get s x)))) ;; positive: member if listed
111+
112+
;; For positive sets, membership is the usual map lookup. For negative
113+
;; sets, the logic inverts — an element is a member if it is *not*
114+
;; present in the exclusion list.
115+
116+
;; Let's verify:
117+
118+
(def vowels (pos-set :a :e :i :o :u))
119+
(def consonants (neg-set :a :e :i :o :u))
120+
121+
^kind/table
122+
{:vowels {:member-a (member? vowels :a)
123+
:member-b (member? vowels :b)
124+
:member-z (member? vowels :z)}
125+
:consonants {:member-a (member? consonants :a)
126+
:member-b (member? consonants :b)
127+
:member-z (member? consonants :z)}}
128+
129+
;; `vowels` is a positive set containing `:a :e :i :o :u`.
130+
;; `consonants` is a negative set excluding `:a :e :i :o :u` —
131+
;; in other words, every letter *except* the vowels.
132+
;; Same data, same structure, inverted meaning.
133+
134+
;; ### Complement
135+
136+
;; Complementing a set is just toggling the `:neg` key:
137+
138+
(defn complement-set
139+
"Toggle between positive and negative."
140+
[s]
141+
(if (negative? s)
142+
(dissoc s :neg)
143+
(assoc s :neg :neg)))
144+
145+
;; This is O(1). No structural rebuild. The `:neg` key acts as a
146+
;; single bit that flips the interpretation of the entire set.
147+
148+
;; Verify that complement round-trips:
149+
150+
^kind/table
151+
{:vowels-complement (= consonants (complement-set vowels))
152+
:double-complement (= vowels (complement-set (complement-set vowels)))}
153+
154+
;; ## The Operation Table
155+
156+
;; Here is where things get interesting. Every set operation —
157+
;; union, intersection, and difference — can be expressed as one of
158+
;; our three map primitives. Which primitive to use depends only on
159+
;; whether each operand is positive or negative:
160+
161+
^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)"}]
166+
167+
;; Read each row as: "when A has this polarity and B has that polarity,
168+
;; use this map primitive."
169+
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.
174+
175+
;; ### Why Does This Work?
176+
177+
;; Consider **union of two positive sets**. The union of `{a, b}` and
178+
;; `{b, c}` should be `{a, b, c}`. As maps, this is
179+
;; `merge({a:a, b:b}, {b:b, c:c})` = `{a:a, b:b, c:c}`. ✓
180+
181+
;; Now consider **union of two negative sets**. The union of
182+
;; "everything except {a, b}" and "everything except {b, c}" should be
183+
;; "everything except {b}" — only elements excluded from *both* stay
184+
;; excluded. As maps, this is
185+
;; `keep({neg:neg, a:a, b:b}, {neg:neg, b:b, c:c})`
186+
;; = `{neg:neg, b:b}`. ✓
187+
188+
;; The sentinel `:neg` is in both maps, so `keep` preserves it.
189+
;; The result is still a negative set — correct!
190+
191+
;; For the **mixed cases**: the union of a positive and a negative set
192+
;; is always a negative set (it contains "everything except..." some
193+
;; smaller set). Intersection of a positive and negative set is always
194+
;; positive (it's a filtered subset). The operations work out because
195+
;; the `:neg` key flows through the map primitives naturally.
196+
197+
;; ## Implementation of Set Operations
198+
199+
;; The dispatch is a simple 2×2 case on polarity:
200+
201+
(defn- dispatch
202+
"Select the map operation based on polarity of a and b.
203+
ops is [pos-pos, neg-neg, pos-neg, neg-pos]."
204+
[[pp nn pn np] a b]
205+
(let [na (negative? a)
206+
nb (negative? b)]
207+
(cond
208+
(and (not na) (not nb)) (pp a b)
209+
(and na nb) (nn a b)
210+
(and (not na) nb) (pn a b)
211+
:else (np a b))))
212+
213+
(def set-union
214+
"Union of two sets (positive or negative)."
215+
(partial dispatch [map-merge map-keep
216+
(fn [a b] (map-remove b a))
217+
map-remove]))
218+
219+
(def set-intersection
220+
"Intersection of two sets (positive or negative)."
221+
(partial dispatch [map-keep map-merge
222+
map-remove
223+
(fn [a b] (map-remove b a))]))
224+
225+
(def set-difference
226+
"Difference of two sets (positive or negative)."
227+
(partial dispatch [map-remove
228+
(fn [a b] (map-remove b a))
229+
map-keep
230+
map-merge]))
231+
232+
;; That's the entire implementation. Three primitives, three operations,
233+
;; four cases each. Let's verify with examples.
234+
235+
;; ## Verification
236+
237+
;; ### Positive × Positive
238+
239+
;; The familiar case: ordinary finite sets.
240+
241+
(def abc (pos-set :a :b :c))
242+
(def bcd (pos-set :b :c :d))
243+
244+
^kind/table
245+
[{:operation "A ∪ B" :result (set-union abc bcd) :expected "{a b c d}"}
246+
{:operation "A ∩ B" :result (set-intersection abc bcd) :expected "{b c}"}
247+
{:operation "A \\ B" :result (set-difference abc bcd) :expected "{a}"}]
248+
249+
;; ### Negative × Negative
250+
251+
;; Two cofinite sets. "Everything except {a,b}" and "everything except {b,c}":
252+
253+
(def not-ab (neg-set :a :b))
254+
(def not-bc (neg-set :b :c))
255+
256+
^kind/table
257+
[{:operation "A ∪ B"
258+
:result (set-union not-ab not-bc)
259+
:expected "neg{b} — everything except b"}
260+
{:operation "A ∩ B"
261+
:result (set-intersection not-ab not-bc)
262+
:expected "neg{a,b,c} — everything except a,b,c"}
263+
{:operation "A \\ B"
264+
:result (set-difference not-ab not-bc)
265+
:expected "pos{c} — just c"}]
266+
267+
;; That last one is worth pausing on. The difference of two negative
268+
;; sets can produce a *positive* set! "Everything except {a,b}" minus
269+
;; "everything except {b,c}" = "things that are excluded from B but
270+
;; not from A" = `{c}`. The `:neg` sentinel is absent from the result
271+
;; because `remove(B, A)` removes the `:neg` key (present in both).
272+
273+
;; ### Mixed: Positive × Negative
274+
275+
(def ab (pos-set :a :b))
276+
(def not-bc' (neg-set :b :c))
277+
278+
^kind/table
279+
[{:operation "pos ∪ neg"
280+
:result (set-union ab not-bc')
281+
:expected "neg{c} — everything except c"}
282+
{:operation "pos ∩ neg"
283+
:result (set-intersection ab not-bc')
284+
:expected "pos{a} — just a"}
285+
{:operation "pos \\ neg"
286+
:result (set-difference ab not-bc')
287+
:expected "pos{b} — just b"}]
288+
289+
;; ### Mixed: Negative × Positive
290+
291+
^kind/table
292+
[{:operation "neg ∪ pos"
293+
:result (set-union not-bc' ab)
294+
:expected "neg{c} — everything except c"}
295+
{:operation "neg ∩ pos"
296+
:result (set-intersection not-bc' ab)
297+
:expected "pos{a} — just a"}
298+
{:operation "neg \\ pos"
299+
:result (set-difference not-bc' ab)
300+
:expected "neg{a,b,c} — everything except a,b,c"}]
301+
302+
;; ## Properties
303+
304+
;; Let's verify some algebraic properties hold:
305+
306+
;; ### De Morgan's Laws
307+
308+
;; `complement(A ∪ B) = complement(A) ∩ complement(B)`
309+
310+
(let [a (pos-set :a :b :c)
311+
b (pos-set :b :c :d)]
312+
^kind/table
313+
[{:law "complement(A ∪ B) = complement(A) ∩ complement(B)"
314+
:holds? (= (complement-set (set-union a b))
315+
(set-intersection (complement-set a) (complement-set b)))}
316+
{:law "complement(A ∩ B) = complement(A) ∪ complement(B)"
317+
:holds? (= (complement-set (set-intersection a b))
318+
(set-union (complement-set a) (complement-set b)))}])
319+
320+
;; ### Identity Elements
321+
322+
^kind/table
323+
[{:law "A ∪ ∅ = A"
324+
:holds? (= abc (set-union abc empty-set))}
325+
{:law "A ∩ U = A"
326+
:holds? (= abc (set-intersection abc universal-set))}
327+
{:law "A ∪ U = U"
328+
:holds? (= universal-set (set-union abc universal-set))}
329+
{:law "A ∩ ∅ = ∅"
330+
:holds? (= empty-set (set-intersection abc empty-set))}]
331+
332+
;; ### Self-Complement
333+
334+
^kind/table
335+
[{:law "A ∪ complement(A) = U"
336+
:holds? (= universal-set (set-union abc (complement-set abc)))}
337+
{:law "A ∩ complement(A) = ∅"
338+
:holds? (= empty-set (set-intersection abc (complement-set abc)))}
339+
{:law "complement(∅) = U"
340+
:holds? (= universal-set (complement-set empty-set))}
341+
{:law "complement(U) = ∅"
342+
:holds? (= empty-set (complement-set universal-set))}]
343+
344+
;; All the standard set algebra laws hold. This isn't a coincidence —
345+
;; it's a consequence of the representation being a faithful encoding
346+
;; of Boolean algebra over the power set.
347+
348+
;; ## Why This Matters
349+
350+
;; **It's just maps.** No new types, no wrappers, no separate code
351+
;; paths. Any system that can store and manipulate maps can represent
352+
;; both finite and cofinite sets. Serialization, hashing, comparison,
353+
;; indexing — they all come for free from the underlying map.
354+
355+
;; **Composition is natural.** Because the sentinel key participates in
356+
;; operations like any other key, you can freely mix positive and
357+
;; negative sets in chains of operations without explicit polarity
358+
;; tracking. The polarity of the result emerges from the operation.
359+
360+
;; **Complement is O(1).** Toggling one key. Not rebuilding a structure,
361+
;; not wrapping in a `Not(...)` node, not allocating a new type.
362+
363+
;; **Three primitives are enough.** `merge`, `keep`, `remove` on maps.
364+
;; These are available in every language. The 4×3 dispatch table is
365+
;; small enough to memorize.
366+
367+
;; ## The Broader Context
368+
369+
;; Cofinite sets appear in the literature under various names —
370+
;; co-sets, complementary representations, "open world" sets. They
371+
;; typically require special algebraic treatment. The insight here is
372+
;; that by encoding the polarity *within* the data (as a sentinel key),
373+
;; the algebra reduces to plain map operations.
374+
375+
;; This representation was developed as part of
376+
;; [Dacite](https://github.com/jclaggett/dacite), a content-addressed
377+
;; data structure system. In a content-addressed store, the sentinel
378+
;; key costs zero additional storage — both key and value slots point
379+
;; to the same hash. The set operations compose with the store's
380+
;; existing map operations, requiring no special support.
381+
382+
;; But the idea is general. Anywhere you have maps, you have both
383+
;; finite and cofinite sets.
384+
385+
;; ## Summary
386+
387+
;; | Concept | Representation |
388+
;; |---------|----------------|
389+
;; | Positive set `{a, b}` | `{a: a, b: b}` |
390+
;; | Negative set "all except {a, b}" | `{:neg :neg, a: a, b: b}` |
391+
;; | Empty set | `{}` |
392+
;; | Universal set | `{:neg :neg}` |
393+
;; | Complement | Toggle `:neg` key |
394+
;; | Membership | pos: `get ≠ nil` · neg: `get = nil` |
395+
;; | All operations | Three map primitives × four polarity cases |
396+
397+
;; One sentinel key. Three map primitives. Complete set algebra.

0 commit comments

Comments
 (0)