0
votes

I am playing around with implicits a bit (I'm learning Scala currently) and wrote a simple class to represent temperatures, with scale conversions. This uses a 'TempOps' class to provide implicit operations on numerics so I can write things like:

(5 Celsius) asFahrenheight

Because I want it to work for any numeric type (at least the built-in ones) I abstracted the TempOps class using an implicit Numeric type class. The code is as follows:

package exercises.temperature

/**
 * Created by steve on 9/16/2015.
 */
sealed case class Temperature(kelvin: Double) {
  import Temperature._

  override def toString():String = s"$kelvin Kelvin"

  def asKelvin = kelvin
  def asFahrenheight = ctof(ktoc(kelvin))
  def asCelsius = ktoc(kelvin)

  def asK = asKelvin
  def asF = asFahrenheight
  def asC = asCelsius
}

sealed case class TempOps[N](t: N)(implicit n: Numeric[N]) {
  implicit val Fahrenheit = Temperature.Fahrenheit(n.toDouble(t))
  implicit val Celsius = Temperature.Celsius(n.toDouble(t))
  implicit val Kelvin = Temperature(n.toDouble(t))
}

object Temperature {
  private val absoluteZeroC = -273.15
  private def ftoc(f: Double) = (f-32)*5/9
  private def ctok(c: Double) = c - absoluteZeroC
  private[temperature] def ktoc(k: Double) = k + absoluteZeroC
  private[temperature] def ctof(c: Double) = c*9/5 + 32

  private[temperature] def Fahrenheit(f: Double) = Temperature(ctok(ftoc(f)))
  private[temperature] def Celsius(c: Double) = Temperature(ctok(c))

  implicit def toTempOps(n: Int) = TempOps(n)
}

This works, but suppose I want to preserve the original numeric type so that in the following the result is still an Int rather than a Double (up to some rounding obviously):

val originalTempValue: Int = 5  // Explicitly an Int
val converted = (originalTempValue Celsius) asFahrenheit

'converted' will now be a Double. How can I modify TempOps to 'preserve' the numeric type being implicitly used, so that in the above 'converted' would wind up being an Int?

2

2 Answers

1
votes

You need to track the original T from where the value came, and then define a conversion from double to that T.

For example:

trait ConvertTemp[T] extends (Double ⇒ T)
object ConvertTemp {
    def apply[T](f: Double ⇒ T) = new ConvertTemp[T] { override def apply(v: Double) = f(v) }
    implicit val convertToInt = apply(Math round _)
    implicit val convertToDouble = apply(identity)
}

sealed case class Temperature[T](kelvin: Double)(implicit convert: ConvertTemp[T]) {
  import Temperature._
  override def toString(): String = s"$kelvin Kelvin"

  def asKelvin = convert(kelvin)

  def asFahrenheight = ctof(ktoc(kelvin))

  def asCelsius = ktoc(kelvin)

  def asK = asKelvin

  def asF = asFahrenheight

  def asC = asCelsius
}

object Temperature {
  private val absoluteZeroC = -273.15

  private def ftoc(f: Double) = (f - 32) * 5 / 9

  private def ctok(c: Double) = c - absoluteZeroC

  private[temperature] def ktoc(k: Double) = k + absoluteZeroC

  private[temperature] def ctof(c: Double) = c * 9 / 5 + 32

  private[temperature] def Fahrenheit[T](f: Double)(implicit convert: ConvertTemp[T]) = Temperature(ctok(ftoc(f)))

  private[temperature] def Celsius[T](c: Double)(implicit convert: ConvertTemp[T]) = Temperature(ctok(c))

  implicit def toTempOps(n: Int) = TempOps(n)
}

sealed case class TempOps[N](t: N)(implicit n: Numeric[N]) {
  implicit val Fahrenheit = Temperature.Fahrenheit(n.toDouble(t))
  implicit val Celsius = Temperature.Celsius(n.toDouble(t))
  implicit val Kelvin = Temperature(n.toDouble(t))
}
0
votes

Using emilianogc's suggestion above I arrived at the solution below.

package exercises.temperature

/**
 * Created by steve on 9/16/2015.
 */
trait ConvertTemp[T] extends (Double => T)
object ConvertTemp {
  def apply[T](f: Double => T) = new ConvertTemp[T] { override def apply(v: Double) = f(v) }
  implicit val convertToInt = apply(x=>Math.round(x).toInt)
  implicit val convertToLong = apply(Math.round _)
  implicit val convertToDouble = apply(identity)
}

sealed case class Temperature[N](private val kelvin: Double)(convert: ConvertTemp[N]) {
  import Temperature._

  override def toString():String = s"$kelvin Kelvin"

  def asKelvin = convert(kelvin)
  def asFahrenheight = convert(ctof(ktoc(kelvin)))
  def asCelsius = convert(ktoc(kelvin))

  def asK = asKelvin
  def asF = asFahrenheight
  def asC = asCelsius
}

sealed case class TempOps[N](t: N)(implicit n: Numeric[N], convert: ConvertTemp[N]) {
  implicit val Fahrenheit = Temperature.Fahrenheit(t)(n,convert)
  implicit val Celsius = Temperature.Celsius(t)(n,convert)
  implicit val Kelvin = Temperature.Kelvin(t)(n,convert)
}

object Temperature {

  private val absoluteZeroC = -273.15
  private def ftoc(f: Double) = (f-32)*5/9
  private def ctok(c: Double) = c - absoluteZeroC
  private[temperature] def ktoc(k: Double) = k + absoluteZeroC
  private[temperature] def ctof(c: Double) = c*9/5 + 32

  private[temperature] def Fahrenheit[N](f: N)(n: Numeric[N], convert: ConvertTemp[N]) = Temperature(ctok(ftoc(n.toDouble(f))))(convert)
  private[temperature] def Celsius[N](c: N)(n: Numeric[N], convert: ConvertTemp[N]) = Temperature(ctok(n.toDouble(c)))(convert)
  private[temperature] def Kelvin[N](c: N)(n: Numeric[N], convert: ConvertTemp[N]) = Temperature(n.toDouble(c))(convert)

  implicit def toTempOps(t: Int) = TempOps(t)
  implicit def toTempOps(t: Long) = TempOps(t)
  implicit def toTempOps(t: Double) = TempOps(t)
}

My remaining dis-satisfaction is that there seems to be a fair bit of 'boilerplate verbage' in the passing around of the numeric class type and the converter in places where it cannot be implicit (because we're dealing with generic types already that can only be inferred via other implicits, I think). If anyone is able to find a way to make this a little less cluttered in that regard, and still compile (some of the explicit type classes in this I was initially expecting could be inferred, but the compiler doesn't seem to want to play ball if I try to make anything more in the code above implicit)