Sets, Relations, and Functions: The Language of Structure
📍 Where we are: Part I · Logic from Scratch — Chapter 1. The preface promised a bedrock built one stone at a time; this is the first stone — the vocabulary of structure that every later chapter, and every later volume, quietly stands on.
Logic, the subject of the chapters just ahead, is a language for stating what is true. But before a language can state anything, it needs nouns to talk about and a way to say how those nouns hang together. Mathematics packs that whole starter kit into three ideas: the set, the relation, and the function. They are the plainest ideas in the book and the most load-bearing: get them clear now and the rest of the volume is bookkeeping on top of them.
This chapter does not just name the three. It builds each from nothing, derives every property the later chapters lean on rather than asserting it, decodes every symbol the first time it appears, and then runs the definitions line by line on committed code so that every claim, "advises is not transitive," "affiliated is a function," "closing cites reaches p1 from p3," is a number a program actually prints, not a story we are telling.
Imagine the contact list on your phone. The list itself is a set — a bag of names, no duplicates, and the order does not matter. "Who follows whom" is a relation — a tangle of arrows drawn between names. And "each person's one home address" is a function — an arrow that never forks, exactly one destination per person. That is the entire chapter: a set is a bag, a relation is a web of arrows, and a function is a web where every arrow leaves a name exactly once. Everything logic does later — and, surprisingly, everything a neural network does later too — is built by stacking these three.
What this chapter covers
- Sets and membership — the bag-of-things idea, extensionality, cardinality, the empty set, and subsets, grounded on the people, papers, and institutions of our running academic world.
- Ordered pairs and the Cartesian product — how to build every conceivable pairing of two sets, why the order inside a pair carries meaning, and how many pairs and how many relations there even are.
- Relations as sets of tuples — advises, cites, and affiliated are not magic; each is literally a set of pairs picked out of a Cartesian product, read straight from the knowledge base.
- The three shapes that matter — reflexive, symmetric, and transitive, each with its exact predicate, worked by hand and confirmed by the machine (advises is not transitive).
- Functions — a relation where each input has exactly one output; affiliated qualifies, advises does not, and the single-valued test in the code says precisely why.
- Composition and transitive closure — chaining advises with itself is grand-advising, and closing cites is citation reachability, traced round by round to a fixpoint and checked against real program output.
- Why neuro-symbolic AI needs this — a knowledge graph is a set of typed relations, reasoning composes relations, and an embedding is a function from symbols to vectors.
Sets: the bag of things
A set is an unordered collection of distinct things, called its elements or members. Two words in that sentence do all the work. Unordered means the set is fixed by which things are in it and by nothing else, so {alice, bob} and {bob, alice} are the same set. Distinct means an element is either in or out; there is no such thing as being in twice, so {alice, alice, bob} is just {alice, bob}. Both facts are two faces of one principle, the axiom of extensionality: two sets are equal exactly when they have the same members, regardless of how they are listed [1].
To say a thing belongs to a set we write the membership symbol ∈, read "is an element of": alice ∈ People asserts that alice is one of the people. Its negation is ∉, read "is not an element of": carol ∉ Institutions. The number of elements in a set A is its cardinality, written with vertical bars around it, (the bars mean "the size of," a different use of the bar from the "such that" bar we meet in the next section, so watch which one you are reading). Our running academic world, defined in kb.py, opens with the three individual-sets we will use, on lines 32–34:
PEOPLE = ["alice", "bob", "carol", "dave", "erin"]
PAPERS = ["p1", "p2", "p3"]
INSTITUTIONS = ["mit", "cmu"]
So , , and . (The code writes them as Python lists, which do record order and can repeat; the set idea is the promise that we will use neither of those two powers, and every operation below respects that promise.)
A set with nothing in it is the empty set, written ∅, the unique set with . It shows up the moment you ask a question with no answer. Who advises alice? Scanning the advising facts, alice never appears in the second slot, so the set of alice's advisors is ∅. The empty set is not "nothing"; it is a perfectly good set that happens to hold zero elements, and treating it as an ordinary object is what keeps later definitions from cracking on edge cases [1].
One set can sit wholly inside another. If every element of A is also an element of B, we say A is a subset of B, written A ⊆ B. Spelled out with the quantifier ∀ (read "for all") and the implication → (read "if… then…"), this is
that is, "for every thing , if is in then is in ." Note that this is vacuously satisfied when is empty, so for every : there is no element of that could fail the test. The rules in kb.py build a little tower of subsets. The professors are {alice, bob} (facts on lines 40–41); the rule on line 75, researcher(X) ⟸ professor(X), makes every professor a researcher, and the rule on line 76 does the same for every student, so all five people are researchers; the rule on line 77, person(X) ⟸ researcher(X), makes every researcher a person. Reading those three rules as set inclusions gives
with cardinalities (a subset of a finite set can be no larger than the set, so the sizes only ever climb or hold). Subset is how a logic will later say "every professor is a person" without listing anyone, the first hint that a crisp set relationship can stand in for a general truth.
Ordered pairs and the Cartesian product
A set forgets order, but the world does not: "alice advises bob" is a different fact from "bob advises alice." To capture direction we need the ordered pair, written (a, b), where, unlike a set, (alice, bob) and (bob, alice) are genuinely different objects. What makes a pair "ordered" is exactly its characteristic property:
two pairs are equal only when they agree in the first slot and in the second slot [2].
The deeper reason we are allowed to help ourselves to ordered pairs is that they are not a new kind of thing at all; they can be built out of sets, so nothing outside our three ideas has smuggled itself in. The standard construction is . To see it has the characteristic property, first dispose of the degenerate case where the two coordinates coincide. When the pair collapses to , a family with a single member, and then reads ; a one-member family can equal another only if that other also has one member with the same content, so , which forces , exactly and . Now assume , so the pair genuinely has two distinct members, and . Two extractions recover the coordinates: the intersection of the pair's two members is , and their union is . Since are the same two-member set (so as well), the extractions must agree on each side: intersecting gives , hence , and unioning gives , which together with forces . Order is thus recoverable from an unordered container, purely by which element sits alone in its own singleton [2].
Now collect every ordered pair you could form by taking a first element from a set A and a second from a set B. That collection is the Cartesian product, written A × B. In "set-builder" notation, where the vertical bar reads "such that" and the ∧ reads "and," it is
How big is it? To choose a pair you make two independent choices: the first coordinate, in ways, and the second, in ways. Independent choices multiply, so
For our people, People × People holds all conceivable "who-to-whom" pairings: every advising link that could exist, true or not, including the five self-pairs like (alice, alice). The Cartesian product is the space of all possibilities; the next idea is how we pick out the possibilities that actually hold.
Relations: sets of tuples
A relation is simply a subset of a Cartesian product, R ⊆ A × B, a chosen handful of pairs out of all conceivable ones. That is the whole definition, and it is startlingly powerful. Because a relation is any subset of the 25 pairs, and each of those 25 pairs is independently either in or out, the count of distinct relations on People is
(each pair contributing an independent factor of 2, in-or-out, so the factors multiply to ). advises is one specific choice among those thirty-three million: the four pairs of People × People that actually hold, listed in kb.py on lines 47–50:
("advises", "alice", "bob"),
("advises", "bob", "carol"),
("advises", "bob", "dave"),
("advises", "carol", "erin"),
The same move gives us cites as a subset of Papers × Papers (kb.py lines 57–58: p2 cites p1, p3 cites p2) and affiliated as a subset of People × Institutions (lines 60–64: each person paired with their school). In sets_relations.py, the helper pairs (lines 16–19) does nothing more exotic than read these facts back out as a mathematical set of pairs:
def pairs(predicate: str) -> set:
"""Extract a binary relation from the KB as a set of (a, b) pairs."""
return {(a, b) for (p, a, b) in
(f for f in FACTS if len(f) == 3) if p == predicate}
That braces-and-bar comprehension is Python's transcription of set-builder notation: it reads "the set of all (a, b) such that (p, a, b) is a length-3 fact and p equals the requested predicate." There is no gap between the code and the math here: pairs("advises") is the relation advises. Sorting it into a definite order for display, sorted(pairs("advises")) evaluates to the four pairs
sorted(pairs("advises")) = [('alice', 'bob'), ('bob', 'carol'), ('bob', 'dave'), ('carol', 'erin')]
with out of the 25 possible. Every operation in the rest of the chapter is an operation on sets like this one.
The academic world as sets and relations: people, papers, and institutions are the individuals; advises, cites, and affiliated are sets of ordered pairs; and composing or closing those pairs is exactly how grand-advising and citation-reachability appear.
Original diagram by the authors, created with AI assistance.
The three shapes a relation can have
Once a relation is a set of pairs, we can ask what shape it has, and three shapes come up so often they have names. sets_relations.py turns each shape into an exact predicate (lines 39–48), and we can read the definition straight off the code [3]:
def is_reflexive(R: set, domain) -> bool:
return all((x, x) in R for x in domain)
def is_symmetric(R: set) -> bool:
return all((b, a) in R for (a, b) in R)
def is_transitive(R: set) -> bool:
return all((a, c) in R for (a, b) in R for (b2, c) in R if b == b2)
Reflexive means every element relates to itself: . The predicate is_reflexive walks the whole domain, a list, in order and demands the self-pair each time. Advising fails at the very first person: (alice, alice) is not among the four pairs, so all(...) short-circuits to False. Nobody advises themselves, so advises is not reflexive, whereas a relation like "is at the same institution as," counting yourself, would be.
Symmetric means the arrows come in both directions: . The predicate takes each stored pair (a, b) and checks that its reverse (b, a) is also present. Advising is emphatically not symmetric: some stored pair, say (alice, bob) ∈ advises, has its reverse (bob, alice) ∉ advises, so all(...) returns False no matter which order the set happens to be walked in. To see symmetry actually hold, take the derived colleague relation, defined in kb.py on lines 82–83: two distinct people are colleagues when they share an institution. Grinding that rule over affiliated (the mit group {alice, bob} and the cmu group {carol, dave, erin}) yields, both directions for every unordered pair inside a group,
colleague = [('alice','bob'), ('bob','alice'),
('carol','dave'), ('carol','erin'), ('dave','carol'),
('dave','erin'), ('erin','carol'), ('erin','dave')]
eight ordered pairs (the mit group contributes , the cmu group ), and is_symmetric(colleague) returns True: the definition of colleague is built from the symmetric condition "share an institution," so every arrow it draws comes with its reverse for free.
Transitive is the one that matters most for reasoning: whenever (a, b) and (b, c) are both in, (a, c) must be too, arrows chain into shortcuts. Formally, ; the predicate is_transitive finds every pair of stored pairs whose inner endpoints match (the guard if b == b2) and demands the shortcut (a, c). Here is the counterexample the whole series leans on. Take the two advises pairs (alice, bob) and (bob, carol): their inner endpoints match at bob, so transitivity would require (alice, carol) ∈ advises. It is not there. So the check fails and advises is not transitive. That missing shortcut is not a bug; it is a different relationship, grand-advising, and the next section builds it deliberately rather than pretending advises already contained it. Evaluating all three shape predicates on advises confirms the hand argument exactly:
is_reflexive(advises, people) = False
is_symmetric(advises) = False
is_transitive(advises) = False
advises has none of the three shapes, which is precisely why it is the relation we keep returning to: every operation ahead is about manufacturing the structure it lacks.
Functions: relations that never fork
A function is a relation with one extra promise: each input appears as the first element of at most one pair. No input maps to two different outputs; the arrows never fork. Written as a formula, R ⊆ A × B is a (partial) function when
"if points to both and , then and were the same all along." When a function is total (every input in appears) we write it and write , also drawn (the bar-arrow ⟼ reads "maps to"), naming the one output belonging to input . sets_relations.py checks the single-valued promise directly (lines 51–58):
def is_function(R: set) -> bool:
"""A relation is a (partial) function iff no input maps to two outputs."""
seen = {}
for a, b in R:
if a in seen and seen[a] != b:
return False
seen[a] = b
return True
The dictionary seen remembers the output each input has already claimed; the instant an input reappears with a different output, the fork is detected and the function promise is broken. Trace it on the two relations.
affiliated is a function. Its five pairs are alice ↦ mit, bob ↦ mit, carol ↦ cmu, dave ↦ cmu, erin ↦ cmu. Each person is a first coordinate exactly once, so no input ever reappears, the seen[a] != b branch never fires, and the loop runs to return True. This mirrors the modelling comment in kb.py (line 59): "Affiliation is functional: each person works at exactly one institution." Note that affiliated is not injective (one-to-one): mit is the output for two different inputs and cmu for three, so many inputs share an output, which is fine, a function forbids one input from forking to two outputs, not two inputs from meeting at one output.
advises is not a function. bob is the first coordinate of two different pairs, (bob, carol) and (bob, dave). Whichever one the loop meets first records seen[bob]; the other then finds bob already in seen with a different output, so the guard seen[a] != b fires and the function returns False. The verdict does not depend on which of bob's pairs comes first: bob advises two students, so the input bob forks either way. This is the difference between a lookup table (a function: ask for bob's institution, get one answer) and a general web of arrows (a relation: ask who bob advises, get a set). Evaluating is_function on each relation agrees:
is_function(affiliated) = True
is_function(advises) = False
Keep the distinction sharp, because the payoff at the end of the chapter is that an embedding is a function, and a function is exactly the kind of relation you can turn into a lookup from a symbol to one vector. As a counting aside, the total functions from People to Institutions number : each of the five people is sent to one of two schools, five independent binary choices. affiliated is one of those thirty-two.
Composition and transitive closure
Two operations turn these static sets into reasoning. The first is composition. To compose two relations is to chain them: follow R from a to some middle b, then follow S from b on to c, and record the shortcut (a, c). Written R ∘ S and read "do R, then S" (some texts write this same chaining S ∘ R, first-relation-last, to line up with function composition where the rightmost map acts first; we use the left-to-right "and-then" order to match the code), it is the set (introducing ∃, read "there exists," for the middle element)
which is exactly compose in sets_relations.py (lines 22–24):
def compose(R: set, S: set) -> set:
"""Relational composition R ∘ S = {(a, c) | (a, b) ∈ R and (b, c) ∈ S}."""
return {(a, c) for (a, b) in R for (b2, c) in S if b == b2}
Compose advises with itself and the shortcuts you get are precisely the grand-advisor links, matching the grandAdvisor rule in kb.py (line 79). Here is the whole computation by hand, one starting pair at a time; for each (a, b) ∈ advises we look for every (b, c) ∈ advises and emit (a, c):
take (a, b) from advises | matching (b, c) in advises | shortcut (a, c) emitted |
|---|---|---|
(alice, bob) | (bob, carol), (bob, dave) | (alice, carol), (alice, dave) |
(bob, carol) | (carol, erin) | (bob, erin) |
(bob, dave) | none (dave advises nobody) | — |
(carol, erin) | none (erin advises nobody) | — |
Three shortcuts survive, and the program prints exactly them:
advises ∘ advises (grand-advising): [('alice', 'carol'), ('alice', 'dave'), ('bob', 'erin')]
alice grand-advises carol and dave (through bob), and bob grand-advises erin (through carol): the pairs advises never contained, manufactured by one round of chaining.
The second operation is the transitive closure: the smallest transitive relation that contains a given one, equivalently "everything reachable by following the arrows one or more hops." Writing for the relation composed with itself times (, , and so on), the closure is the union over all hop-counts,
You cannot literally add up infinitely many terms, but on a finite relation you never have to: once a round adds no new pair, every later round adds none either, so the union stabilizes. That is what transitive_closure does (lines 27–36):
def transitive_closure(R: set) -> set:
"""Smallest transitive relation containing R (reachability). Computed by
repeatedly adding composed pairs until a fixpoint — the same "iterate to a
limit" idea as forward chaining, one relation at a time."""
closure = set(R)
while True:
added = compose(closure, R) - closure
if not added:
return closure
closure |= added
The line added = compose(closure, R) - closure computes exactly the new pairs (one more hop, compose(closure, R), minus what we already have); |= folds them in; the loop halts the round nothing new appears. Trace it on cites, whose two pairs are (p2, p1) and (p3, p2):
| round | closure at start | compose(closure, cites) new pairs | closure after |
|---|---|---|---|
| 1 | {(p2,p1), (p3,p2)} (size 2) | (p3, p2) reaches (p2, p1) ⟹ add (p3, p1) | {(p2,p1), (p3,p1), (p3,p2)} (size 3) |
| 2 | {(p2,p1), (p3,p1), (p3,p2)} (size 3) | every one-hop extension already present ⟹ ∅ | unchanged — fixpoint, return |
In round 1 the only pair whose endpoint feeds another citation is (p3, p2), and p2 cites p1, so (p3, p1) is born. In round 2 nothing new can form (p1 cites nothing, so no chain extends past it), added is empty, and the loop returns. The program prints the fixpoint:
transitive closure of cites: [('p2', 'p1'), ('p3', 'p1'), ('p3', 'p2')]
Two pairs closed to three, the newcomer being ('p3', 'p1'): p3 reaches p1 through p2, even though it never cites it directly. This is precisely what the recursive citesTransitively rule in kb.py (lines 86–88) computes, and the "repeat until nothing new appears" loop is the same iterate-to-a-fixpoint engine that forward chaining runs over the whole knowledge base, where the 23 base facts of kb.py grow in a few waves until the derived facts stop changing. That machinery is the whole subject of the Fixpoints: Reasoning as Reaching a Limit chapter; here it is, already, in ten lines on one relation. Running the module's demonstration block (sets_relations.py, lines 61–67) drives all four results at once, and this is its literal output:
advises ∘ advises (grand-advising): [('alice', 'carol'), ('alice', 'dave'), ('bob', 'erin')]
transitive closure of cites: [('p2', 'p1'), ('p3', 'p1'), ('p3', 'p2')]
affiliated is a function: True
advises is transitive: False
Read those four lines slowly, because they are the chapter in miniature: composition builds the grand-advisor shortcuts, closure builds citation reachability, affiliated passes the single-valued test, and advises fails the transitivity test, the two shape claims we argued by hand, now confirmed by the machine.
Why neuro-symbolic AI stands on this floor
This vocabulary is not a warm-up; it is the shared ground of both halves of the field this series is about. On the symbolic side, a knowledge graph, a database of facts drawn as a labelled network, is a set of relation-triples: every edge is a member of some typed relation, exactly like the pairs of advises. Reasoning over that graph is relation algebra, and it is nothing more than the two operations we just ran. Deriving grand-advisor is composition; finding everything a paper transitively cites is transitive closure. The engines of Part II do this at scale, but they do nothing this chapter has not already named.
On the neural side, an embedding, the bridge to the "neuro" in neuro-symbolic, is a function from symbols to vectors: it sends each individual (alice, p1, mit) to one point in a continuous space, never forking, exactly the single-valued promise we just defined and tested. What makes embeddings interesting, and drives the later volumes, is that we want that function to preserve structure, so that composing relations in the graph corresponds to some tidy operation on the vectors. The recurring target is a translation model in which a relation is a fixed vector and holding of a triple means head plus relation lands near tail; there, the composition advises ∘ advises we computed by set intersection reappears as the addition of two relation vectors. Sets, relations, and functions are thus the common language in which both a logician's proof and a network's vector arithmetic can be written down and compared, the floor both sides stand on.
The unsolved part
Here is the honest crack in the foundation. A relation is crisp: a pair is either in advises or it is not, and membership is or with nothing between, which is exactly why is_function and is_transitive can return a clean Boolean. But much of what we want to reason about is graded and uncertain. Is a half-finished co-supervision an advising link? Should a citation the model is 80% sure exists count as a member of cites? The set-theoretic answer is unforgiving: in or out, no dial. That crispness is a strength for proof, it is what makes transitivity a yes-or-no property computable in a single pass, and a genuine limitation everywhere the world is fuzzy, which is most places.
The neuro-symbolic program can be read as one long attempt to keep the structure of these crisp relations, composition, transitivity, and the function promise, while relaxing membership from a hard into a soft, learnable number in the whole interval between and . Once membership is a real number, though, the tidy theorems wobble: the composition of two "0.8-true" pairs is true to what degree, and does transitivity still close to a unique fixpoint when every arrow carries a weight? Whether the structure that made the symbols useful can survive that relaxation, rather than quietly breaking, is not settled; it is the open question the rest of this series circles.
Why it matters
Every formal object ahead is one of these three in disguise. A logical predicate is a relation; an interpretation that makes a formula true is a set of atoms; a variable assignment is a function from variables to individuals. Lattices will be sets ordered by a special relation; a forward-chaining derivation will be a relation growing to a fixpoint; embeddings and graph neural networks will be functions on the vertices of a relation. If you can see advises as a set of four pairs out of twenty-five, grand-advising as their composition, and affiliated as a single-valued function, then the notation of every chapter that follows is already half-read: the abstractions never get more exotic than this, they only stack higher.
Key terms
- Set — an unordered collection of distinct elements; equality is by extensionality (same members), membership (∈) is all-or-nothing, cardinality counts the members, and the empty set ∅ is the set with none.
- Subset (
A ⊆ B) — ; how "every professor is a person" is stated without naming anyone, and why ∅ is a subset of everything. - Ordered pair
(a, b)— a two-slot object with the characteristic property ; built from sets by , so(alice, bob)differs from(bob, alice). - Cartesian product (
A × B) — the set of all pairs with a first fromAand a second fromB; , soPeople × Peoplehas 25 pairs. - Relation — a subset of a Cartesian product; there are of them on People, and advises is the one specific set of 4 pairs read from the knowledge base.
- Reflexive / symmetric / transitive — for all ; ; ; advises has none of the three, colleague is symmetric.
- Function — a single-valued relation, ; written , ; affiliated qualifies, advises does not since bob advises two students.
- Composition (
R ∘ S) — ; composing advises with itself yields the three grand-advisor pairs. - Transitive closure () — the smallest transitive relation containing , ; reachability, computed by adding shortcuts to a fixpoint (cites closes from 2 pairs to 3, gaining
('p3','p1')). - Knowledge graph — a set of typed relation-triples; the symbolic object reasoning composes and closes.
- Embedding — a function from symbols to vectors, ideally preserving relational structure; the neural bridge that makes this a neuro-symbolic series.
Where this leads
We now have nouns, arrows, and single-valued maps, enough to say that things relate, but not yet to say when one thing outranks another. The next chapter, Order and Lattices: When Things Have Rank, takes a special family of relations, those that are reflexive, transitive, and never loop back, and shows how they impose a hierarchy: greater and lesser, more and less general, a bottom and a top. That ordering machinery is what lets the chain "every professor is a researcher is a person" become a genuine ladder a reasoner can climb, and it is the structure the whole logic of the volume is quietly built on.