Quantitative
Statically-checked physical units with seamless syntax
Install / Use
/learn @propensive/QuantitativeREADME
<img alt="GitHub Workflow" src="https://img.shields.io/github/actions/workflow/status/propensive/quantitative/main.yml?style=for-the-badge" height="24"> <img src="https://img.shields.io/discord/633198088311537684?color=8899f7&label=DISCORD&style=for-the-badge" height="24"> <img src="/doc/images/github.png" valign="middle">
Quantitative
Statically-checked physical units with seamless syntax
When working with physical quantities, such as lengths, masses or temperatures,
it can be easy to mix up quantities with different units, especially if we
represent all quantities with Doubles, which is often necessary for
performance.
Quantitative represents physical quantities with a generic Quantity type, an
opaque alias of Double, which statically encodes the value's units in its
type parameter. This provides all the desirable homogeneity constraints when
combining quantities, with the performance of Doubles, and without
compromising on intuitive syntax for arithmetic operations.
Quantities can be multiplied and divided arbitrarily, with new units computed by the compiler, and checked for consistency in additions and subtractions.
Features
- statically checks that physical quantities have consistent units by making them distinct types
Quantityvalues encode the (nonzero) power of each unit in their type- all
Quantitys are opaque aliases ofDouble, so are stored and processed efficiently - enforces homogeneous units for all additions and subtractions
- calculates resultant units for multiplications and divisions
- unitless values are seamlessly represented by
Doubles - distinguishes between dimensions (such as length or mass) and units (such as metres or feet)
- different units of the same dimension may be combined
- convertions between different units of the same dimension
- requires no new or special syntax
- supports units which are offset from zero, such as degrees Celsius and Fahrenheit
- fully extensible: new units, dimensions and conversions can be introduced
- provides implementations of base and most derived SI units
- represents the seven SI base dimensions (length, mass, time, luminosity, amount of substance, current and temperature) as well as other distinct dimensions, such as angles
Availability
Quantitative 0.1.0 is available as a binary for Scala 3.4.0 and later, from Maven
Central. To include it in an sbt build, use
the coordinates:
libraryDependencies += "dev.soundness" % "quantitative-core" % "0.1.0"
Getting Started
All Quantitative terms and types are defined in the quantitative package,
import quantitative.*
and exported to the soundness package:
import soundness.*
Quantity types
Physical quantities can be represented by different Quantity types, with an appropriate parameter that encodes
the value's units. We can create a quantity by multiplying an existing Double (or any numeric type) by some
unit value, such as Metre or Joule, which are just Quantity values equal to 1.0 of the appropriate unit.
For example:
syntax scala
##
val distance = 58.3*Metre
The types of these values will be inferred. The value distance will get the type Quantity[Metres[1]], since
its value is a number of metres (raised to the power 1).
In general, types representing units are written in the plural (for example, Metres, Feet, Candelas), with
a bias for distinction when the singular name is often used in the plural; for example, the type is Kelvins
even though "Kelvins" and "Kelvin" are both commonly used for plural values. Unit instances are always named in
the singular.
We can compute an area value by squaring the distance,
syntax scala
##
val area = distance*distance
which should have units of square metres (m ²). Quantitative represents this as the type, Quantity[Metres[2]]; the
2 singleton literal value represents the metres being squared. Likewise, a volume would have the parameter
Metres[3].
Representation and displaying
Each quantity, regardless of its units, is represented in the JVM as a Double using an opaque type
alias.
The precise types, representing units, are known statically, but are erased by runtime. Hence, all
dimensionality checking takes place at compiletime, after which, operations on Quantitys will be
operations on Doubles, and will achieve similar performance.
The raw Double value of a Quantity can always be obtained with Quantity#value
Due to this representation, the toString method on Quantitys is the same as Doubles toString,
so the toString representations will show just the raw numerical value, without any units. In
general, toString should not be used. A gossamer.Show instance is provided to produce
human-readable Text values, so calling show on a Quantity will produce much better output.
Derived units
We can also define:
syntax scala
##
val energy = Joule*28000
The type of the energy value could have been defined as Quantity[Joule[1]], but 1 J is equivalent to 1
kgâ‹…m
²â‹…s
¯
², and it's more useful for the type to reflect a product of thes more basic units (even though we
can still use the Joule value to construct it).
Metres, seconds and kilograms are all SI base units. Kilograms are a little different, since nominally, a kilogram is one thousand grams (while a gram is not an SI base unit), and this has a small implication on the way we construct such units.
Quantitative provides general syntax for metric naming conventions, allowing prefixes such as Nano or Mega
to be applied to existing unit values to specify the appropriate scale to the value. Hence, a kilogram value
is written, Kilo(Gram). But since the SI base unit is the kilogram, this and any other multiple of Gram,
such as Micro(Gram), will use the type Kilogram, or more precisely, Kilogram[1].
Therefore, the type of energy is Quantity[Grams[1] & Metres[2] & Second[-2]], using a combination of three
base units raised to different powers. They are combined into an intersection type with the & type operator,
which provides the useful property that the order of the intersection is unimportant;
Second[-2] & Metres[2] & Grams[1] is an identical type, much as kg m
²s
¯
² and s
¯
²m
²kg are identical
units.
Just as we could construct an area by multiplying two lengths, we can compute a new value with appropriate units
by combining, say, area and energy,
syntax scala
##
val volume = distance*distance*distance
val energyDensity = energy/volume
and its type will be inferred with the parameter Kilogram[1] & Metres[-1] & Second[-2].
If we had instead calculated energy/area, whose units do not include metres, the type parameter would be just
Kilogram[1] & Second[-2]; the redundant Metres[0] would be automatically removed from the conjunction.
We can go further. For example, the "SUVAT" equations of motion can be safely implemented as methods, and their dimensionality will be checked at compiletime. For example, the equation,
<math xmlns="http://www.w3.org/1998/Math/MathML"><mi>s</mi><mo>=</mo><mi>u</mi><mi>t</mi><mo>+</mo><mfrac><mn>1</mn><mn>2</mn></mfrac><mi>a</mi><msup><mi>t</mi><mn>2</mn></msup></math>
calculating a distance (s) from an initial velocity (u), acceleration (a) and time (t) can be
implemented using Quantitative Quantitys with,
syntax scala
##
type Velocity = Quantity[Metres[1] & Seconds[-1]]
type Time = Quantity[Seconds[1]]
type Acceleration = Quantity[Metres[1] & Seconds[-2]]
type Distance = Quantity[Metres[1]]
def s(u: Velocity, t: Time, a: Acceleration): Distance =
u*t + 0.5*a*t*t
or more verbosely,
syntax scala
##
def distance
(velocity0: Quantity[Metres[1] & Seconds[-1]],
time: Quantity[Seconds[1]],
acceleration: Quantity[Metres[1] & Seconds[-2]])
: Quantity[Metres[1]] =
velocity0*time + 0.5*acceleration*time*time
While the method arguments have more complex types, the expression, u*t + 0.5*a*t*t, is checked for
dimensional consistency. If we had written t + 0.5*a*t*t or u*t + 0.5*a*a*t instead, these would
have produced errors at compiletime.
Combining mixed units
Kilograms, metres and seconds are units of in the mass, length and time dimensions, which are never interchangeable. Yet we sometimes need to work with different units of the same dimension, such as feet, metres, yards and miles as different (but interchangeable) units of length; or kilograms and pounds, as units of mass.
Each type representing units, such as Metres or Kilograms, must be a subtype of the Units type,
which is parameterized with its power (with a singleton literal integer) and a dimension, i.e. another type
representing the nature of the measurement. For Metres the dimension is Length; for Kilograms it is
Mass; Candela's is Luminosity.
Metres[PowerType] is a subtype of Units[PowerType, Length], where PowerType must be a singleton
integer type. More specifically, Metres[1] would be a subtype of Units[1, Length].
Note that there are no special dimensions for compound units, like energy, since the time, length and mass
components of the units of an energy quantity will be associated with the Second, Metres and Kilogram
types respectively.
Encoding the dimension in the type makes it possible to freely mix different units of the same dimension.
It is possible to create new length or mass units, such as Inch or Pound, which share the Length or Mass
dimensions. This allows them to be considered equivalent in some calculations, if a conversion coefficient is
available.
Quantitative defines a variety of imperial measurements, and will a
