SkillAgentSearch skills...

Quantitative

Statically-checked physical units with seamless syntax

Install / Use

/learn @propensive/Quantitative

README

<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
  • Quantity values encode the (nonzero) power of each unit in their type
  • all Quantitys are opaque aliases of Double, 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

View on GitHub
GitHub Stars6
CategoryDevelopment
Updated5mo ago
Forks0

Languages

Scala

Security Score

72/100

Audited on Oct 15, 2025

No findings