-
Notifications
You must be signed in to change notification settings - Fork 122
SQUIP 1: Generic Type for Underlying Quantity Value
By: Gary Keorkunian
This SQUIP describes a proposed change to the implementation of Quantity values. The proposed change aims to replace Double as the type for a Quantities underlying value with a Generic type. While Double is perhaps the type that provides the best performance, it does not offer the best precision in fractional operations which are quite common in dimensional analysis.
Currently, Squants uses a Double to store the underlying value of a Quantity.
val length: Length = Kilometers(100.0)
length.value.isInstanceOf[Double] should be(true)
The goal is to allow users to select the value type within their code. This will likely be achieved by adding a type parameter to each Quantity and it's Units. Users can choose from any type that supports "numeric" style operations.
val lengthDouble: Length[Double] = Kilometers(100.0)
length.value.isInstanceOf[Double] should be(true)
val lengthBigDecimal: Length[BigDecimal] = Kilometers(BigDecimal(100.0))
length.value.isInstanceOf[BigDecimal] should be(true)
Type inference should allow existing code, which uses Double, to continue working as expected.
The implementation required to solve what this SQUIP attempts to address is currently being researched. Type Classes will likely be part of the solution. Some pseudocode:
// Define the Type Class root trait
trait SquantsNumeric[N] {
// define numeric operations required by Squants
def plus(x: N, y: N)
def times(x: N, y: N)
...
}
// Define Type Classes for specific types
trait SquantsDouble extends SquantsNumeric[Double] {
// implement operations for a Double
def plus(x: Double, y: Double) = x + y
...
}
trait SquantsBigDecimal extends SquantsNumeric[BigDecimal] {
// implement operations for a BigDecimal
def plus(x: BigDecimal, y: BigDecimal) = x + y
}
trait SquantsSpireRational extends SquantsNumeric[Rational] {
// implement operation for a spire.Rational
def plus(x: Rational, y: Rational) = x + y
}
...
// Quantity is updated to accept generic with SquantsNumeric Type Class
abstract class Quantity[A <: Quantity[A, N], N](implicit val num: SquantsNumeric[N]) { self A: =>
def value: N
def unit: UnitOfMeasure[Quantity[A, _]]
// apply numeric operations using the Type Class
def plus(that: A) = unit(num.add(this.value, that.value))
}
...
// Specific types are updated to accept generic types with defined Type Class
class Length[N](val value: N, val unit: LengthUnit)(implicit num: SquantsNumeric[N]) extends Quantity[N, Length] {
// apply numeric operation using the Type Class
def *(that: Length[N]): Area = SquareMeters(num.times(this.toMeters, that.toMeters))
...
}
...
// User code
val lengthD = Kilometers(10.0) // creates Length[Double] == 10.0 km
val lengthB = Kilometers(BigDecimal(10.0)) // creates Length[BigDecimal] == 10.0 km
val lengthR = Kilometers(r"10.0") // creates Length[Rational] = 10.0 km
lengthD.toMeters // returns 10000.0d
lengthB.toMeters // returns BigDecimal(10000.0)
lengthR.toMeters // return r"10000.0"
// The following would clearly be OK
val sum = Meters(10.0) + Meters(10.0) // returns Length[Double] == 20.0 m
// However, to support this
val sum = Meters(10.0) + Meters(r"10.0") // returns Length[Double] = 20.0 m
// or
val sum = Meters(r"10.0") + Meters(10.0) // returns Length[Rational] = 20.0 m
// will require a conversion between from one type to another.