I would recommend you avoid having three different possible representations of a temperature. This just leads to lots of runtime branching and conversion. It does make sense to have a dedicated type for temperatures, and it makes sense to keep the scale used a private implementation detail, but sticking to one convention simplifies things.
module Physics.Quantities.Temperature (Temperature) where
newtype Temp = KelvinTemp { getKelvinTemperature :: Double }
deriving (Eq, Ord)
Note that I don't export the Kelvin-specific constructor, so for anybody using this type it can't matter which temperature scale is used. And because the internal representation is fixed, the compiler can figure out the Eq
and Ord
instances by itself.
Now ok, obviously you'll still need to be able to actually get stuff done, so you will need accessors. One way is simple read-in-this-scale like
toCelsius :: Temp -> Double
toCelsius (KelvinTemp tK) = tK - waterTriplePointInK
But those would be one-way, not allowing you to create temperature values again. An elegant way of achieving this is to use bidirectional functions – isomorphisms. The most popular representation is the one from the lens library:
import Control.Lens
kelvin :: Iso' Temp Double
kelvin = iso getKelvinTemperature KelvinTemp
celsius :: Iso' Temp Double
celsius = iso (\(Temp tK) -> tK - waterTriplePointInK)
(\tC -> Temp $ tC + waterTriplePointInK)
where waterTriplePointInK = 273.15
fahrenheit :: Iso' Temp Double
fahrenheit = iso (\(Temp tK) -> (tK - fahrZeroInK)/fahrScaleFact)
(\tF -> Temp $ tF*fahrScaleFact + fahrZeroInK)
where fahrZeroInK = 255.372
fahrScaleFact = 5/9
Now you can do stuff like
*Main> let tBoil :: Temp; tBoil = 100^.from celsius
*Main> tBoil^.fahrenheit
212.00039999999993
*Main> 37^.from celsius.fahrenheit
98.60039999999992
*Main> 4000^.from kelvin.celsius
3726.85
If you really want to have different representations for the different scales, here's another approach that's both more well-typed and will avoid runtime branching:
{-# LANGUAGE DataKinds, KindSignatures, MultiParamTypeClasses #-}
data TemperatureScale = KelvinSc | CelsiusSc | FahrenheitSc
newtype KelvinTemperature = Kelvin {getKelvinTemperature ::Double}
newtype CelsiusTemperature = Celsius {getCelsiusTemperature ::Double}
newtype FahrenheitTemperature = Fahrenheit {getFahrenheitTemperature::Double}
type family Temperature (sc :: TemperatureScale) where
Temperature 'KelvinSc = KelvinTemperature
Temperature 'CelsiusSc = CelsiusTemperature
Temperature 'FahrenheitSc = FahrenheitTemperature
class ConvTemperature t t' where
convTemperature :: Temperature t -> Temperature t'
instance ConvTemperature KelvinSc KelvinSc where convTemperature = id
instance ConvTemperature CelsiusSc CelsiusSc where convTemperature = id
instance ConvTemperature FahrenheitSc FahrenheitSc where convTemperature = id
instance ConvTemperature KelvinSc FahrenheitSc where
...
...
If you're really serious about it, check out the units package, which does all of this and much more.