A Scala 3.7+ primer for this codebase
If you know topology cold but Scala 3's newest context-abstraction features are new to you, read this before anything else. TDA4j leans hard on Scala 3.7+ syntax that didn't exist a few years ago, and code samples you find by searching the web (or asking an LLM trained on older Scala) will often look superficially similar but be subtly wrong for this codebase. Every construct below is demonstrated with real code from the current source tree, not a simplified stand-in.
The problem all of this solves
TDA4j wants to write code like "any CellT that has a boundary operation and a total order can be fed
to the homology machinery" — generic over the shape of a capability, not over a fixed class hierarchy.
Scala's traditional answer to this is the typeclass pattern: a trait describing the capability, and
given/implicit instances witnessing that a particular type has it. Scala 3.5+ added a second, newer
way to write typeclasses (sometimes called "type member style" or "modularity" style, following SIP-64,
https://docs.scala-lang.org/sips/sips/typeclasses-syntax.html, and described further at
https://dotty.epfl.ch/docs/reference/experimental/typeclasses.html). TDA4j uses this newer style for its
own typeclasses (RingModule, Field, Cell, OrderedCell, ...) while still using the traditional
Ordering[T]-style parametrized typeclasses for standard-library ones. You need to be fluent in reading
both.
Context bounds, old and new
A context bound like [CellT: Ordering] has existed in Scala for a long time — it's sugar for an
implicit/given parameter (using Ordering[CellT]). Two things changed recently that show up constantly in
this codebase:
Named context bounds (as). Instead of writing a context bound and then separately summoning it, you
can name the bound's evidence inline:
trait OrderedCell extends Cell:
type Self: Ordering as ordering
(Chain.scala.) This says "Self has an Ordering, and I'll call that ordering instance ordering
inside this trait body" — no summon[Ordering[Self]] needed anywhere below. The same pattern appears on
plain type parameters too, e.g. Chain.apply[CellT: Ordering as ord, ...] in Chain.scala's from.
Multiple bounds in one clause. type Self: {Ordering, Filterable} (seen in SimplexStream.scala's
Filtration trait) bounds one type by several typeclasses at once, replacing the older, clunkier
[T: Ordering: Filterable] chained-colon style.
Type is TypeClass: the typeclass-as-type-member pattern
This is the single most important piece of syntax to internalize before reading Chain.scala or
Homology.scala. A typeclass in this style is a trait with an abstract type member called Self,
rather than a type parameter:
trait HasDimension:
type Self
extension (self: Self) def dim: Int
trait Cell extends HasDimension:
type Self
extension (self: Self) def boundary[CoefficientT: Field]: Seq[(Self, CoefficientT)]
trait OrderedCell extends Cell:
type Self: Ordering as ordering
(algebra/Chain.scala, current source — note boundary returns Seq[(Self, CoefficientT)], not a
Chain; more on that in Architecture.)
To say "type T implements Cell," you don't write Cell[T] — you write T is Cell, using the
special infix type alias is that the typeclasses feature provides (roughly infix type is[A, C <: {type
Self}] = C {type Self = A} — a refinement type under the hood, not a real generic parameter). You'll see
this both as a type (Chain[CellT, CoefficientT] is RingModule, a type used all over Homology.scala) and
as a context bound ([CellT: OrderedCell], which desugars to "there exists a given instance of OrderedCell
{ type Self = CellT }" — the compiler treats both spellings, OrderedCell[CellT]-shaped and CellT is
OrderedCell-shaped, as satisfying the same bound).
A concrete instance looks like this — SimplexOrderedCell.scala's actual construction of Simplex[VertexT] is
OrderedCell:
given defaultSimplexIsOrderedCell: [VertexT: Ordering] => (Simplex[VertexT] is OrderedCell) =
simplexIsOrderedCell[VertexT]()
where simplexIsOrderedCell builds an anonymous new (Simplex[VertexT] is OrderedCell): instance,
providing the ordering member OrderedCell requires and, in an extension (spx: Simplex[VertexT])
block, concrete implementations of dim and boundary. Once this given is in scope, any
Simplex[Int] value can call .dim and .boundary[Double] directly, with no explicit typeclass-dictionary
plumbing at the call site — that's the entire payoff of the pattern. One gotcha specific to this
codebase's own package split: a given only comes into scope via a wildcard import that explicitly says
so — import org.appliedtopology.tda4j.cells.{given, *}, not just import
org.appliedtopology.tda4j.cells.*. A plain import pkg.* does not bring given instances into scope
in Scala 3; every file in this codebase that reaches across a subpackage boundary uses the {given, *}
form for exactly this reason — see Architecture's package-layout section.
One syntax detail worth flagging because it trips people up: given [CellT: OrderedCell as oCell] =>
Ordering[CellT] = oCell.ordering (algebra/Chain.scala) is the "anonymous given via arrow" form — a given
with no name, whose value is given after =>, parametrized by a context-bound clause on the left of the
arrow. Read given [bounds] => Body = value as "for any type satisfying [bounds], here is a value of
type Body." This is how the library bridges its own OrderedCell.ordering member back into the
standard-library Ordering[T] typeclass, so anything that's OrderedCell automatically also participates
in ordinary Scala code (.sorted, SortedMap, etc.) that expects a plain Ordering.
Extension methods
extension (self: Self) def dim: Int inside a trait declares an abstract extension method; a concrete
given instance provides the method body in its own extension (...) block. Outside of typeclass bodies,
plain top-level extension blocks are used too, e.g. SimplexOps.scala's entire file is one big
extension [VertexT](spx: Simplex[VertexT]) block delegating most of SortedSet's API (.size, .map,
.filter, .union, ...) to spx.underlying. This is why Simplex "feels like" a SortedSet even though,
as the next section explains, it technically isn't one at runtime.
Opaque types
opaque type Simplex[VertexT] = SortedSet[VertexT]
(Simplex.scala.) Simplex[VertexT] is defined to be SortedSet[VertexT] at the bytecode level — zero
runtime wrapper, zero boxing overhead — but outside Simplex.scala itself, the compiler treats Simplex[VertexT]
and SortedSet[VertexT] as distinct, incompatible types. This buys type safety (you can't accidentally pass
a raw SortedSet[Int] where a Simplex[Int] is expected, so a caller can't build a Simplex bypassing
whatever invariants its constructors enforce) at zero runtime cost. The two extension methods .underlying
and .asSimplex (Simplex.scala:19,21) are the sanctioned crossing points between the two views — every
other file that needs to fall back to raw SortedSet operations goes through .underlying, never an
unsafe cast.
Practical consequence for writing new code: you cannot pattern-match structurally on a Simplex's
contents without going through its extension-method API or the provided unapplySeq (Simplex.from(...)
constructs; Simplex.unapplySeq deconstructs, letting you write case Simplex(a, b, c) => ...).
given/summon resolution: static, not dynamic — and why this matters more than usual here
This is ordinary Scala 3 implicit-resolution behavior, but it is unusually load-bearing in this codebase,
enough that it has caused real, confirmed bugs (see Hard-won invariants for the full story).
The short version: when a given instance's body summons another given (e.g. Chain[CellT,
CoefficientT] is RingModule's implementation needs an Ordering[CellT] in scope to build its internal
SortedMap/PriorityQueue), that inner summon happens once, when the outer given is first
constructed — not fresh on every later call to the outer instance's methods. If you construct a given
too early, before the specific Ordering you actually wanted is in scope, the instance permanently closes
over whatever fallback was in scope at that moment, and no later import or context change will fix it.
This is exactly why every HomologyState in homology/Homology.scala declares given Ordering[CellT] =
stream.filtrationOrdering as its first line, before summoning Chain[CellT,CoefficientT] is RingModule
on the next line — order matters, and it isn't stylistic.
What to read next
Architecture walks the algebraic core and complex-construction layers using the
concepts above; Persistence engines covers the four homology algorithms and
their trust status; Hard-won invariants is the concentrated list of non-obvious rules (like
the given-timing one above) that this codebase depends on and that are easy to violate even once you
know they exist.