Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell Return Type Polymorphism

Tags:

haskell

I have the following data structure:

data TempUnit = Kelvin Float
              | Celcius Float
              | Fahrenheit Float

I want to implement a function which converts a temperature from Kelvin to another Unit. How can I pass the return type unit to the function?

like image 531
SvenK Avatar asked Dec 31 '12 16:12

SvenK


2 Answers

One way of doing this would be to use 3 separate types for the different temperature units and then use a type-class to "unite" them as temperatures, e.g.

newtype Kelvin = Kelvin Float
newtype Celcius = Celcius Float
newtype Fahrenheit = Fahrenheit Float

class TempUnit a where
   fromKelvin :: Kelvin -> a
   toKelvin :: a -> Kelvin

instance TempUnit Kelvin where
   fromKelvin = id
   toKelvin = id

instance TempUnit Celcius where
   fromKelvin (Kelvin k) = Celcius (k - 273.15)
   toKelvin (Celcius c) = Kelvin (c + 273.15)

instance TempUnit Fahrenheit where
   fromKelvin (Kelvin k) = Fahrenheit ((k-273.15)*1.8 + 32)
   toKelvin (Fahrenheit f) = Kelvin ((f - 32)/1.8 + 273.15

Now you can just use toKelvin/fromKelvin and the appropriate implementation will be chosen based on the (inferred) return type, e.g.

absoluteZeroInF :: Fahrenheit 
absoluteZeroInF = fromKelvin (Kelvin 0)

(Note the use of newtype rather than data, this is the same as data but without the runtime cost of an extra constructor.)

This method provides an arbitrary conversion function convert :: (TempUnit a, TempUnit b) => a -> b automatically: convert = fromKelvin . toKelvin. On that note, this requires writing type signatures of functions that handle arbitrary temperatures with the TempUnit a => ... a constraints rather than just a plain TempUnit.


One could also use a "sentinel" value that is otherwise ignored, e.g.

fromKelvin :: TempUnit -> TempUnit -> TempUnit
fromKelvin (Kelvin _) (Kelvin k) = Kelvin k
fromKelvin (Celcius _) (Kelvin k) = Celcius (k - 273.15)
fromKelvin (Fahrenheit _) (Kelvin k) = Fahrenheit (...)

(This is probably better done by the method @seliopou suggests: breaking out a separate Unit type.)

This can be used like so:

-- aliases for convenience
toC = Celcius 0
toK = Kelvin 0
toF = Fahrenheit 0

fromKelvin toC (Kelvin 10)
fromKelvin toF (Kelvin 10000)

Note that this method is not type-safe: what happens when trying to convert Celcius 100 with fromKelvin? (i.e. what is the value of fromKelvin toF (Celcius 100)?)


All this said, it would be best to internally standardise on one unit and only convert to the others on input and output, i.e. only the functions that read or write temperatures need to worry about conversions, everything else just works with (e.g.) Kelvin.

like image 60
huon Avatar answered Oct 02 '22 23:10

huon


Let me suggest a refactoring that may help you along your way:

data Unit = Kelvin | Celcius | Fahrenheit
data Temp = Temp Unit Float

Then you could easily do what you want to do:

convert :: Temp -> Unit -> Temp

EDIT:

If you can't perform that refactoring, then you can still do what you want to do, it's just a little less clean:

convert :: Temp -> Temp -> Temp

Say you want to convert a temperature in Kelvin (value bound to the identifier t) to Celcius. You'd do something like this:

convert t (Celcius 0)

Your implementation of convert would pattern match on the second argument to determine the units to convert to.

like image 45
seliopou Avatar answered Oct 02 '22 23:10

seliopou