Skip to content

SQUIP 1: Generic Type for Underlying Quantity Value

garyKeorkunian edited this page Apr 6, 2017 · 7 revisions

By: Gary Keorkunian

Abstract

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.

Description

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.

Implementation

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.