llattice
A dependency-free Lattice trait for Rust — join (least upper bound) and meet (greatest lower bound) — with built-in implementations for integers, floats, bool, Option, HashSet, and Vec. One shared lattice vocabulary for the whole family.
What is llattice?
A lattice is a partially-ordered set in which every pair of elements has both a least upper bound and a greatest lower bound. llattice distills that idea to its smallest useful form: a single trait with two methods.
join— the least upper bound (supremum,⊔): the smallest element that is≥both inputs. For numbers,max; for sets,∪(union).meet— the greatest lower bound (infimum,⊓): the largest element that is≤both inputs. For numbers,min; for sets,∩(intersection).
use Lattice;
assert_eq!; // least upper bound = max
assert_eq!; // greatest lower bound = min
llattice is a leaf crate: it has zero dependencies and imports nothing beyond std. It was extracted from libdictenstein to break a dependency cycle, and now serves as the shared lattice vocabulary for the libdictenstein / liblevenshtein / lling-llang / duallity / libgrammstein family. Because the trait lives in one small crate everyone depends on, a HashSet<T> produced in one crate and merged in another agree on what join means — no orphan-rule gymnastics, no re-derivation, no diamond conflicts.
Terminology.
⊑is the partial order ("approximates" / "is below").⊔is join;⊓is meet. A join-semilattice has only⊔(every pair has a supremum); a meet-semilattice has only⊓; a lattice has both. The greatest element (if any) is ⊤ ("top"), the least is ⊥ ("bottom").
The lattice laws
join and meet are not arbitrary binary operations — they obey four laws that make them genuine lattice operations. The built-in impls satisfy all of them (where the element type's own == is well-behaved).
| Law | Join form | Meet form |
|---|---|---|
| Idempotency | a ⊔ a = a |
a ⊓ a = a |
| Commutativity | a ⊔ b = b ⊔ a |
a ⊓ b = b ⊓ a |
| Associativity | (a ⊔ b) ⊔ c = a ⊔ (b ⊔ c) |
(a ⊓ b) ⊓ c = a ⊓ (b ⊓ c) |
| Absorption | a ⊔ (a ⊓ b) = a |
a ⊓ (a ⊔ b) = a |
These laws are not decoration — they are exactly what makes lattice merge safe under reordering and duplication:
- Idempotency ⟹ applying the same update twice is a no-op (
a ⊔ a = a). At-least-once delivery is safe. - Commutativity ⟹ the order two updates arrive in does not matter (
a ⊔ b = b ⊔ a). - Associativity ⟹ how you parenthesize a batch of merges does not matter.
Together these three give the property CRDTs rely on: any fold of any multiset of states, in any order, with any grouping, yields the same result.
The order and the operations are two views of one structure. Define the partial order from join:
a ⊑ b ⟺ a ⊔ b = b (b is an upper bound of a, so a is "below" b)
a ⊑ b ⟺ a ⊓ b = a (the dual reading, via meet)
So for u32, a ⊑ b is just a ≤ b; for HashSet, a ⊑ b is a ⊆ b. join climbs the order toward ⊤; meet descends toward ⊥.
A worked example: the powerset lattice of {1, 2, 3}
The subsets of {1, 2, 3}, ordered by ⊆, form a lattice where join = ∪ and meet = ∩. Its Hasse diagram (edges = "covered by", drawn upward) is the canonical picture of a lattice — and it is exactly what HashSet<i32> computes:
use Lattice;
use HashSet;
let s12: = .into_iter.collect;
let s23: = .into_iter.collect;
// Following the diagram upward to the least common ancestor:
assert_eq!; // ⊔ = ∪ = {1,2,3}
// …and downward to the greatest common descendant:
assert_eq!; // ⊓ = ∩ = {2}
⊥ is {} (the empty set), ⊤ is {1, 2, 3}. Every pair has a unique supremum and infimum — the defining property of a lattice.
Built-in implementations
Implementing Lattice requires Clone + Send + Sync. The crate ships these blanket and concrete impls:
| Type | join (⊔, supremum) |
meet (⊓, infimum) |
Order ⊑ |
Bounds |
|---|---|---|---|---|
u8 … u128, usize, i8 … i128, isize |
max |
min |
≤ |
MIN / MAX |
f32, f64 |
f::max |
f::min |
≤ |
±∞ |
bool |
‖ (logical OR) |
&& (logical AND) |
false ≤ true |
false / true |
Option<T: Lattice> |
Some if either is Some |
Some only if both are Some |
None ⊑ Some(_) |
None = ⊥ |
HashSet<T: Eq + Hash> |
∪ (union) |
∩ (intersection) |
⊆ |
{} = ⊥ |
Vec<T: Eq> |
concat + dedup (order-preserving) | intersection (order-preserving) | (see note) | [] = ⊥ |
Notes on the structural impls:
Option<T>is the "lifted" lattice.joinkeeps a value if either side has one (Noneacts as bottom —None ⊔ x = x);meetrequires both sides to be present (None ⊓ x = None). When both areSome, it recurses:Some(a) ⊔ Some(b) = Some(a ⊔ b). This is the standard way to adjoin a fresh⊥to any latticeT.Vec<T>is the deduplicating, order-preserving view.joinappends elements of the right operand not already present ([3,1,2] ⊔ [4,2,1] = [3,1,2,4]);meetkeeps the left operand's elements that also appear in the right, in the left's order ([3,1,2] ⊓ [4,2,1] = [1,2]). Treat it as a set-with-insertion-order, not a free monoid: idempotency and commutativity hold up to set-equality of contents, but the ordering ofjoinis left-biased.
use Lattice;
// bool — the two-element lattice ⊥=false ⊑ ⊤=true
assert_eq!; // OR
assert_eq!; // AND
// Option<T> — None is bottom; join fills in, meet requires both
assert_eq!;
assert_eq!;
assert_eq!; // recurses: max(5,3)
assert_eq!; // recurses: min(5,3)
Quick start
[]
= "0.1"
The entire public surface is one trait and one import:
use Lattice;
use HashSet;
// HashSet: join = union, meet = intersection
let a: = .into_iter.collect;
let b: = .into_iter.collect;
assert_eq!;
assert_eq!;
// Numeric: join = max, meet = min
assert_eq!;
assert_eq!;
Implement it for your own type by saying what "combine upward" and "combine downward" mean:
use Lattice;
/// A monotone version counter that only ever advances.
;
assert_eq!;
Use cases
CRDT-style merges & state-based replication
A join-semilattice is the algebraic backbone of a state-based CvRDT (Convergent Replicated Data Type). In Shapiro et al.'s formulation, each replica holds a value drawn from a join-semilattice, and the merge of two replica states is their join. Because ⊔ is idempotent, commutative, and associative, replicas that have seen the same set of updates — regardless of order, duplication, or batching — converge to the identical state. No coordination, no conflict resolution, no consensus round-trip:
use Lattice;
use HashSet;
// Three replicas of a grow-only set (G-Set), each having seen different updates:
let r1: = .into_iter.collect;
let r2: = .into_iter.collect;
let r3: = .into_iter.collect;
// Merge in ANY order — the result is always the full set (convergence):
let merged = r1.join.join;
assert_eq!;
assert_eq!; // order-independent
The same shape powers a last-writer-wins register (join over (timestamp, value) pairs), a version vector (join = element-wise max, i.e. Vec/map of counters), or a monotone clock (join = max). Each is just a different Lattice instance over the same two-method contract.
Monotone state & fixpoints
Lattices also model monotone computation: dataflow analyses, abstract interpretation, and Datalog-style fixpoint iteration all advance a value monotonically up a lattice (x ⊑ f(x)) until it stops changing. llattice gives those engines a uniform join to accumulate facts and a uniform meet to intersect constraints.
Relationship to semirings
There is a precise bridge between lattices and idempotent semirings. In a semiring (S, ⊕, ⊗, 0̄, 1̄) whose addition is idempotent (a ⊕ a = a), the ⊕ operation is automatically a join: it is commutative, associative, idempotent, and induces the natural order a ⊑ b ⟺ a ⊕ b = b. So every idempotent semiring carries a join-semilattice for free.
The converse does not hold blindly: a semiring's ⊗ (multiplication) is generally path composition, not lattice meet — in a weighted-automaton (WFST) setting, ⊗ concatenates edge weights along a path while ⊕ selects the best among alternative paths. Conflating ⊗ with ⊓ would be a category error.
For that reason the semiring ↔ lattice bridge lives in lling-llang, not here. lling-llang owns the IdempotentSemiring types and provides the Lattice impl that exposes their ⊕ as join. llattice deliberately stays a pure, dependency-free leaf so that crates needing only the lattice vocabulary never pull in semiring machinery.
Why a shared crate (not a local trait)?
Rust's orphan rule means a trait can only be implemented for a foreign type by the crate that defines the trait or the crate that defines the type. If each member of the family declared its own Lattice, then libdictenstein's HashSet lattice and lling-llang's HashSet lattice would be different, incompatible traits — a value could not flow between them, and any crate depending on both would face a diamond. Extracting Lattice into a single zero-dependency leaf crate gives the whole family one canonical trait, one set of blanket impls, and one source of truth for what join/meet mean.
References
- Birkhoff, G. Lattice Theory (3rd ed., 1967). American Mathematical Society Colloquium Publications, Vol. 25. 10.1090/coll/025
- Davey, B. A., & Priestley, H. A. (2002). Introduction to Lattices and Order (2nd ed.). Cambridge University Press. 10.1017/CBO9780511809088
- Shapiro, M., Preguiça, N., Baquero, C., & Zawirski, M. (2011). Conflict-Free Replicated Data Types. In Stabilization, Safety, and Security of Distributed Systems (SSS 2011), LNCS 6976, pp. 386–400. Springer. 10.1007/978-3-642-24550-3_29
License
Licensed under Apache-2.0. Minimum supported Rust version: 1.70.
Part of the vinary-tree family:
libdictenstein ·
liblevenshtein ·
lling-llang ·
duallity