Question

I'm trying to create a library that can convert distances from one unit into another. Ideally I'd be able to specify a distance in one unit, and when passed to a method that requires a different unit, have the scala compiler convert it automatically. This is what I have so far:

abstract class BaseUnit(scale: Option[Double] = None) {
  def unit: String

  def scalingFactor: Double = scale match {
    case Some(factor) => factor
    case None => 1.0
  }
}

object Cm {
  implicit def inch2cm(inch: Inch):Cm = new Cm(Some(0.393 * inch.scalingFactor))
}

class Cm(scale: Option[Double] = None) extends BaseUnit(scale) {
  def unit: String = "cm"
}

object Inch {
  implicit def cm2inch(cm: Cm):Inch = new Inch(Some(2.54 * cm.scalingFactor))
}

class Inch(scale: Option[Double] = None) extends BaseUnit(scale) {
  def unit: String = "inch"
}

class Distance[A <: BaseUnit](val scalar: Double, val unit: A) {
  override def toString: String = (scalar*unit.scalingFactor)+unit.unit
}


def foo(x: Distance[Cm], y: Distance[Cm]): String = x.toString()+","+y.toString()

Using it without explicitly stating the type parameter seems to make Scala use the Nothing type:

val a = new Distance(10, new Inch)                                         

println(foo(a, a))                                                               

> scala test.scala

 found   : this.Distance[Nothing]                                   
 required: this.Distance[this.Cm]                                   
Note: Nothing <: this.Cm, but class Distance is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)                   
println(foo(a, a))                                                  
            ^                                                       
one error found     

Following the compiler's suggestion results in foo returning 10.0inch,10.0inch rather than the expected 3.93cm,3.93cm.

If I explicitly specify the type, the compiler picks up on the difference, but still doesn't implicitly convert one to the other.

val a = new Distance[Inch](10, new Inch)

println(foo(a, a))                      

// found   : this.Distance[this.Inch]    
// required: this.Distance[this.Cm]      
//  println(foo(a, a))                     
//              ^                          
//  one error found                        

Am I doing something wrong, or does the compiler not allow this usage of implicit conversion?

Was it helpful?

Solution

You just need to

class Distance[A <: BaseUnit](val scalar: Double, val unit: A) { ... }

so that the compiler has a reason to not make A too specific. Otherwise it's free to choose Nothing since it's not related to anything that you're doing.

Also, you know how to convert between units, but you haven't taught it how to convert between distances. You can:

implicit def convertDist[A <: BaseUnit, B <: BaseUnit](da: Distance[A])(implicit a2b: (A => B)): Distance[B] = new Distance[B](da.scalar, a2b(da.unit))

or something like that. (As you define it now, the conversions are backwards, incidentally.)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top