减少这种代码中的重复次数的公认方法是什么?
newtype Fahrenheit = Fahrenheit Double deriving (Eq)
newtype Celsius = Celsius Double deriving (Eq)
newtype Kelvin = Kelvin Double deriving (Eq)
newtype Rankine = Rankine Double deriving (Eq)
newtype Reaumure = Reaumure Double deriving (Eq)
newtype Romer = Romer Double deriving (Eq)
newtype Delisle = Delisle Double deriving (Eq)
newtype Newton = Newton Double deriving (Eq)
instance Show Fahrenheit where
show (Fahrenheit f) = show f ++ " °F"
instance Show Celsius where
show (Celsius c) = show c ++ " °C"
instance Show Kelvin where
show (Kelvin k) = show k ++ " K"
instance Show Rankine where
show (Rankine r) = show r ++ " °R"
instance Show Reaumure where
show (Reaumure r) = show r ++ " °Ré"
instance Show Romer where
show (Romer r) = show r ++ " °Rø"
instance Show Delisle where
show (Delisle d) = show d ++ " °De"
instance Show Newton where
show (Newton n) = show n ++ " N°"
class Temperature a where
increaseTemp :: a -> Double -> a
decreaseTemp :: a -> Double -> a
toFahrenheit :: a -> Fahrenheit
toCelsius :: a -> Celsius
toKelvin :: a -> Kelvin
toRankine :: a -> Rankine
toReaumure :: a -> Reaumure
toRomer :: a -> Romer
toDelisle :: a -> Delisle
toNewton :: a -> Newton
instance Temperature Fahrenheit where
increaseTemp (Fahrenheit f) n = if n < 0 then error "negative val" else Fahrenheit $ f + n
decreaseTemp (Fahrenheit f) n = if n < 0 then error "negative val" else Fahrenheit $ f - n
toFahrenheit = id
toCelsius (Fahrenheit f) = Celsius $ (f - 32) * 5 / 9
toKelvin (Fahrenheit f) = Kelvin $ (f - 32) * 5 / 9 + 273.15
toRankine (Fahrenheit f) = Rankine $ f + 458.67
toReaumure (Fahrenheit f) = Reaumure $ (f - 32) * 4 / 9
toRomer (Fahrenheit f) = Romer $ (f - 32) * 7 / 24 + 7.5
toDelisle (Fahrenheit f) = Delisle $ (212 - f) * 5 / 6
toNewton (Fahrenheit f) = Newton $ (f - 32) * 11 / 60
instance Temperature Celsius where
increaseTemp (Celsius c) n = if n < 0 then error "negative val" else Celsius $ c + n
decreaseTemp (Celsius c) n = if n < 0 then error "negative val" else Celsius $ c - n
toFahrenheit (Celsius c) = Fahrenheit $ c * 9 / 5 + 32
toCelsius = id
toKelvin (Celsius c) = Kelvin $ c + 273.15
toRankine (Celsius c) = Rankine $ c * 9/5 + 491.67
toReaumure (Celsius c) = Reaumure $ c * 4 / 5
toRomer (Celsius c) = Romer $ c * 21 / 40 + 7.5
toDelisle (Celsius c) = Delisle $ (100 - c) * 3 / 2
toNewton (Celsius c) = Newton $ c * 33 / 100
instance Temperature Kelvin where
increaseTemp (Kelvin k) n = if n < 0 then error "negative val" else Kelvin $ k + n
decreaseTemp (Kelvin k) n = if n < 0 then error "negative val" else Kelvin $ k - n
toFahrenheit (Kelvin k) = Fahrenheit $ (k - 273.15) * 9 / 5 + 32
toCelsius (Kelvin k) = Celsius $ k - 273.15
toKelvin = id
toRankine (Kelvin k) = Rankine $ k * 9 / 5
toReaumure (Kelvin k) = Reaumure $ (k - 273.15) * 4 / 5
toRomer (Kelvin k) = Romer $ (k - 273.15) * 21 / 40 + 7.5
toDelisle (Kelvin k) = Delisle $ (373.15 - k) * 3 / 2
toNewton (Kelvin k) = Newton $ (k - 273.15) * 33 / 100
-- rest of the instances omitted.
此外,在类定义中,有一种方法可以将输入变量的类型限制为一个单位。即toCelsius :: a -> Celsius
,约束a可以做什么?或暗示它仅适用于已声明实例的类型。
答案 0 :(得分:5)
主要问题似乎是单位转换,您可以使用DataKinds
和许多其他吓人的语言扩展(仅适用于3个单位)来显着缩短转换时间,减少重复使用的次数(但仅适用于3个单位,但您应该能够概括起来很容易):
{-# LANGUAGE DataKinds,
KindSignatures,
RankNTypes,
ScopedTypeVariables,
AllowAmbiguousTypes,
TypeApplications #-}
data TemperatureUnit = Fahrenheit | Celsius | Kelvin
newtype Temperature (u :: TemperatureUnit) = Temperature Double deriving Eq
class Unit (u :: TemperatureUnit) where
unit :: TemperatureUnit
instance Unit Fahrenheit where unit = Fahrenheit
instance Unit Celsius where unit = Celsius
instance Unit Kelvin where unit = Kelvin
instance Show TemperatureUnit where
show Celsius = "°C"
show Fahrenheit = "°F"
show Kelvin = "K"
instance forall u. Unit u => Show (Temperature u) where
show (Temperature t) = show t ++ " " ++ show (unit @u)
convertTemperature :: forall u1 u2. (Unit u1, Unit u2) => Temperature u1 -> Temperature u2
convertTemperature (Temperature t) = Temperature . fromKelvin (unit @u2) $ toKelvin (unit @u1) where
toKelvin Celsius = t + 273.15
toKelvin Kelvin = t
toKelvin Fahrenheit = (t - 32) * 5/9 + 273.15
fromKelvin Celsius k = k - 273.15
fromKelvin Kelvin k = k
fromKelvin Fahrenheit k = (k - 273.15) * 9/5 + 32
然后您可以像这样使用它:
-- the explicit type signatures here are only there to resolve
-- ambiguities; In more realistic code you'd not need them as often
main = do
let (t1 :: Temperature Celsius) = Temperature 10.0
(t2 :: Temperature Fahrenheit) = Temperature 10.0
putStrLn $ show t1 ++ " = " ++ show (convertTemperature t1 :: Temperature Fahrenheit)
-- => 10.0 °C = 50.0 °F
putStrLn $ show t2 ++ " = " ++ show (convertTemperature t2 :: Temperature Celsius)
-- => 10.0 °F = -12.222222222222221 °C
这里的窍门是DataKinds
允许我们将常规数据类型提升为类型级别,并将其数据构造函数提升为类型级别(据我了解,在现代版本的GHC中,这已经不再是真正的不同了)对不起,我本人对此感到有些困惑。然后,我们仅定义一个帮助器类以获取单元的数据版本,以便我们可以基于它进行调度。这使我们可以尝试使用所有新类型包装器进行操作,除了更少的新类型包装器(更少的实例声明和更少的命名函数)。
当然,另一件事是,在不同的单位转换之间仍然存在组合爆炸式增长-您可以将其吸收并手动编写所有n^2
公式,也可以尝试将其概括化(可能可以根据@chepner的评论使用温度单位,但我不确定您是否想在所有类型之间进行转换)。这种方法无法解决固有的问题,但确实可以消除您使用newtype
-每单元方法带来的一些语法噪声。
您的increaseTemp
和decreaseTemp
函数可以实现为单个函数offsetTemperature
,同时允许负数。尽管我认为让它们以与第二个参数相同的单位而不是一个Double
来测量温度更有意义:
offsetTemperature :: Temperature u -> Temperature u -> Temperature u
offsetTemperature (Temperature t) (Temperature offset) = Temperature (t + offset)
PS:温度可能不应该是Eq
的一个实例-众所周知,浮点数相等(可以预测,但可能不会做您想做的事)。我只将其保留在这里,因为它在您的示例中。
答案 1 :(得分:4)
这是@Cubic出色答案的改编,但是:您不需要特殊的数据类型即可完成此操作。
{-# LANGUAGE ScopedTypeVariables, TypeApplications #-}
import Data.Proxy
newtype Temperature u = Temperature Double deriving Eq
class TemperatureUnit u where
label :: Proxy u -> String
toKelvin :: Temperature u -> Double
fromKelvin :: Double -> Temperature u
instance TemperatureUnit u => Show (Temperature u) where
show (Temperature t) = show t ++ " " ++ label (Proxy @u)
convertTemperature :: forall u1 u2. (TemperatureUnit u1, TemperatureUnit u2) => Temperature u1 -> Temperature u2
convertTemperature = fromKelvin . toKelvin
data Fahrenheit
data Celsius
data Kelvin
instance TemperatureUnit Fahrenheit where
label _ = "°F"
toKelvin (Temperature t) = (t - 32) * 5/9 + 273.15
fromKelvin k = Temperature $ (k - 273.15) * 9/5 + 32
instance TemperatureUnit Celsius where
label _ = "°C"
toKelvin (Temperature t) = t + 273.15
fromKelvin k = Temperature $ k - 273.15
instance TemperatureUnit Kelvin where
label _ = "K"
toKelvin (Temperature t) = t
fromKelvin k = Temperature k
据我所知,这种情况下数据类型方法的优点是,如果出于其他目的需要TemperatureUnit
数据类型,则可以重用它,而不必同时定义{{1} }等类型,就像我在这里所做的那样。它还将可能的温度类型限制为您在data Fahrenheit
类型中定义的温度类型,这可能对您有利或不利。例如,最好进行一次额外的类型检查,使您无法使用TemperatureUnit
,但是这种错误很可能会被其他地方的编译器捕获,尽管错误可能不太明显。而且,如果要导出此功能,则可能需要开放的温度类型世界,以便下游模块可以添加自己的温度类型。
因此,如果您还没有在其他地方使用TemperatureUnit Bool
类型,则IMO不使用数据类型会更简单,更灵活。