Haskell返回类型多态性

6

我有以下数据结构:

data TempUnit = Kelvin Float
              | Celcius Float
              | Fahrenheit Float

我想要实现一个将温度从开尔文转换为其他单位的函数。我该如何将返回类型的单位传递给该函数?
3个回答

14

可以采用三种不同的类型来表示不同的温度单位,然后使用类型类将它们“合并”为温度,例如:

一种方法是使用3个单独的类型来表示不同的温度单位,然后使用类型类将它们“合并”为温度,例如:

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
现在你只需要使用toKelvin / fromKelvin,适当的实现将根据(推断出的)返回类型进行选择,例如:
absoluteZeroInF :: Fahrenheit 
absoluteZeroInF = fromKelvin (Kelvin 0)

(请注意使用newtype而不是data,这与data相同,但没有额外构造函数的运行时成本。)

该方法提供了一个任意转换函数convert :: (TempUnit a,TempUnit b) => a -> b自动执行:convert = fromKelvin . toKelvin。另外需要注意的是,这要求在处理任意温度的函数的类型签名中使用TempUnit a => ... a约束而不仅仅是普通的TempUnit


也可以使用“哨兵”值,该值被忽略,例如:

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

(按照 @seliopou 建议的方法,最好通过拆分一个单独的 Unit 类型来完成这个操作。)

可以像这样使用:

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

fromKelvin toC (Kelvin 10)
fromKelvin toF (Kelvin 10000)
请注意,此方法不是类型安全的:尝试将Celsius 100转换为fromKelvin时会发生什么?(即fromKelvin toF(Celsius 100)的值是多少?)
所有这些都说了,最好在内部标准化一个单位,并仅在输入和输出时转换到其他单位,即只有读取或写入温度的函数需要考虑转换,其他一切都可以使用(例如)Kelvin来处理。

1
@SvenK 这是你可能得到的最好的解决方案。如果你能重构自己的代码以匹配这个模式,那么你应该这样做。 - seliopou
1
我基本上同意,但我必须强烈反对无端地提高开尔文温标的地位。你应该基于唯一明智的温标——兰氏温标来进行转换。兰氏温标 - Daniel Fischer
感谢大家提供的出色答案。@dbaupp 这不是生产代码,我在大学学习函数式编程时做一些练习,以自学一些Haskell。新年快乐。 - SvenK

4

让我建议一种重构方法,可以帮助你更好地完成任务:

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

那么你可以轻松地做你想做的事情:
convert :: Temp -> Unit -> Temp

编辑:

如果你无法进行重构,那么你仍然可以做你想做的事情,只是稍微不太干净:

convert :: Temp -> Temp -> Temp

假设你想要将一个以Kelvin为单位的温度(用标识符t表示)转换成Celsius,你可以按照以下步骤进行:

convert t (Celcius 0)

您的convert实现将会通过匹配第二个参数来确定要进行转换的单位。

3

在您的代码中只有一个类型,那就是 TempUnitKelvinCelciusFahrenheit 不是类型,它们是数据构造器。因此,您无法使用多态来在它们之间进行选择。

如果您想要使用返回类型多态性,您需要定义三种不同的类型并将它们作为同一类型类的实例。这可能看起来像这样:

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

您可以通过提供类型注释(或在需要特定类型的上下文中使用结果)来选择所需的转换:

myTemp :: Celsius
myTemp = fromKelvin $ Kelvin 42

然而,这似乎并不是多态的好用法。一个更加合理的方法是使用代数数据类型TemperatureUnit,将温度表示为数字和单位的组合。这样,转换函数只需要将目标单位作为参数即可 - 不涉及多态。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接