cardinality

cardinality

Build development cycles that reduce complexity.

A type is a state space. Objects and values fit into types, with simple rules.

Unit 1 · Boolean 2 · Option[Boolean] 3

Long 264 · String ω · (String, String) ω2

Map[Char, Boolean] 3216 · Char => Boolean 2216.¹

Sums add, products multiply, functions are exponentials. Every value a type admits is a state the program must handle — or can wrongly produce. The calculator puts a number on that space inside your build, so each change can be judged by whether it shrank the space or grew it.

The loop

Measure

Add the plugin to project/plugins.sbt:

addSbtPlugin("dev.constructive" % "sbt-cardinality" % "0.0.6")

and run it:

sbt cardinalityReport
scala-cardinality — 1 source, 10 definitions
  stored-value estimates: constructor inputs only; finite capacities are upper bounds
  ?: unresolved, not a proof of infinity
  2 unresolved · 4 with more than one value · 2 with one value · 2 abstract
Stored-value estimates (constructor inputs; `?` = unresolved)

?  example.Holder[A]  class     Light.scala:11  unresolved: A
ω  example.Succ       class     Light.scala:17
ω  example.Timeline   class     Light.scala:19
2  example.Custom     class     Light.scala:5
1  example.Zero       object    Light.scala:16
—  example.Light      abstract  Light.scala:3

Every row is a definition, its number, and the reason when the number is not exact. The same report is written to target/cardinality/report.txt, so a build can keep it, diff it, or post it as an artifact.

Narrow

Replace the type, not the test:

The number falls, and the compiler keeps it fallen: a state the type cannot represent cannot be built, tested, or hallucinated.

Enforce

Diff report.txt in review and CI. A change that widens a type shows the new number next to the old one; a change that narrows it is the improvement, in one line. The reading a report page has the row anatomy and the moves that lower a number.

What the loop buys

Ergonomics

For one type, with no build plugin:

Counter.`type`(dialects.Scala3("Either[Boolean, Option[Boolean]]").parse[Type].get)
// TinySize(5)

For a source, Counter.source sums the value spaces of every definition it introduces; Counter.sourceSignature sums what their members declare. See counting methods for the other number a report gives: the implementations a signature admits.

Current version: 0.0.6.

Further Reading


¹ The report prints finite capacities as bounds (≤265536) and keeps products above the finite tier coarse ((String, String) prints ω). The arithmetic above is the target; a bound is not a licence to stop looking.