1
votes

I made the following class to convert between different temperature types:

data Temp = Kelvin Float | Celsius Float |Fahrenheit Float deriving Show

conversionKelvin:: Temp -> Temp
conversionKelvin (Celsius x) = Kelvin (x + 273.15)
conversionKelvin (Fahrenheit x) = Kelvin((x - 32) * 5/9 + 273.15)
conversionKelvin (Kelvin x) = Kelvin x

conversionCelsius:: Temp -> Temp
conversionCelsius (Kelvin x) = Celsius (x - 273.15)
conversionCelsius (Fahrenheit x) = Celsius((x - 32) * 5/9)
conversionCelsius (Celsius x) = Celsius x

conversionFahrenheit:: Temp -> Temp
conversionFahrenheit (Celsius x) = Fahrenheit (x * 9/5 + 32)
conversionFahrenheit (Kelvin x) = Fahrenheit((x - 273.15)*9/5 + 32)
conversionFahrenheit (Fahrenheit x) = Fahrenheit x

So far all's good, however I want to implement the instance Eq and Ord. I thought about converting each type to celsius and then see which is bigger, but I can't manage to get past the compiler. Any help?

Edit: Here's my attempt at instancing Eq:

instance Eq Temp where
    a == b = conversionCelsius(a) == conversionCelsius(b)

It compiles but it makes haskell enter some kind of loop (doesn't print the output)

3
How did you implmement the instance Eq? Beware that floating-point calculations often have rounding errors, so two values can be equal "by accident" if they are close to each other and vice versa. - Willem Van Onsem
You also need to convert the Kelvin to Kelvin, etc. - Willem Van Onsem
@WillemVanOnsem , noted and updated - Sanzor
One way to avoid issues with rounding is to replace the fixed-precision Float with an arbitrary-precision exact Rational (from Data.Ratio), then use fromRational to convert to fixed precision at the end. Otherwise, for example, converting a temperature from Celsius to Fahrenheit and back will not necessarily produce values that will compare equal with (==): let { x = 1 :: Rational; } in (x, ((x * 9/5 + 32.0) - 32.0) * 5/9) produces exactly equal values (1 % 1, 1 % 1), but using Float / Double instead produces unequal values (1.0,0.9999996) / (1.0,0.9999999999999984). - Jon Purdy
Consider storing every temperature in the same unit (probably Kelvin), and only convert to/from the other units for display purposes. You'll probably be working with temperatures more than you need to create or display them, and if everything is normalized to Kelvin right away, Eq and Ord are trivial and derivable. - chepner

3 Answers

6
votes

This leads to infinite recursion

instance Eq Temp where
    a == b = conversionCelsius(a) == conversionCelsius(b)

This is because the comparison a == b is between two Temps, and the comparison conversionCelsius(a) == conversionCelsius(b) is again between two Temps, so we recurse forever.

To stop the recursion, you need to have at least a base case, where the comparison between Temps is done by comparing Floats. A possible solution is the following one.

instance Eq Temp where
    Celsius x == Celsius y = x == y
    a         == b         = conversionCelsius a == conversionCelsius b

This will compare Celsius temperatures directly, comparing their Float values. Other temperatures are instead converted to Celsuis before recursing. After we recurse, we will immediately find the base case, so the recursion ends.

4
votes

I think you first need to convert Kelvin to Kelvin as well, etc.:

conversionKelvin:: Temp -> Temp
conversionKelvin (Celsius x) = Kelvin (x + 273.15)
conversionKelvin (Fahrenheit x) = Kelvin((x - 32) * 5/9 + 273.15)
conversionKelvin k@(Kelvin _) = k

As for the Eq instance, you can indeed first convert the two temperatures to the same unit (regardless what that unit is), and then check if these are equal:

instance Eq Temp where
    a == b | ~(Kelvin ka) <- conversionKelvin a, ~(Kelvin kb) <- conversionKelvin b = ka == kb

That being said, calculations with Floats often result in rounding errors. This thus means that two Temps that are the same can be considered different, and two Temps that are slightly different can be considered the same.

You can also implement a function toKelvin :: Temp -> Float, since it is clear what unit of the result is:

toKelvin :: Temp -> Float
toKelvin (Celsius x) = x + 273.15
toKelvin (Fahrenheit x) = (x - 32) * 5/9 + 273.15
toKelvin (Kelvin k) = k

and then use:

instance Eq Temp where
    a == b = toKelvin a == toKelvin kb
0
votes

How about picking a normal form for your representation, deriving the Eq instance

type    Temp :: Type
newtype Temp = Celsius Float
  deriving
  newtype Eq

but then creating pattern synonyms for different units

pattern Kelvin :: Float -> Temp
pattern Kelvin kelvin <- Celsius (celsiusToKelvin -> kelvin)
  where Kelvin kelvin =  Celsius (kelvinToCelsius kelvin)

pattern Fahrenheit :: Float -> Temp
pattern Fahrenheit fahrenheit <- Celsius (celsiusToFahrenheit -> fahrenheit)
  where Fahrenheit fahrenheit =  Celsius (fahrenheitToCelsius fahrenheit)