Skip to content

dimensional (Haskell)

The venerable statically-typed units library of the Haskell ecosystem: physical quantities as Quantity d a where d is a type-level vector of seven integer exponents over the fixed SI basis, with all dimension algebra performed by GHC's closed type families — no compiler extension, no plugin, stock GHC only.

FieldValue
LanguageHaskell (GHC only; DataKinds, closed TypeFamilies, TypeOperators, KindSignatures)
LicenseBSD3
Repositorybjornbm/dimensional
DocumentationHackage haddocks · README.md (the haddock header of Numeric.Units.Dimensional is the de-facto manual)
Key authorsBjörn Buckwalter (author/maintainer, 2006–); Douglas McClean (co-designer of the 1.0 DataKinds rewrite, per Gundry 2015 fn. 20)
CategoryStatically-typed units library (library-only; contrast the plugin and compiler-native approaches)
MechanismPhantom Dimension-kinded type index + closed type families over type-level integers (numtype-dk)
Exponent domainℤ⁷ — integer exponents over a fixed basis of the 7 SI base dimensions; no rationals, no user-added generators
Checking timeCompile time (opt-in runtime checking via Numeric.Units.Dimensional.Dynamic)
Analyzed versionf759f32 (2026-01-01; local clone $REPOS/haskell/dimensional, pinned in the survey's grounding table)
Latest release1.6.2 (January 2026, "Support for GHC 9.14" — CHANGELOG.md)

NOTE

dimensional is the closed-type-family data point of this survey's Haskell trio: it encodes Kennedy-style dimension algebra using only stock GHC features, where uom-plugin extends GHC's constraint solver with a true abelian-group unifier and F# builds one into the compiler. The second half of this page contrasts it with Richard Eisenberg's units package — the other type-family library, which trades dimensional's fixed SI basis for user-extensible dimensions and a "locally coherent system of units" generalization. The mechanism theory behind both is in type-system mechanisms; the cross-system synthesis is in the comparison capstone.


Overview

What it solves

dimensional makes GHC's type checker verify dimensional consistency of numeric code. The haddock header of the core module states the contract (src/Numeric/Units/Dimensional.hs):

"In this module we provide data types for performing arithmetic with physical quantities and units. Information about the physical dimensions of the quantities/units is embedded in their types and the validity of operations is verified by the type checker at compile time. The wrapping and unwrapping of numerical values as quantities is done by multiplication and division of units, of which an incomplete set is provided."

That last sentence is the library's signature idiom: there are no dimensioned literals. A quantity is formed by multiplying a number by a unit with *~, and a number is recovered by dividing a quantity by a unit with /~ — mirroring the metrological definition of a quantity as numerical value × unit (the VIM/SI framing collected in concepts):

haskell
-- dimensional: src/Numeric/Units/Dimensional.hs
(*~) :: (Num a) => a -> Unit m d a -> Quantity d a
x *~ (Unit _ _ y) = Quantity (x Prelude.* y)

(/~) :: Fractional a => Quantity d a -> Unit m d a -> a
(Quantity x) /~ (Unit _ _ y) = (x Prelude./ y)

The package is among the oldest living systems in this survey: the copyright line runs from 2006 (Dimensional.hs), and version 1.0 (2015-11) rewrote the original functional-dependency encoding onto DataKinds and closed type families (CHANGELOG.md: "Changed to DataKinds and ClosedTypeFamilies encoding of dimensions" — the rewrite Gundry's paper cites as dimensional-dk). The dimensional.cabal tested-with list spans GHC 8.10.7 through 9.14.1 — two decades of maintenance on one design.

Design philosophy

Three commitments, each stated in the library's own documentation.

Newtonian scope, SI discipline. The haddock header draws the physics boundary and the standards allegiance in two sentences (Dimensional.hs):

"We limit ourselves to "Newtonian" physics. We do not attempt to accommodate relativistic physics in which e.g. addition of length and time would be valid."

"As far as possible and/or practical the conventions and guidelines of NIST's 'Guide for the Use of the International System of Units (SI)' [1] are followed."

The NIST guide (SP 811, a grounding source of this survey's concepts page) is cited section-by-section throughout the source — SIUnits.hs walks its tables, and deviations are explained inline.

Self-documenting client code over clever inference. The README.md positions the encoding choice as an ergonomics decision:

"Data kinds and closed type families provide a flexible, safe, and discoverable implementation that leads to largely self-documenting client code."

Client signatures read as physics — escapeVelocity :: (Floating a) => Mass a -> Length a -> Velocity a — because every common dimension has a named Quantity synonym. What the library deliberately does not chase is complete unit polymorphism: the haddock concedes that "we could provide the 'Mul' and 'Div' classes with full functional dependencies" but that "Efforts are underway to develop a type-checker plugin that does enable these scenarios" (Dimensional.hs $dimension-arithmetic) — the gap uom-plugin was later built to fill.

Exactness where it is free. Every Unit carries its conversion factor to the SI coherent unit as an ExactPi — an exact rational multiple of a power of π from the exact-pi companion package — so degree↔radian and inch↔metre chains stay exact until a value is demanded at an approximate numeric type.


How it works

One data family, two variants: Quantity and Unit

The central type is a data family indexed by a promoted Variant kind, giving units and quantities different runtime representations behind one set of operators (src/Numeric/Units/Dimensional/Internal.hs):

haskell
-- dimensional: src/Numeric/Units/Dimensional/Internal.hs
class KnownVariant (v :: Variant) where
  data Dimensional v :: Dimension -> Type -> Type
  ...

instance KnownVariant ('DQuantity s) where
  newtype Dimensional ('DQuantity s) d a = Quantity a   -- bare newtype: erased
  ...
  dmap = coerce

instance (Typeable m) => KnownVariant ('DUnit m) where
  data Dimensional ('DUnit m) d a = Unit !(UnitName m) !ExactPi !a  -- runtime record
  ...

type Unit (m :: Metricality) = Dimensional ('DUnit m)
type Quantity = SQuantity E.One
type SQuantity s = Dimensional ('DQuantity s)

A Quantity d a is a newtype around a — the dimension is purely phantom. A Unit m d a is a genuine runtime value: a structured UnitName (with UCUM interchange names), the exact ExactPi factor to the SI coherent unit, and that factor pre-approximated at type a. The haddock explains why the two share a type (Dimensional.hs $types): "to allow code reuse as they are largely subject to the same operations" and to permit "reuse of operators (and functions) between the two without resorting to occasionally cumbersome type classes."

The Metricality index ('Metric / 'NonMetric) is a second phantom that gates SI-prefix application (see Extensibility); the s in SQuantity s is a type-level ExactPi' scale factor used by the fixed-point module (see Expressiveness edges).

ConceptType / itemRole
QuantityQuantity d a (= SQuantity E.One d a)newtype around a; dimension d phantom; stored in SI coherent unit
UnitUnit m d aname + exact SI factor (ExactPi) + approximated factor a
Dimension (type level)'Dim l m t i th n j of kind Dimension7-vector of type-level integer exponents
ExponentsTypeInt (from numtype-dk)'Neg1, 'Zero, 'Pos2, … with type families +, -, *, /
Dimension (term level)Dimension' (Dim' Int Int Int Int Int Int Int)for Dynamic quantities, parsing, pretty-printing
Metricality'Metric / 'NonMetriccompile-time gate on SI-prefix application
Formation / elimination*~, /~ (and *~~, /~~ over functors)number × unit → quantity; quantity ÷ unit → number
Unit constructionmkUnitZ / mkUnitQ / mkUnitR, siUnit, onenew named units as integer/rational/real multiples of existing ones
Runtime escape hatchAnyQuantity a, DynQuantity a (Dynamic)dimension carried as a term-level Dimension', checked at runtime

Arithmetic: types computed by closed type families

Multiplication and division are defined once for both variants; their result dimension is computed by type families, and their result variant too — the product of two units is a 'NonMetric unit, the product of two quantities is a quantity (Dimensional.hs, Variants.hs):

haskell
-- dimensional: src/Numeric/Units/Dimensional.hs
(*) :: (KnownVariant v1, KnownVariant v2, KnownVariant (v1 V.* v2), Num a)
    => Dimensional v1 d1 a -> Dimensional v2 d2 a -> Dimensional (v1 V.* v2) (d1 * d2) a

(+) :: Num a => Quantity d a -> Quantity d a -> Quantity d a
(+) = liftQ2 (Prelude.+)

(^) :: (Fractional a, KnownTypeInt i, KnownVariant v, KnownVariant (Weaken v))
    => Dimensional v d1 a -> Proxy i -> Dimensional (Weaken v) (d1 ^ i) a

Addition needs no dimension arithmetic at all — both operands must have the samed, enforced by ordinary nominal type equality. Powers take the exponent as a Proxy to a type-level integer (x ^ pos2), because the exponent changes the result type. The worked example from the haddock header shows the idiom end to end (Dimensional.hs):

haskell
-- dimensional: src/Numeric/Units/Dimensional.hs (haddock header example)
escapeVelocity :: (Floating a) => Mass a -> Length a -> Velocity a
escapeVelocity m r = sqrt (two * g * m / r)
  where
      two = 2 *~ one
      g = 6.6720e-11 *~ (newton * meter ^ pos2 / kilo gram ^ pos2)

sqrt is dimensionally typed too — sqrt :: Floating a => Quantity d a -> Quantity (Sqrt d) a — and Sqrt (DEnergy * DLength / DMass / DLength) reduces to DVelocity by type-family evaluation. A Show instance renders any quantity in the SI coherent unit derived from its dimension — the doctest sum [12.4 *~ meter, 1 *~ foot] prints 12.7048 m — and showIn (mile / hour) renders in a chosen unit (Dimensional.hs, Internal.hs).


Dimension representation

The dimension is a promoted seven-field tuple — one field per SI base dimension, in the order length, mass, time, current, temperature, amount of substance, luminous intensity (src/Numeric/Units/Dimensional/Dimensions/TypeLevel.hs):

haskell
-- dimensional: src/Numeric/Units/Dimensional/Dimensions/TypeLevel.hs
data Dimension = Dim TypeInt TypeInt TypeInt TypeInt TypeInt TypeInt TypeInt

type DOne      = 'Dim 'Zero 'Zero 'Zero 'Zero 'Zero 'Zero 'Zero
type DLength   = 'Dim 'Pos1 'Zero 'Zero 'Zero 'Zero 'Zero 'Zero
type DTime     = 'Dim 'Zero 'Zero 'Pos1 'Zero 'Zero 'Zero 'Zero

type family (a :: Dimension) * (b :: Dimension) where
  DOne * d = d
  d * DOne = d
  ('Dim l  m  t  i  th  n  j) * ('Dim l' m' t' i' th' n' j')
    = 'Dim (l + l') (m + m') (t + t') (i + i') (th + th') (n + n') (j + j')

This is the free abelian group ℤ⁷ on a fixed seven-generator basis — the concrete instance of the free-abelian-group representation theory, with group multiplication as component-wise exponent addition. Three properties follow:

  • Exponents are integers, supplied by the numtype-dk companion package as a TypeInt kind ('Neg1/'Zero/'Pos2/…) with its own type-level +, -, *, / families. The restriction is deliberate (TypeLevel.hs, comment above ^): "We limit ourselves to integer powers of Dimensionals as fractional powers make little physical sense." The NRoot d x family divides each exponent by x via numtype-dk's partial type-level division, which is stuck (no matching equation) when the division is inexact — so sqrt of a non-square dimension is a compile error, and exponents are unrepresentable (contrast mp-units' rational exponents and F#'s RationalPower).
  • The representation is a normal form by construction. Because a dimension is its exponent vector, kg·m and m·kg are not two type expressions to be proved equal — both reduce to the same 'Dim tuple. dimensional therefore never needs the normalization constraints that plague the units package (below); equality of ground dimensions is ordinary ~.
  • The basis is closed. There is no type-class or type-family hook through which a client can add an eighth generator. This is the page's headline finding — expanded under Extensibility.

Every type-level dimension has a term-level twin — Dimension' with strict Int fields (TermLevel.hs) — reachable via KnownDimension/dimension, which is what the Dynamic module, the Show instance, and unit-name machinery operate on. The single-kind design is itself argued for in the haddock (Dimensional.hs $dimensions): providing "type variables for the seven base dimensions in 'Dimensional'" instead "would have made any type signatures involving 'Dimensional' very cumbersome."

Checking & inference

There is no unification-level dimension algebra: checking is GHC's ordinary nominal equality plus forward reduction of the closed type families above. That split determines exactly what works and what doesn't.

Ground dimensions: complete and silent. Any concrete dimension expression — DEnergy * DLength / DMass / DLength — reduces to a unique 'Dim vector, so mismatches reduce to a mismatch of two literal tuples and additions type-check without any constraint machinery. Decidability is inherited from closed-type-family termination (each family is a structurally terminating rewrite).

Variables: stuck, by design of GHC. For an opaque d, d * d cannot reduce — type families "may pattern match only on constructors, not other type families" and the abelian-group axioms "are hardly going to" form a terminating rewrite system (Gundry 2015 §2.2, the analysis that motivated the plugin). Dimensional polymorphism is therefore expressible only up to syntactic spelling of the stuck family application:

haskell
-- Expressible: the signature spells the result as the literal family application.
-- (Derived from the family definitions in Dimensions/TypeLevel.hs @ f759f32;
--  not compiled locally for this survey.)
sqr :: Num a => Quantity d a -> Quantity (d * d) a
sqr x = x * x

-- Rejected: GHC cannot prove  d * d ~ d ^ 'Pos2  for an opaque d.
-- Both sides are stuck family applications; there is no AG unifier to relate them.
sqr' :: Num a => Quantity d a -> Quantity (d ^ 'Pos2) a
sqr' x = x * x

The same wall blocks commutativity (u * v ~ v * u is unprovable for variables), "backwards" inference (from d1 * d2 and d1, GHC will not solve for d2 — the haddock names type-checker plugins as the way to get this, "e.g. for linear algebra"), and any signature whose author normalized the algebra differently than the type families do. In Kennedy's terms the system checks the free abelian group's word problem on closed terms but has no equational theory for open ones — the defining limitation of the closed-type-family row in this survey's mechanism comparison. Where the F# compiler inferssqr : float<'u> -> float<'u ^ 2> from an unannotated body, dimensional requires the annotation and requires it in normal form.

In practice the library leans on monomorphic-by-synonym style — signatures like Mass a -> Length a -> Velocity a are ground, so the sharp edge cuts only authors of generic dimensional combinators (vector spaces, linear algebra over quantities — exactly the use cases the haddock defers to plugins).

Extensibility

New units: cheap, safe, and exact. A unit is defined as a multiple of an existing unit with mkUnitZ (integer factor), mkUnitQ (rational), or mkUnitR (real/ExactPi), carrying a structured name with a UCUM interchange code (src/Numeric/Units/Dimensional/NonSI.hs):

haskell
-- dimensional: src/Numeric/Units/Dimensional/NonSI.hs
foot :: Fractional a => Unit 'NonMetric DLength a
foot = mkUnitQ (ucum "[ft_i]" "ft" "foot") (1 Prelude./ 3) $ yard

inch :: Fractional a => Unit 'NonMetric DLength a
inch = mkUnitQ (ucum "[in_i]" "in" "inch") (1 Prelude./ 12) $ foot

Zero factors are rejected at construction because — in the library's own words — "the library relies upon units forming a group under multiplication" (Dimensional.hs, mkUnitR haddock; the group structure is the free-abelian-group story again, this time on the unit side).

Prefixes: a compile-time gate. SI prefixes are functions, not string glue, and their type consumes 'Metric and produces 'NonMetric (SIUnits.hs):

haskell
-- dimensional: src/Numeric/Units/Dimensional/SIUnits.hs
kilo :: Num a => Unit 'Metric d a -> Unit 'NonMetric d a

So kilo (kilo meter) and kilo (meter / second) are type errors — composite units are 'NonMetric by the Variants.hs product family — statically enforcing NIST SP 811's prohibitions on compound (§6.2.4) and stand-alone (§6.2.6, the section cited in the source) prefixes.

New base dimensions: impossible. This is the finding the brief flags. The Dimension kind is a closed seven-field tuple; there is no class, family, or open kind through which client code can add a generator. A codebase that wants to track currency, information (bits), or angle as an independent dimension has exactly three bad options: hijack an unused SI slot, encode it as Dimensionless, or fork the library. Likewise there is no notion of alternative unit systems: every Unit's exactValue is defined "expressed in terms of the SI coherent derived unit … of the same 'Dimension'" (Dimensional.hs), so CGS or natural units exist only as conversion factors into SI, and every Quantity is stored in the SI coherent basis. Both restrictions are precisely what Eisenberg's units package was designed to lift — and mp-units, Boost.Units, and uom (Rust) all chose the open-basis road as well.

Expressiveness edges

  • Fractional powers — absent, deliberately. NRoot/Sqrt/Cbrt reduce only when every exponent divides; sqrt (x :: Frequency Double) does not type-check, so V/√Hz-style noise densities are unrepresentable. The exponent domain is , full stop (TypeLevel.hs).
  • Affine quantities (temperature) — conversion functions, not types. The library documents the problem and its 80% answer (SIUnits.hs$celsius): "A problematic area is units which increase proportionally to the base SI units but cross zero at a different point." degreeCelsius is literally kelvin (a relative/interval unit), and absolute temperatures go through fromDegreeCelsiusAbsolute / toDegreeCelsiusAbsolute, which hard-code the 273.15 offset. There is no affine/point type: adding two absolute temperatures — the classic error a torsor representation rules out — type-checks fine. (Contrast Pint's delta units and mp-units' quantity_point.)
  • Logarithmic units (dB, neper) — absent, documented. NonSI.hs: "The units of section 5.1.2 are purposefully (but not permanently) omitted. In fact the logarithmic units (see section 8.7) are problematic and it is not clear how to implement them. Perhaps with a conversion function similar to for degrees Celsius." Still open at the analyzed pin — matching Gundry's frontier list in type-system mechanisms.
  • Angles — dimensionless, with the known consequences.type DPlaneAngle = DOne; type PlaneAngle = Dimensionless (Quantities.hs) — the orthodox SI reading. radian-vs-degree scaling is handled (units carry factors; sin/cos take Dimensionless), but nothing stops adding an angle to a pure ratio, and Hz vs rad/s is untrackable.
  • Kind vs dimension — not modelled. The library aliases, rather than distinguishes, same-dimension quantities: type DTorque = DMomentOfForce with DMomentOfForce = DEnergy, and type DActivity = DFrequency (becquerel vs hertz) (Quantities.hs). Torque added to energy type-checks. The synonyms document intent without enforcing it — the gap that mp-units' quantity_spec hierarchy and uom (Rust)'s Kind tag were invented to close.
  • What it has that most rivals lack: exact conversion factors (ExactPi arithmetic keeps π-rational chains exact through unit composition); a fixed-point/scaled-quantity layerSQuantity s d a gives quantities an extra type-level ExactPi' scale factor, with FixedPoint.hs providing Angle8/Angle16/Angle32 binary-angle synonyms and rescale machinery for integer representations; and a dynamic tier (Dynamic.hs) where AnyQuantity/DynQuantity carry the dimension as a term-level Dimension' value, support arithmetic that propagates invalidity, and promoteQuantity re-enters the static world with a runtime check — a principled static↔dynamic bridge few statically-typed systems in this survey offer.

Zero-cost story

For quantities, erasure is structural and visible in the source rather than asserted in prose (Internal.hs):

haskell
-- dimensional: src/Numeric/Units/Dimensional/Internal.hs
newtype Dimensional ('DQuantity s) d a = Quantity a      -- the representation
liftQ2 :: (a -> a -> a) -> SQuantity s1 d1 a -> SQuantity s2 d2 a -> SQuantity s3 d3 a
liftQ2 = coerce                                          -- +, -, abs, … are coerce

instance Storable a => Storable (SQuantity s d a) where
  sizeOf _ = sizeOf (undefined :: a)                     -- exactly the payload size
  alignment _ = alignment (undefined :: a)
  poke ptr = poke (castPtr ptr :: Ptr a) . coerce

newtype instance U.Vector (SQuantity s d a)    =  V_Quantity {unVQ :: U.Vector a}
newtype instance U.MVector v (SQuantity s d a) = MV_Quantity {unMVQ :: U.MVector v a}

A Quantity DVelocity Double is a Double at runtime: the arithmetic wrappers are coerce (dmap = coerce, liftQ/liftQ2 = coerce), the Storable instance is a pointer cast with INLINE pragmas, and an unboxed Vector of quantities is defined as an unboxed Vector of the payload — no boxing, no per-element tags. Quantity formation costs one multiplication (x *~ u multiplies by the unit's pre-approximated factor at construction; thereafter values live in the SI coherent unit and +/- are raw machine ops).

Units are deliberately not free: Unit is a strict three-field record (name, ExactPi, factor). The design collapses all unit-ness into the boundary operators *~//~, after which only erased quantities flow. The repository ships a criterion harness comparing raw Double pipelines against the dimensional equivalents (benchmarks/Main.hsrawArithmetic vs arithmetic over 1000-element lists), but commits no result numbers; the honest statement is that the erasure evidence is the newtype/coerce/Storable definitions above, not a published benchmark. No erasure theorem exists for this encoding either — Kennedy proved parametricity for his calculus and F# asserts erasure normatively, but the type-family encodings have no analogous mechanized result (see the open problems in type-system mechanisms).

Diagnostics

dimensional's own haddock documents the error for adding metres to seconds — the best case, where GHC's expected/actual types surface the library's synonyms (src/Numeric/Units/Dimensional.hs L98–110 @ f759f32; provenance: repo documentation — the library's recorded GHC output, per this survey's rung-2 ladder; not re-captured locally):

text
let x = 1 *~ meter + 1 *~ second

Couldn't match type 'Numeric.NumType.DK.Integers.Zero
               with 'Numeric.NumType.DK.Integers.Pos1
Expected type: Unit 'Metric DLength a
  Actual type: Unit 'Metric DTime a
In the second argument of `(*~)', namely `second'
In the second argument of `(+)', namely `1 *~ second'

Two things are characteristic. First, the mismatch is reported at the exponent level — the leading line says a type-level 'Zero isn't 'Pos1 (the length components of the two 'Dim vectors) before the readable Unit 'Metric DLength a / Unit 'Metric DTime a pair appears. Second, because addition fixed the expected type from the left operand, the error lands on the second unit inside 1 *~ second — accurate, but one step removed from the + that is morally at fault. The header immediately shows the less friendly case (Dimensional.hs L112–124, same provenance):

text
let x = 1 *~ meter / (1 *~ second) + 1 *~ kilo gram

Couldn't match type 'Numeric.NumType.DK.Integers.Zero
               with 'Numeric.NumType.DK.Integers.Neg1
Expected type: Quantity DMass a
  Actual type: Dimensional
                 ('DQuantity V.* 'DQuantity) (DLength / DTime) a
In the first argument of `(+)', namely `1 *~ meter / (1 *~ second)'
In the expression: 1 *~ meter / (1 *~ second) + 1 *~ kilo gram
In an equation for `x':
      x = 1 *~ meter / (1 *~ second) + 1 *~ kilo gram

When the offending side is a computed dimension, internal machinery leaks: the Dimensional ('DQuantity V.* 'DQuantity) (DLength / DTime) a spelling exposes the variant-product family instead of reducing to Quantity DVelocity a. (The block predates the scale-factor parameter added to 'DQuantity, so the exact spelling has drifted cosmetically since; the shape — internal families in the "actual type" — is the durable point.) The author's own verdict, in the same header:

"It is the author's experience that the usefulness of the compiler error messages is more often than not limited to pinpointing the location of errors."

That is still a materially better story than the units package's normal-form errors quoted in the contrast below — and materially worse than uom-plugin's 'm' ~ 's'-level messages or F#'s The unit of measure 'm' does not match the unit of measure 's'.

Ergonomics & compile-time cost

  • Prelude replacement. Client modules enable NoImplicitPrelude and import Numeric.Units.Dimensional.Prelude, which "re-exports the 'Prelude', hiding arithmetic functions whose names collide with the dimensionally-typed versions"*, /, +, -, sqrt, sum, pi, the trigonometric family, all shadowed. This is the largest single adoption cost: dimensional code lives in a different arithmetic dialect, and mixing plain and dimensioned math in one module means qualified Prelude imports (P.*). The haddock also advises NegativeLiterals (Dimensional.hs).
  • Declaration overhead is low for the SI-shaped 99%: quantities are formed with *~ and named synonyms (Velocity Double), and scores of dimension/quantity synonyms — the NIST-guide tables plus extras — ship in Quantities.hs. Exponents need value-level proxies (x ^ pos2, nroot pos3) — mild noise unique to the type-level-integer encoding. Convenience constants _1_9, pi, tau cover dimensionless literals.
  • Error readability is bimodal, as the previous section shows: synonym-level when the mismatch is between named ground dimensions, internals-level when a computed dimension or a polymorphic context is involved.
  • Compile-time cost: no measured data. Neither the repository nor its docs publish compile-time figures (this survey found none in the clone @ f759f32). Structurally the encoding is frugal — every dimension is a 7-tuple whose family reductions are constant-size, with none of the quadratic normalization of factor lists (units) or plugin solver passes (uom-plugin) — and the tested-with matrix (GHC 8.10.7 → 9.14.1, dimensional.cabal) shows the maintenance burden of tracking nine GHC majors is being paid.

The units package contrast: user-extensible dimensions

Richard Eisenberg's units (with Takayuki Muranushi; pinned locally @ c06d560, units 2.4.1.5 + units-defs 2.2.1) is the other type-family units library — the one Gundry 2015 §5.2 calls "the state of the art as far as units of measure in Haskell are concerned" before demonstrating why a plugin beats it. It differs from dimensional on exactly the two axes this page flagged as findings.

Open dimension basis. Where dimensional hard-codes seven generators, units is "completely agnostic to the actual system of units used" (README.md): the engine package defines only Dimensionless/Number, and even SI is just the bundled units-defs library. A base dimension is any type with a Dimension instance, and a quantity's index is a type-level association list of factors rather than a fixed-width vector (units/Data/Metrology/Qu.hs, Factor.hs):

haskell
-- units: units/Data/Metrology/Qu.hs
newtype Qu (a :: [Factor *]) (lcsu :: LCSU *) (n :: *) = Qu n

-- units: units/Data/Metrology/Factor.hs
data Factor star = F star Z          -- a dimension (or unit) with a Z exponent

so the group is the free abelian group over an open set of generators — [F LengthDim One, F TimeDim MOne] — with unary type-level integers (Z.hs). Declaring a new basis element is three lines (or one Template-Haskell call, declareDimension) (units/README.md):

haskell
-- units: units/README.md — a user-defined base dimension and two units
data LengthDim = LengthDim
instance Dimension LengthDim

data Meter = Meter
instance Unit Meter where
  type BaseUnit Meter = Canonical      -- Meter is the canonical unit of LengthDim
  type DimOfUnit Meter = LengthDim

data Foot = Foot
instance Unit Foot where
  type BaseUnit Foot = Meter           -- inter-convertible with Meter
  conversionRatio _ = 0.3048

The payoff shows immediately in units-defs, which does what dimensionalcannot: it declares PlaneAngle and SolidAngle as fundamental dimensions, on the record that "It would be wrong to divide 2 meters by 1 meter and conclude that the quantity is 2 radians or degrees" (units-defs/Data/Dimensions/SI.hs), and ships a CGS module alongside SI.

The coherent-unit-system generalization. dimensional stores every quantity in the SI coherent unit; units abstracts the storage basis into a type-level map from dimensions to units — the LCSU, locally coherent system of units:

haskell
-- units: units/README.md
type MyLCSU = MkLCSU '[(LengthDim, Meter), (TimeDim, Second)]

Within one LCSU no conversions ever happen; conversions occur only at the % / # boundaries or between LCSUs. Gundry's summary of why this matters: it lets code "be typechecked for dimension safety, but remain polymorphic in the particular units, and makes it easier to avoid numeric overflow" (Gundry 2015 §5.2) — e.g. an astrophysics simulation can store lengths in parsecs without accumulating 3.086e16-scale factors, something structurally impossible in dimensional.

Why its inference is weaker than a plugin's. The price of the open basis is that the factor-list index is not a canonical form — [F Length 1, F Time -1] and [F Time -1, F Length 1] are different types. units therefore compares indices up to a normalization type family: addition constrains its operands with d1 @~ d2, defined as (Factor.hs):

haskell
-- units: units/Data/Metrology/Factor.hs
type family (a :: [Factor *]) @~ (b :: [Factor *]) :: Constraint where
  a @~ b = (Normalize (a @- b) ~ '[])

Gundry's §2.2.1 dissects the consequences. Errors are phrased in normal forms — adding a mass to a length yields, verbatim (quoted from gundry-2015-typechecker-plugin-uom-haskell.pdf §2.2.1, a local artifact of this survey):

text
Couldn't match
    type '[F Mass One, F Length (P Zero)]'
    with '[]'
In the expression: mass |+| distance

— the user must decode "the normalized quotient of your two dimensions is not the empty list" where dimensional said DLength vs DTime. And polymorphism degrades the same way it does in dimensional, but harder, because even ground equalities now route through Normalize: the normalisation approach, Gundry writes, "breaks down when there are variables or other non-canonical unit expressions present. It cannot conclude that [u * v] is interchangeable with [v * u], because it cannot compute the normal form of variables such as u and v" (operators rendered in ASCII). A two-argument polymorphic function picks up an inferred type whose context is a wall of Normalize/Reorder/@@+ applications (reproduced in full in §2.2.1). units papers over the worst of it with redim — a compile-time "dimension-safe cast" that re-normalizes an index against a signature (units/README.md: "When providing type annotations, it is good practice to start your function with a redim $") — a manual crank for equalities a real abelian-group unifier discharges silently. That unifier is exactly what uom-plugin adds to GHC and what F# has natively; the three-way trade (dimensional: canonical-but-closed basis; units: open basis, normal-form constraints; plugin: open basis and full AG unification, at the cost of a compiler extension) is the closed-type-family section of type-system mechanisms in miniature.

NOTE

Which to reach for. Within Haskell, dimensional is the conservative choice: stock GHC, SI-only, ground-type ergonomics, two decades of releases. units buys extensible dimensions and unit-polymorphic storage at the price of gnarlier errors and redim ceremony. uom-plugin buys real inference at the price of a GHC-version-coupled plugin. The survey's comparison page plays the three against the non-Haskell systems.


Strengths

  • Erased quantities, visibly. newtype + coerce + payload-sized Storable + newtype-deriving unboxed Vector instances: the zero-cost claim is inspectable in ~30 lines of Internal.hs.
  • Canonical representation ⇒ trivial ground checking. The 'Dim 7-vector is its own normal form; no normalization constraints, no redim, no plugin — plain ~ equality, on any stock GHC from 8.10 to 9.14.
  • Exactness discipline. ExactPi conversion factors and the mkUnitZ/mkUnitQ/mkUnitR hierarchy keep unit chains exact (1 *~ inch :: Length Rational is 127 % 5000 m, per the NonSI.hs doctests).
  • Metricality gate. Double prefixes and prefixed composite units are compile errors — a NIST SP 811 rule made structural.
  • Static↔dynamic bridge. AnyQuantity/DynQuantity + promoteQuantity give a checked runtime tier for parsing/serialization, term-mirrored by Dimension'.
  • Scaled/fixed-point quantities. The SQuantity s d a scale parameter and FixedPoint.hs support integer and binary-angle representations — rare among the systems in this survey.
  • Longevity. 2006–2026 under one maintainer, with the 1.0 encoding migration (fundeps → DataKinds) executed without abandoning the API.

Weaknesses

  • Closed 7-dimension basis. No custom base dimensions — currency, information, angle-as-dimension are unrepresentable; no alternative unit systems beyond conversion factors into SI. The single deepest limitation, and the reason the units package exists.
  • No kind discipline. Torque = energy, becquerel = hertz, angle = 1 — aliases document, nothing enforces (Quantities.hs).
  • Integer exponents only. NRoot rejects inexact roots at compile time — sound but restrictive; no domain for noise densities or fracture mechanics.
  • Polymorphism at the syntactic mercy of type families. Quantity (d * d) works; proving d * d ~ d ^ 'Pos2 doesn't; no backwards inference. Generic dimensional libraries need uom-plugin or heavy annotation.
  • Affine and logarithmic blind spots. Celsius via offset functions (absolute temperature addition type-checks); dB/neper explicitly unimplemented.
  • Prelude shadowing. NoImplicitPrelude + operator redefinition is a real adoption barrier and complicates mixed dimensioned/plain arithmetic.
  • Error messages leak internals (Numeric.NumType.DK.Integers.Zero, 'DQuantity V.* spellings) whenever the mismatch isn't between two named ground synonyms — per the author's own assessment, useful mainly for locating errors.

Key design decisions and trade-offs

DecisionRationaleTrade-off
Fixed 7-generator SI basis in one Dimension kindSignatures stay compact (per-dimension type variables would be "very cumbersome"); representation is canonicalNo user-defined base dimensions or unit systems; angle/kind distinctions unrepresentable
Closed type families for the group algebraStock GHC, decidable, silent on ground dimensionsStuck on variables: no AC reasoning, no backwards inference; polymorphic signatures must spell normal forms
Integer exponents (numtype-dk TypeInt)"Fractional powers make little physical sense"; NRoot statically checks divisibilityNo exponents (contrast mp-units, F# RationalPower)
One Dimensional data family for Quantity and UnitOperator reuse across variants; quantities erase while units carry names + exact factorsIntimidating operator signatures (KnownVariant, V.* families) that surface in error messages
Values stored in the SI coherent unit; units act only at *~//~Arithmetic after construction is raw machine ops; one multiplication at the boundaryStorage basis is unchangeable — no LCSU-style unit-polymorphic representation (contrast units)
ExactPi conversion factors + mkUnitZ/Q/R hierarchyExact π-rational unit chains; doctest-verifiable exact conversionsUnit is a runtime record; extra dependency surface (exact-pi, numtype-dk)
Prefixes as 'Metric'NonMetric functionsNIST §6.2.4/§6.2.6 made structural; double/composite prefixing rejected at compile timeOccasionally over-strict; unit-name machinery (UnitNames, UCUM codes) is a large ancillary subsystem
Prelude-shadowing Numeric.Units.Dimensional.PreludeClient code reads as ordinary arithmetic on quantitiesNoImplicitPrelude everywhere; qualified imports for mixed plain/dimensioned math

Sources