coulomb: a statically typed unit analysis library for Scala

Build Status Scala Steward badge Join the chat at https://gitter.im/erikerlandson/community

Documentation

Scala Requirements

The coulomb libraries currently require scala 2.13.0, or higher.

How to include coulomb in your project

The core coulomb package can be included by adding the dependencies shown below. Note that its two 3rd-party dependencies -- spire and singleton-ops -- are %Provided, and so you must also include them, if your project does not already do so. The shapeless package is also a dependency, but is included transitively via spire.

libraryDependencies ++= Seq(
  "com.manyangled" %% "coulomb" % "0.5.0",
  "org.typelevel" %% "spire" % "0.17.0-RC1",
  "eu.timepit" %% "singleton-ops" % "0.5.0"
)

The coulomb project also provides a selection of predefined units, which are available as separate sub-packages.

libraryDependencies ++= Seq(
  "com.manyangled" %% "coulomb-si-units" % "0.5.0",        // The seven SI units: meter, second, kilogram, etc
  "com.manyangled" %% "coulomb-accepted-units" % "0.5.0",  // Common non-SI metric: liter, centimeter, gram, etc
  "com.manyangled" %% "coulomb-time-units" % "0.5.0",      // minute, hour, day, week
  "com.manyangled" %% "coulomb-info-units" % "0.5.0",      // bit, byte, nat
  "com.manyangled" %% "coulomb-mks-units" % "0.5.0",       // MKS units: Joule, Newton, Watt, Volt, etc
  "com.manyangled" %% "coulomb-customary-units" % "0.5.0", // non-metric units: foot, mile, pound, gallon, pint, etc
  "com.manyangled" %% "coulomb-temp-units" % "0.5.0"       // Celsius and Fahrenheit temperature scales
)

Other coulomb packages

In addition to core functionality and fundamental units, coulomb provides the following packages.

scala.js support

The core coulomb package and several other sub-packages are cross-published to scala.js

Code of Conduct

The coulomb project supports the Scala Code of Conduct; all contributors are expected to respect this code. Any violations of this code of conduct should be reported to the author.

Tutorial

Table of Contents

Running Tutorial Examples

Except where otherwise noted, the following tutorial examples can be run in a scala REPL as follows:

% cd /path/to/scala
% sbt coulomb_tests/console
scala> import shapeless._, coulomb._, coulomb.si._, coulomb.siprefix._, coulomb.mks._, coulomb.time._, coulomb.info._, coulomb.binprefix._, coulomb.accepted._, coulomb.us._, coulomb.temp._, coulomb.define._, coulomb.parser._

Examples making use of numeric quantity operations depend on corresponding typeclasses for numeric algebras. Alebras for the common scala and spire numeric types can be obtained this way:

scala> import spire.std.any._     // import algebras for common numeric types
scala> import spire.std.double._  // import algebras for Double

Features

The coulomb libraries provide the following features:

Allow a programmer to associate unit analysis with values, in the form of static types

val length = 10.withUnit[Meter]
val duration = (30.0).withUnit[Second]
val mass = Quantity[Float, Kilogram](100)

Express those types with arbitrary and natural static type expressions

val speed = (100.0).withUnit[(Kilo %* Meter) %/ Hour]
val acceleration = (9.8).withUnit[Meter %/ (Second %^ 2)]

Let the compiler determine which unit expressions are equivalent (aka convertable) and transparently convert between them

val mps: Quantity[Double, Meter %/ Second] = (60.0).withUnit[Mile %/ Hour]

Cause a compile-time error when operations are attempted with non-convertable unit types

val mps: Quantity[Double, Meter %/ Second] = (60.0).withUnit[Mile] // compile-time type error!

Automatically determine correct output unit types for operations on unit quantities

val mps: Quantity[Double, Meter %/ Second] = 60D.withUnit[Mile] / 1D.withUnit[Hour]

Allow a programmer to easily declare new units that will work seamlessly with existing units

// a new unit of length:
trait Smoot
implicit val defineUnitSmoot = DerivedUnit[Smoot, Inch](67, name = "Smoot", abbv = "Smt")

// a unit of acceleration:
trait EarthGravity
implicit val defineUnitEG = DerivedUnit[EarthGravity, Meter %/ (Second %^ 2)](9.8, abbv = "g")

Quantity and Unit Expressions

coulomb defines the class Quantity for representing values with associated units. Quantities are represented by their two type parameters: A value type N (typically a numeric type such as Int or Double) and a unit type U which represents the unit associated with the value. Here are some simple declarations of Quantity objects:

import coulomb._
import coulomb.si._

val length = 10.withUnit[Meter]            // An Int value of meters
val duration = (30.0).withUnit[Second]     // a Double value in seconds
val mass = Quantity[Float, Kilogram](100)  // a Float value in kg

Three operator types can be used for building more complex unit types: %*, %/, and %^.

import coulomb._
import coulomb.si._

val area = 100.withUnit[Meter %* Meter]   // unit product
val speed = 10.withUnit[Meter %/ Second]  // unit ratio
val volume = 50.withUnit[Meter %^ 3]      // unit power

Using these operators, units can be composed into unit type expressions of arbitrary complexity.

val acceleration = (9.8).withUnit[Meter %/ (Second %^ 2)]
val ohms = (0.01).withUnit[(Kilogram %* (Meter %^ 2)) %/ ((Second %^ 3) %* (Ampere %^ 2))]

Quantity Values

The internal representation type of a Quantity is given by its first type parameter. Each quantity's value is accessible via the value field

import coulomb._, coulomb.info._, coulomb.siprefix._

val memory = 100.withUnit[Giga %* Byte]  // type is: Quantity[Int, Giga %* Byte]
val raw: Int = memory.value              // memory's raw integer value

Standard Scala types Float, Double, Int and Long are supported, as well as any other numeric type N for which spire algebra typeclasses are defined, for example BigDecimal or spire Rational. Algebra typeclasses for standard value types may be imported via import spire.std.any._

Operations on coulomb Quantity objects require only the typeclasses they need to operate. If you wish to work with operations that do not require algebras, then these typeclasses do not need to exist:

scala> import coulomb._, coulomb.si._

scala> case class Foo(foo: String)  // no algebras are defined for this type
defined class Foo

scala> Foo("goo").withUnit[Meter].show  // 'show' requires no algebra typeclass
res0: String = Foo(goo) m

String representations

The show method can be used to obtain a human-readable string that represents a quantity's type and value using standard unit abbreviations. The showFull method uses full unit names. The methods showUnit and showUnitFull output only the unit without the value.

scala> val bandwidth = 10.withUnit[(Giga %* Bit) %/ Second]
bandwidth: coulomb.Quantity[...] = coulomb.Quantity@40240000

scala> bandwidth.show
res1: String = 10 Gb/s

scala> bandwidth.showFull
res2: String = 10 gigabit/second

scala> bandwidth.showUnit
res3: String = Gb/s

scala> bandwidth.showUnitFull
res4: String = gigabit/second

Predefined Units

A variety of units and prefixes are predefined by several coulomb sub-packages, which are summarized here. The relation between the packages below and maven packages is at the top of this page.

Unit Types and Convertability

The concept of unit convertability is fundamental to the coulomb library and its implementation of unit analysis. Two unit type expressions are convertable if they encode an equivalent "abstract quantity." For example, Meter and Mile are convertable because they both encode the abstract quantity of length. Foot %^ 3 and Liter are convertable because they both encode a volume, or length^3. Kilo %* Meter %/ Hour and Foot %* (Second %^ -1) are convertable because they encode a velocity, or length / time.

In coulomb, abstract quantities like length are represented by a unique BaseUnit. For example the base unit for length is the type coulomb.si.Meter. Compound abstract quantities such as length / time or length ^ 3 are internally represented by pairs of base units with exponents:

  • velocity <=> length / time <=> (Meter ^ 1)(Second ^ -1)
  • volume <=> length ^ 3 <=> (Meter ^ 3)
  • acceleration <=> length / time^2 <=> (Meter ^ 1)(Second ^ -2)
  • bandwidth <=> information / time <=> (Byte ^ 1)(Second ^ -1)

In coulomb, a unit quantity will be implicitly converted into a quantity of a different unit type whenever those types are convertable. Any attempt to convert between non-convertable unit types results in a compile-time type error.

scala> def foo(q: Quantity[Double, Meter %/ Second]) = q.showFull

scala> foo(60f.withUnit[Mile %/ Hour])
res5: String = 26.8224 meter/second

scala> foo(1f.withUnit[Mile %/ Minute])
res6: String = 26.8224 meter/second

scala> foo(1f.withUnit[Foot %/ Day])
res7: String = 3.5277778E-6 meter/second

scala> foo(1f.withUnit[Foot %* Day])
       error: type mismatch;

Unit Conversions

As described in the previous section, unit quantities can be converted from one unit type to another when the two types are convertable. Unit conversions come in a couple different forms:

// Implicit conversion
scala> val vol: Quantity[Double, Meter %^ 3] = 4000D.withUnit[Liter]
vol: coulomb.Quantity[Double,coulomb.si.Meter %^ 3] = Quantity(4.0)

scala> vol.showFull
res2: String = 4.0 meter^3

// Explicit conversion using the `toUnit` method
scala> 4000D.withUnit[Liter].toUnit[Meter %^ 3].showFull
res3: String = 4.0 meter^3

Unit Operations

Unit quantities support math operations +, -, *, /, and pow. Quantities must be of convertable unit types to be added or subtracted. The type of the left-hand argument is taken as the type of the output:

scala> (1.withUnit[Foot] + 1.withUnit[Yard]).show
res4: String = 4 ft

scala> (4.withUnit[Foot] - 1.withUnit[Yard]).show
res5: String = 1 ft

Quantities of any unit types may be multiplied or divided. Result types are different than either argument:

scala> (60.withUnit[Mile] / 1.withUnit[Hour]).show
res6: String = 60 mi/h

scala> (1.withUnit[Yard] * 1.withUnit[Yard]).show
res7: String = 1 yd^2

scala> (1.withUnit[Yard] / 1.withUnit[Inch]).toUnit[Percent].show
res8: String = 3600 %

When raising a unit to a power, the exponent is given as a literal type:

scala> 3D.withUnit[Meter].pow[2].show
res13: String = 9.0 m^2

scala> Rational(3).withUnit[Meter].pow[-1].show
res14: String = 1/3 m^(-1)

scala>  3.withUnit[Meter].pow[0].show
res15: String = 1 unitless

Declaring New Units

The coulomb library strives to make it easy to add new units which work seamlessly with the unit analysis type system. There are two varieties of unit declaration: base units and derived units.

A base unit, as its name suggests, is not defined in terms of any other unit; it is axiomatic. The Standard International Base Units are all declared as base units in the coulomb.si subpackage. In the coulomb.info sub-package, Byte is declared as the base unit of information.

Declaring a base unit is special in the sense that it also defines a new kind of fundamental abstract quantity. For example, by declaring coulomb.si.Meter as a base unit, coulomb establishes Meter as the canonical representation of the abstract quantity of Length. Any other unit of length must be declared as a derived unit of Meter, or it would be considered non-convertable with other lengths.

Here is an example of defining a new base unit Scoville, representing an abstract quantity of Spicy Heat. The BaseUnit value must be defined as an implicit value:

import coulomb._
import coulomb.define._  // BaseUnit and DerivedUnit
object SpiceUnits {
  trait Scoville
  implicit val defineUnitScoville = BaseUnit[Scoville](name = "scoville", abbv = "sco")
}

The second variety of unit declarations is the derived unit, which is defined in terms of some unit expression involving previously-defined units. Derived units do not define new kinds of abstract quantity, and are generally more common than base units:

object NewUnits {
  import coulomb._, coulomb.define._, coulomb.si._, coulomb.us._

  // a furlong is 660 feet
  trait Furlong
  implicit val defineUnitFurlong = DerivedUnit[Furlong, Foot](coef = 660, abbv = "flg")

  // speed of sound is 1130 feet/second (at sea level, 20C)
  trait Mach
  implicit val defineUnitMach = DerivedUnit[Mach, Foot %/ Second](coef = 1130, abbv = "mach")

  // a standard earth gravity is 9.807 meters per second-squared
  // Define an abbreviation "g"
  trait EarthGravity
  implicit val defineUnitEG = DerivedUnit[EarthGravity, Meter %/ (Second %^ 2)](coef = 9.807, abbv = "g")

  // The maximum ping time to the moon
  // https://twitter.com/cmuratori/status/1219847348433481729
  trait MoonUnit
  implicit val defineUnitMoonUnit = DerivedUnit[MoonUnit, Second](coef = 2.71321035034, abbv = "moo")
}

Notice that there are no constraints or requirements associated with the unit types Scoville, Furlong, etc. These may simply be declared, as shown above, however they may also be pre-existing types. In other words, you may define any type, pre-existing or otherwise, to be a coulomb unit by declaring the appropriate implicit value.

Newer versions of coulomb allow Base Units to be implicitly inferred for any type, even if no BaseUnit object has been specifically declared, by importing the undeclaredBaseUnits policy:

scala> import coulomb.policy.undeclaredBaseUnits._
import coulomb.policy.undeclaredBaseUnits._

scala> case class Foo(goo: String)
defined class Foo

scala> (1.withUnit[Foo] + 1.withUnit[Kilo %* Foo]).show
res1: String = 1001 Foo

scala> (10.withUnit[Seq[Int]] / 5.withUnit[Second]).show
res2: String = 2 Seq[Int]/s

Unitless Quantities

When units in an expression all cancel out -- for example, a ratio of quantities with convertable units -- the value is said to be "unitless". In coulomb the unit expression type Unitless represents this particular state. Here are a few examples of situations when Unitless values arise:

// ratios of convertable unit types are always unitless
scala> (1.withUnit[Yard] / 1.withUnit[Foot]).toUnit[Unitless].show
res1: String = 3 unitless

// raising to the zeroth power
scala> 100.withUnit[Second].pow[0].show
res2: String = 1 unitless

// Radians and other angular units are derived from Unitless
scala> math.Pi.withUnit[Radian].toUnit[Unitless].show
res3: String = 3.141592653589793 unitless

// Percentages
scala> 90D.withUnit[Percent].toUnit[Unitless].show
res4: String = 0.9 unitless

Unit Prefixes

Unit prefixes are a first-class concept in coulomb. In fact, prefixes are derived units of Unitless:

scala> 1.withUnit[Kilo].toUnit[Unitless].show
res1: String = 1000 unitless

scala> 1.withUnit[Kibi].toUnit[Unitless].show
res2: String = 1024 unitless

Because they are just another kind of unit, prefixes work seamlessly with all other units.

scala> 3.withUnit[Meter %^ 3].toUnit[Kilo %* Liter].showFull
res1: String = 3 kiloliter

scala> 3D.withUnit[Meter %^ 3].toUnit[Mega %* Liter].showFull
res2: String = 0.003 megaliter

scala> (1.withUnit[Kilo] * 1.withUnit[Meter]).toUnit[Meter].showFull
res3: String = 1000 meter

scala> (1D.withUnit[Meter] / 1D.withUnit[Mega]).toUnit[Meter].showFull
res4: String = 1.0E-6 meter

The coulomb library comes with definitions for the standard SI prefixes, and also standard binary prefixes.

It is also easy to declare new prefix units using coulomb.define.PrefixUnit

scala> trait Dozen
defined trait Dozen

scala> implicit val defineUnitDozen = PrefixUnit[Dozen](coef = 12, abbv = "doz")
defineUnitDozen: coulomb.define.DerivedUnit[Dozen,coulomb.Unitless] = DerivedUnit(12, dozen, doz)

scala> 1D.withUnit[Dozen %* Inch].toUnit[Foot].show
res1: String = 1.0 ft

Using WithUnit

The WithUnit type alias can be used to make unit definitions more readable. The following two function definitions are equivalent:

def f1(duration: Quantity[Float, Second]) = duration + 1f.withUnit[Minute]
def f2(duration: Float WithUnit Second) = duration + 1f.withUnit[Minute]

There is a similar WithTemperature alias for working with Temperature values.

Type Safe Configurations

One of the significant use cases for coulomb is adding unit type awareness to software configurations "at the edge." The coulomb libraries include some integrations with popular configuration libraries:

Absolute Temperature and Time Values

In coulomb, both time and temperature units can serve as units in Quantity values, but they can also serve as measures against an absolute offset.

In the case of temperature units, the Temperature type represents absolute temperature values, with respect to absolute zero. The type EpochTime represents absolute date/time moments, based on the unix epoch: midnight of Jan 1, 1970. EpochTime is similar to java.time.Instant, and can interoperate with it.

Temperature and EpochTime are both specializations of OffsetQuantity[N, U]. These obey somewhat different laws than Quantity:

OffsetQuantity - OffsetQuantity => Quantity  // e.g. EpochTime - EpochTime => Quantity
OffsetQuantity + Quantity => OffsetQuantity  // e.g. Temperature + Quantity => Temperature
OffsetQuantity - Quantity => OffsetQuantity

Working with Type Parameters and Type-Classes

Previous topics have focused on how to work with specific Quantity and unit expressions. However, suppose you wish to write your own "generic" functions or classes, where Quantity values have parameterized types? For these situations, coulomb provides a set of implicit type-classes that allow Quantity operations to be supported with type parameters. These typeclasses can be accessed via import coulomb.unitops._

The UnitString type-class supports unit names and abbreviations:

scala> import coulomb.unitops._

scala> def uname[N, U](q: Quantity[N, U])(implicit us: UnitString[U]): String = us.full
uname: [N, U](q: coulomb.Quantity[N,U])(implicit us: coulomb.unitops.UnitString[U])String

scala> uname(3.withUnit[Meter %/ Second])
res0: String = meter/second

The various numeric operations are supported by a set of typeclasses, summarized in the following table.

operation implicit class algebra
< <= > >= === =:= UnitOrd[N1,U1,N2,U2] Order[N1]
+ UnitAdd[N1,U1,N2,U2] AdditiveSemigroup[N1]
- UnitSub[N1,U1,N2,U2] AdditiveGroup[N1]
* UnitMul[N1,U1,N2,U2] MultiplicativeSemigroup[N1]
/ UnitDiv[N1,U1,N2,U2] MultiplicativeGroup[N1]
pow UnitPow[N, U, P] MultiplicativeSemigroup[N]
unary - UnitNeg[N] AdditiveGroup[N]

For common numeric types, the various algebras in the table above can be obtained via import spire.std.any._, or individually as in import spire.std.double._. The following code block shows an example of using typeclasses to support some numeric Quantity operations:

scala> def operate[N1, U1, N2, U2](q1: Quantity[N1, U1], q2: Quantity[N2, U2])(implicit
    add: UnitAdd[N1, U1, N2, U2],
    mul: UnitMul[N1, U1, N2, U2],
    pow: UnitPow[N1, U1, 3],
    ord: UnitOrd[N1, U1, N2, U2]) = {
  val r1 = q1 + q2
  val r2 = q1 * q2
  val r3 = q1.pow[3]
  val r4 = q1 < q2
  (r1, r2, r3, r4)
}

scala> val (r1, r2, r3, r4) = operate(2f.withUnit[Meter], 3f.withUnit[Meter])
r1: coulomb.Quantity[Float,coulomb.si.Meter] = Quantity(5.0)
r2: coulomb.Quantity[Float,coulomb.si.Meter %^ Int(2)] = Quantity(6.0)
r3: coulomb.Quantity[Float,coulomb.si.Meter %^ Int(3)] = Quantity(8.0)
r4: Boolean = true

scala> List(r1.show, r2.show, r3.show, r4.toString)
res1: List[String] = List(5.0 m, 6.0 m^2, 8.0 m^3, true)

The ability to convert between unit quantities is represented by the UnitConverter typeclass. This example illustrates the use of UnitConverter:

scala> def pints[U](beer: Quantity[Double, U])(implicit
    cnv: UnitConverter[Double, U, Double, Pint]): Unit = {
  val pintsOfBeer = beer.toUnit[Pint]
  print(s"I have so much beer: ${pintsOfBeer.showFull}")
}

scala> pints(500D.withUnit[Milli %* Liter])
I have so much beer: 1.0566882094325938 pint

The UnitConverter typeclass is also used by default typeclasses for UnitAdd, UnitMul and the other numeric operations above. This typeclass can be extended by adding unit converter policies.

Compute Model for Quantity Operations

Previous sections have disussed the various operations that can be performed on Quantity objects. As mentioned in the section on unit operations, the quantity result type of binary operations is governed by the left-hand-side of an exression:

scala> (1.withUnit[Foot] + 1.withUnit[Yard]).show
res0: String = 4 ft

As described in the previous section on operation typeclasses the UnitAdd typeclass implements unit quantity addition. Here is the code for UnitAdd:

trait UnitAdd[N1, U1, N2, U2] {
  def vadd(v1: N1, v2: N2): N1
}
object UnitAdd {
  implicit def evidenceASG0[N1, U1, N2, U2](implicit
      as1: AdditiveSemigroup[N1],
      uc: UnitConverter[N2, U2, N1, U1]): UnitAdd[N1, U1, N2, U2] =
    new UnitAdd[N1, U1, N2, U2] {
      def vadd(v1: N1, v2: N2): N1 = as1.plus(v1, uc.vcnv(v2))
    }
}

As the above code suggests, all quantity operations by convention first convert the RHS value to the value and unit type of the left hand side, and then use the appropriate algebra (in this example AdditiveSemigroup) to perform that operation with respect to the LHS value and unit. Note that this means only the LHS value type requires this algebra to exist.

The full set of Quantity operation typeclasses are defined in unitops.scala.

How do unit conversions operate? Here is the default implementation of UnitConverter:

trait UnitConverter[N1, U1, N2, U2] {
  def vcnv(v: N1): N2
}
trait UnitConverterDefaultPriority {
  implicit def witness[N1, U1, N2, U2](implicit
      cu: ConvertableUnits[U1, U2],
      cf1: ConvertableFrom[N1],
      ct2: ConvertableTo[N2]): UnitConverter[N1, U1, N2, U2] =
    new UnitConverter[N1, U1, N2, U2] {
      def vcnv(v: N1): N2 = ct2.fromType[Rational](cf1.toType[Rational](v) * cu.coef)
    }
}

As the above code illustrates, a standard unit conversion proceeds by:

  1. Converting the input type to Rational
  2. Multiply by the unit conversion coefficient (also a Rational)
  3. Converting the result to the output type.

Coulomb uses the Rational type as the intermediary because it can operate lossessly on integer values, and it can accommodate most numeric repesentations with zero or minimal loss. Note that any numeric precision loss in this process is most likely to occur during the final conversion to the LHS value type.

The potential for precision loss is greatest when working with integer value types, and particuarly when the LHS unit is larger than the RHS unit, as in the second conversion below:

scala> ((100.withUnit[Meter]) + (1.withUnit[Kilo %* Meter])).show
res0: String = 1100 m

scala> ((1.withUnit[Kilo %* Meter]) + (100.withUnit[Meter])).show
res1: String = 1 km

Some unit conversion specializations are applied. For example, in the case that both LHS and RHS units and value types are the same, the fast and lossless identity function is applied:

implicit def witnessIdentity[N, U]: UnitConverter[N, U, N, U] = {
  new UnitConverter[N, U, N, U] {
    @inline def vcnv(v: N): N = v
  }
}

Another example is the case of Double value types, where preconverting the conversion coefficient to Double is efficient while maintaining Double accuracy:

implicit def witnessDouble[U1, U2](implicit
    cu: ConvertableUnits[U1, U2]): UnitConverter[Double, U1, Double, U2] = {
  val coef = cu.coef.toDouble
  new UnitConverter[Double, U1, Double, U2] {
    @inline def vcnv(v: Double): Double = v * coef
  }
}

UnitConverter and its full set of typeclass rules are defined in unitops.scala.

Unit Conversions for Custom Value Types

Coulomb's typeclass rules can be extended to support new value types.

There are two components to a custom extension. The first is to define any desired algebras on the new value type. Defining algebras is optional, if no numeric operations need to be supported.

The second customization component is to define what it means to multiply a value by a conversion coefficient. This can be accomplished using the UnitConverterPolicy typeclass, defined in coulomb.unitops.

In the following example, coulomb is extended to support the spire Complex type. In the case of Complex, algebras such as additive and multiplicative (semi)groups are already defined.

scala> import coulomb.unitops._, spire.math.Complex, spire.algebra._

// define what it means to apply unit conversion coefficients to Complex
scala> implicit def complexPolicy[U1, U2]: UnitConverterPolicy[Complex[Double], U1, Complex[Double], U2] =
  new UnitConverterPolicy[Complex[Double], U1, Complex[Double], U2] {
    def convert(v: Complex[Double], cu: ConvertableUnits[U1, U2]): Complex[Double] = v * cu.coef.toDouble
  }

scala> q.show
res0: String = (1.0 + 2.0i) m

scala> q.toUnit[Foot].show
res1: String = (3.2808398950131235 + 6.561679790026247i) ft

scala> (q * q).show
res2: String = (-3.0 + 4.0i) m^2

scala> (q + q).show
res3: String = (2.0 + 4.0i) m