简体   繁体   中英

Haskell Return Type Polymorphism

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?

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, eg

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, eg

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 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, eg

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 ? (ie 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, ie only the functions that read or write temperatures need to worry about conversions, everything else just works with (eg) Kelvin .

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.

There is only one type in your code and that's TempUnit . Kelvin , Celcius and Fahrenheit aren't types, they're data constructors. So you can't use polymorphism to select between them.

If you want to use return type polymorphism, you'd need to define 3 distinct types and make them instances of the same type class. That could look something like this:

newtype Kelvin = Kelvin Float
newtype Celsius = Celsius Float
newtype Fahrenheit = Fahrenheit Float

class Temperature t where
  fromKelvin :: Kelvin -> t
  toKelvin :: t -> Kelvin

instance Temperature Kelvin where
  fromKelvin = id
  toKelvin = id

instance Temperature Celsius where
  fromKelvin (Kelvin k) = Celsius $ -- insert formula here
  toKelvin (Celsius c) = Kelvin $ -- insert formula here

instance Temperature Fahrenheit where
  -- same as for Celsius

You could then select which conversion you want by supplying type annotations (or using the result in a context where a specific type is required):

myTemp :: Celsius
myTemp = fromKelvin $ Kelvin 42

However this does not seem like a good use of polymorphism. An approach where you have an algebraic data type TemperatureUnit and then represent a temperature as a number combined with a unit, seems much more reasonable. That way conversion functions would simply take the target unit as an argument - no polymorphism involved.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM