Reputation: 41
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?
Upvotes: 0
Views: 129
Reputation: 41
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)
Upvotes: 0
Reputation: 930
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))
}
Upvotes: 1