A complete multiset is a finite-support signed count function. An absent key has count zero; negative counts are allowed. One simple default implementation uses a protected immutable association list with unique keys and nonzero values. Its default is empty, and optimized subclasses may override the public queries.
trait CompleteMultiSet[T] {
// Representation contract: finite, immutable, unique keys, nonzero counts.
protected def entries: List[(T, Int)] = Nil
def count(t: T): Int =
entries.find { case (key, _) => key == t }.map(_._2).getOrElse(0)
lazy val present: Seq[T] = entries.map(_._1)
private def combine(other: CompleteMultiSet[T],
op: (Int, Int) => Int): CompleteMultiSet[T] = {
val keys = (present.toList ++ other.present.toList).distinct
val resultRows = keys.flatMap { t =>
val n = op(count(t), other.count(t))
if (n == 0) Nil else List((t, n))
}
new CompleteMultiSet[T] {
override protected val entries: List[(T, Int)] = resultRows
}
}
def +(other: CompleteMultiSet[T]): CompleteMultiSet[T] =
combine(other, (a, b) => java.lang.Math.addExact(a, b))
def -(other: CompleteMultiSet[T]): CompleteMultiSet[T] =
combine(other, (a, b) => java.lang.Math.subtractExact(a, b))
def max(other: CompleteMultiSet[T]): CompleteMultiSet[T] =
combine(other, (a, b) => math.max(a, b))
def min(other: CompleteMultiSet[T]): CompleteMultiSet[T] =
combine(other, (a, b) => math.min(a, b))
def <=(other: CompleteMultiSet[T]): Boolean =
(present.toList ++ other.present.toList).distinct.forall { t =>
count(t) <= other.count(t)
}
override def toString: String =
present.distinct.filter(t => count(t) != 0)
.map(t => s"$t:${count(t)}").mkString("{", ", ", "}")
}The representation invariant and abstraction function assign the association-list value at a stored key and zero elsewhere. Every operation preserves both operands and returns a fresh immutable value. The finite-support pointwise operations principle makes the support union sufficient because every operation sends to zero. It is necessary for signed counts: the empty value is not at most a value having count at one key, although the empty left support alone would give a vacuous true comparison. Likewise, pointwise minimum can retain a negative count appearing only in the other operand.
present == List() represents the all-zero, empty complete multiset. The list-backed default count takes time for a first-entry hit or an empty list, and in the worst case for a missing or final key, assuming constant-cost key equality. The default binary operations can consequently take quadratic time; they are deliberately simple defaults, not optimized implementations. The lazy present avoids an early call to a subclass's representation during initialization.Counts use
Int, so addition and subtraction explicitly throw ArithmeticException when the mathematical result is unrepresentable instead of silently wrapping. Equivalently, their mathematical precondition requires representable result counts. This machine-size restriction is separate from permitting negative counts.Keep a private immutable hash-map value in a mutable variable. Each update computes a complete replacement from snapshots and commits it once; this handles self-aliasing and arithmetic failure without partially modifying the receiver.
class MultiSetVarImp[T] extends MultiSetVar[T] {
private var table = Map.empty[T, Int]
def count(t: T): Int = table.getOrElse(t, 0)
def present: Seq[T] = table.keys.toVector
def copy(other: CompleteMultiSet[T]): Unit = {
val saved = other.present.distinct.iterator
.map(t => (t, other.count(t))).filter(_._2 != 0).toMap
table = saved
}
private def snapshot(other: MultiSetVar[T]): Map[T, Int] =
other.present.distinct.iterator
.map(t => (t, other.count(t))).filter(_._2 != 0).toMap
private def update(other: MultiSetVar[T], op: (Int, Int) => Int): Unit = {
val left = table
val right = snapshot(other)
val keys = left.keySet ++ right.keySet
val builder = Map.newBuilder[T, Int]
keys.foreach { t =>
val n = op(left.getOrElse(t, 0), right.getOrElse(t, 0))
if (n != 0) builder += ((t, n))
}
val replacement = builder.result()
table = replacement
}
def +(other: MultiSetVar[T]): Unit =
update(other, (a, b) => java.lang.Math.addExact(a, b))
def -(other: MultiSetVar[T]): Unit =
update(other, (a, b) => java.lang.Math.subtractExact(a, b))
def max(other: MultiSetVar[T]): Unit =
update(other, (a, b) => math.max(a, b))
def min(other: MultiSetVar[T]): Unit =
update(other, (a, b) => math.min(a, b))
def <=(other: CompleteMultiSet[T]): Boolean = {
val left = table
(left.keySet ++ other.present).forall { t =>
left.getOrElse(t, 0) <= other.count(t)
}
}
}The representation invariant is a finite map of nonzero counts with no representation exposed to callers. The abstraction function adds implicit zero elsewhere.
getOrElse implements that default; keys enumerates stored keys; distinct removes repeated input support entries; toMap builds an independent immutable map; and the builder accumulates the next representation before it is assigned. The private immutable snapshots remain stable while the new state is computed.For self-addition, both snapshots contain the same old counts, so they double; self-subtraction yields empty support; self-max and self-min leave the value unchanged. A distinct operand is never assigned or mutated. An overflow exception during building occurs before the final assignment, so the receiver remains unchanged. These are instances of snapshot-before-update semantics.
Lookup is expected under standard hash assumptions. Enumerating
present takes for support size . Combining two instances of this implementation takes expected time and temporary storage, including snapshot construction. For an arbitrary argument implementation, add the costs of its count calls: copying a default list-backed complete multiset can require , whereas copying the hash-backed version is expected linear. No generic interface can guarantee another implementation's query cost. Inputs are assumed to have finite, complete support enumerations and stable counts during an operation; concurrent mutation would need a separate synchronization contract.Inside the supplied
PaymentsHash class, scan until a matching node or the end of the chain. A null return means the user has no record in this bucket.private def find(user: Int, head: PaymentsHash.Node): PaymentsHash.Node = {
var p = head
while (p != null && p.user != user) p = p.next
p
}The loop invariant says that every node already passed has a different user identity and
p heads the unexamined suffix. Short-circuit evaluation of && prevents dereferencing null. The representation invariant supplies an acyclic finite chain, so each unsuccessful step shortens that suffix and proves algorithm termination. The code changes no node fields.For each table index , let be the set of non-null nodes reachable by following
next. A sufficient representation invariant is:- The array has exactly buckets, and every bucket heads a finite acyclic linked list.
- The sets are pairwise disjoint, so one physical node cannot belong to two buckets.
- Every reachable node has a valid user identity, lies in bucket
hash(node.user), and has both counters between zero andInt.MaxValue. - User identities are unique among all reachable nodes, not merely within a single chain.
The abstraction function maps these nodes to the total functions of part (a): if the unique node for exists, and are its fields; otherwise both are zero. A stored record with two zero counters is harmless and has the same observable meaning as an absent record. This intentionally permits multiple concrete representations of the same abstract program state.
Initially every bucket is null, so the representation invariant holds and the abstraction function yields all zeros. The successful-update branch preserves every structural condition; the insertion branch creates a fresh node with a previously absent identity in its correct bucket. Both branches realize exactly the incoming-increment postcondition, demonstrating correct operations through an abstraction function.
Let denote the non-null nodes reachable from the head. The representation invariant requires a finite acyclic null-terminated linked list and pairwise distinct names among its reachable nodes. Names and numbers retain the interface's string data type interpretation; no ordering of names is required.
The abstraction function is the finite partial functionName uniqueness makes this a function, rather than a relation assigning multiple numbers to one name. Initially
list == null, so and the abstract book is empty. Unreachable allocated nodes are outside this abstract state.A sufficient representation invariant is:
The abstraction function isThe last two conditions ensure each representative is a genuine member of its own block and that the abstract function is idempotent function. Cells beyond
used are unconstrained and ignored. When used == 0, these conditions hold vacuously and the abstraction function yields the empty map. This is an explicit-key version of quick-find, rather than a parent-pointer disjoint-set forest. Protected access 2026-10-24
Protected access permits designated subclass code to use a member while excluding ordinary clients. The precise receiver and scope restrictions depend on the language; it should not be confused with public access to every instance's representation. Access control helps protect representation invariants but does not ensure that authorized subclass code maintains them.
Representation exposure 2026-10-13
Representation exposure gives outside code access that can modify storage on which the representation invariant or promised behavior depends. Returning the internal array exposes a route around the API; merely hiding the field name does not close it. Incoming shared mutable storage can create the same problem. Copies or immutable values can prevent such modification, but their costs and sharing rules belong in the design.[ref]
Reading 13: Abstraction Functions & Rep Invariants. Reading 13, Invariants, Immutability: mutable aliases through both returned values and constructor arguments
The bounded queue with encapsulation deliberately stores immutable labels. A FIFO queue of mutable objects needs a separate promise about whether changing a returned object changes stored content; protecting the queue's ordering and size does not automatically make every stored object immutable.
Representation independence 2026-10-13
Representation independence means that clients restricted to the promised API cannot distinguish implementations by the specified results and state transitions. State what observations count: equal returned labels and failure statuses do not imply equal running times, allocations or physical layouts. If the API also promises a resource bound, a replacement must meet that bound too.
Replace the compact array in the bounded queue with encapsulation by a circular buffer with storage , front index and count . Its representation invariant is , , and . Its abstraction function reads the occupied slots using modular arithmetic:On successful
enqueue(x), write and increment . On successful dequeue(), return , replace by and decrement . Full and empty failures do not alter storage. peek and size have the same abstract meanings as before. Keeping a count distinguishes a full circular buffer from an empty one even when both would have the same next insertion index.For , start empty, enqueue , dequeue twice, enqueue , try to enqueue , then
peek and size. Both implementations return from the removals, report full for , and finish with front , size three and abstract sequence . The compact array stores from index zero; the circular buffer stores with . Their abstraction functions explain why different stored entries describe the same ordered content.Under constant-cost slot access and index arithmetic, with no resizing and labels that fit in one slot, the compact array does slot moves on a successful removal: worst-case time complexity as capacity varies. The circular buffer does no slot moves and uses worst-case time complexity for all four operations. Both allocate slots initially; that construction cost is separate. These are operation-count claims in Big O notation, not measured timings. The short correctness outline checks that each implementation's abstraction function produces the specified new sequence and return value after every successful or rejected call. Repeating that check gives equal traces for clients using only this API.
Representation invariant Created 2026-10-13 Updated 2026-10-24
A representation invariant specifies which concrete storage states are valid at the API boundary. Construction must establish it, and each completed operation must preserve it. Internal steps may temporarily violate it only while that state is inaccessible to observers; callbacks or concurrent access would require further reasoning.[ref]
Reading 13: Abstraction Functions & Rep Invariants. Reading 13, How to Establish Invariants: creation, preservation and absence of representation exposure
The short proof pattern is mathematical induction on the number of completed calls: the initial state satisfies ; every permitted transition from an -state ends in an -state; hence every state observable through the API satisfies . An unrestricted external write falls outside this argument.
Sentinel node 2026-10-24
A sentinel is a permanently present linked list node that is excluded from the represented data. A header sentinel provides a predecessor even for the first real element, allowing uniform link updates. Its datum has no abstract meaning and must not be counted by the abstraction function. A representation invariant should keep the sentinel outside its own finite acyclic successor chain.
Shared-array double stack Created 2026-10-24 Updated 2026-10-31
Sparse linked vector representation 2026-10-31
Represent a finite sparse vector by a sorted doubly linked list of its nonzero coordinates. A list header stores the dimension; every ordinary linked list node stores a distinct valid coordinate index, its nonzero value, and predecessor and successor references. In a circular linked list, the endpoints connect to the header, and an all-zero vector has the header linked to itself in both directions. The representation invariant requires strictly increasing ordinary indices, valid bounds and mutually consistent forward and backward links. The abstraction function assigns zero to every coordinate absent from the list. The header is metadata, never an ordinary coordinate.
For example, has dimension nine and only three stored coordinates. The header's index field can store nine even though nine is not a valid coordinate index. Keeping the header distinct is therefore essential when searching for or removing entries.
A circular sparse linked vector representation stores only nonzero coordinates
. Details. Stackable trait modification 2026-10-24
A Scala trait can intercept a method, call the next implementation through
super, and add behavior before or after it. abstract override marks such an implementation as requiring an appropriate concrete method later in the class linearization. A character-counting plugin can update its cached counts only after a successful underlying document edit, preserving its additional representation invariant without changing the edit's abstract result. Type guard 2026-10-31
An operation checking that a value has a requested runtime extension type and exposing the fields of that type. In Oberon, a pointer guard such as
p(RealTail) permits a pointer to a base record to access the real-valued field of an appropriate record extension. Failure of the dynamic type check is a runtime error, so a representation invariant should establish that guarded values have the requested extension before the operation is used. Unreduced rational representation 2026-10-14
Consider an original abstract data type whose values are rational numbers. Store an ordered pair of integers, interpreted as a numerator and a denominator. Use exact, unbounded integer arithmetic in this model. The storage space, representation invariant and abstraction function areThe representation invariant does not require a positive denominator or a reduced fraction. Consequently , and are three different concrete states with the same abstract rational number . Similarly, and both represent zero. The abstraction function is a surjection: every rational number has an integer numerator and a nonzero integer denominator, so every abstract value has a representation.
Both and violate the representation invariant; neither has a value under this abstraction function. By contrast, is completely valid. Simplifying it to the reduced fraction may be a useful implementation choice, but is not a requirement of this representation. Strengthening the representation invariant to require a positive denominator and a greatest common divisor of one would choose a unique stored representative for each rational number; every constructor and operation would then need to establish that stronger condition.
Equality of abstract values is therefore different from equality of stored ordered pairs. For valid and ,This defines an equivalence relation on : each equivalence class contains all representations of one rational number. An API equality operation can compare these cross-products using exact integer arithmetic. Comparing the two fields individually would incorrectly declare and unequal as rational numbers. The general reason is that an abstraction function may map several concrete values to one abstract value.[ref]
8.3 Module Documentation. Section 8.3.1, Abstraction Functions: several concrete values mapping to one abstract value; Section 8.3.4, Representation Invariants: valid domain of AF


