重新定义(==)的Eq实例

时间:2019-06-11 16:38:35

标签: haskell operator-overloading custom-data-type

我具有以下表示温度的数据声明:

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

-- Functions for conversion between temperatures
kelvToCels :: Temp -> Temp
kelvToCels (Kelvin k) = Celsius (k-273.15)

kelvToFahr :: Temp -> Temp
kelvToFahr (Kelvin k) = Fahrenheit (((9/5)*(k-273.15))+32)

celsToKelv :: Temp -> Temp
celsToKelv (Celsius c) = Kelvin (c+273.15)

celsToFahr :: Temp -> Temp
celsToFahr (Celsius c) = Fahrenheit (((9/5)*c)+32)

fahrToKelv :: Temp -> Temp
fahrToKelv (Fahrenheit f) = Kelvin ((5/9)*(f-32)+273.15) 

fahrToCels :: Temp -> Temp
fahrToCels (Fahrenheit f) = Celsius ((f-32)/(9/5))

我希望能够比较温度,例如

> (Celsius 100) == (Fahrenheit 212.0) 计算结果为true。

这是我的尝试:

instance Eq Temp where
   Celsius c == Fahrenheit f = 
    (celsToFahr c) == f

结果:ghci错误,因为RHS上的c和f是Floats而不是Temps,因此这里是一个“修复”:

instance Eq Temp where
   Celsius c == Fahrenheit f = 
    (celsToFahr (Celsius c)) == (Fahrenheit f)

此编译没有错误,但是(Celsius 100) == (Fahrenheit 212.0) 引发异常:函数==

中的非穷尽模式

我还想创建一个Ord实例,以类似的方式重新定义compare

我已经走到了尽头,我找不到与我类似的任何示例,因此非常感谢任何建议。预先感谢。

2 个答案:

答案 0 :(得分:11)

我建议您从不写不完整的模式匹配。考虑一下这对您的xToY函数意味着什么,这意味着它们应该能够处理任何输入-因此它们的名称应更改为toY

我还要保证通过返回Float(显然不能用错误的构造函数标记)而不是返回Temp(可以)来知道使用哪个构造函数。所以:

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

toCelsiustoFahrenheit类似。如果您确实想这样做,则可以分别编写类似的内容

normalizeKelvin :: Temp -> Temp
normalizeKelvin = Kelvin . toKelvin

但是这是否明智取决于您打算如何使用此代码。

鉴于此,我们现在可以编写一个Eq实例,该实例不是递归的,只需选择其中一个比例作为自然比例并将其转换为 * 。所以:

instance Eq Temp where
    t == t' = toKelvin t == toKelvin t'

请注意,在这里,当我们调用Temp时,我们是从Float实例调度到Eq的{​​{1}}实例,这不同于您的代码是从{{ 1}}实例返回到对(==)的{​​{1}}实例的另一个调用。

* 如果您对舍入有偏执,可以首先检查是否完全需要转换。所以:

Temp

答案 1 :(得分:0)

我建议您避免使用三种可能的温度表示形式。这只会导致大量的运行时分支和转换。拥有专用的温度类型确实是有意义的,并且使秤使用私有的实现细节是有意义的,但是坚持一个约定可以简化事情。

module Physics.Quantities.Temperature (Temperature) where
newtype Temp = KelvinTemp { getKelvinTemperature :: Double }
  deriving (Eq, Ord)

请注意,我没有导出特定于Kelvin的构造函数,因此,对于使用此类型的任何人, 都不会影响使用哪种温度标度。并且由于内部表示形式是固定的,因此编译器可以自行找出EqOrd实例。

现在好了,显然您仍然需要能够完成工作,因此您将需要访问器。一种方法是像这样简单地按此比例读取

toCelsius :: Temp -> Double
toCelsius (KelvinTemp tK) = tK - waterTriplePointInK

但是那将是单向的,不允许您再次创建温度值。实现此目标的一种优雅方法是使用双向函数 –同构。最受欢迎的代表是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

现在您可以做类似的事情

*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

如果您真的想针对不同的比例使用不同的表示形式,这是另一种类型更合理的方法,它将避免运行时分支:

{-# 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
  ...
...

如果您真的很认真,请查看units package,它可以完成所有这些以及更多工作。