Haskell 哈斯克尔:还原样板
在这样的代码中,减少重复次数的公认方法是什么Haskell 哈斯克尔:还原样板,haskell,Haskell,在这样的代码中,减少重复次数的公认方法是什么 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
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.
此外,在类定义中,有一种方法可以将输入变量中的类型限制为一个单位。ie toCelsius::a->Celsius,有什么办法可以限制a是什么吗?或者这是因为它只在声明了实例的类型上工作。主要的问题似乎是单位转换,使用数据种类和一系列其他可怕的语言扩展只需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
这里的诀窍是DataTypes允许我们将常规数据类型提升到种类级别,并将它们的数据构造函数提升到类型级别,据我所知,在现代版本的GHC中,这已经不再是真正不同的事情了?对不起,我自己在这个问题上有点动摇。然后,我们只需定义一个helper类来获取单元的数据版本,以便我们可以基于它进行调度。这使我们可以尝试使用所有的newtype包装器,除了较少的newtype包装器和较少的实例声明,以及较少的命名函数
当然,另一件事是,在不同的单位转换之间仍然存在一个组合爆炸-你可以吸收它并手工编写所有的n^2公式,或者你可以尝试根据@chepner的评论对温度单位进行推广,但我不确定你想在两者之间转换的所有东西都是可能的。这种方法无法解决这个固有的问题,但它确实消除了newtype-per-unit方法带来的一些语法噪音
您的increaseTemp和DecreateTemp函数可以作为单个函数offsetTemperature实现,同时允许使用负数。虽然我认为让它们以相同的单位作为第二个参数,而不仅仅是一个双参数,这更有意义:
PS:温度可能不应该是Eq的一个例子-浮点相等是出了名的不可靠的可预测的,但可能不会做你想要的。我之所以把它放在这里,是因为它在你的例子中。主要的问题似乎是单位转换,你可以使用数据种类和一系列其他吓人的语言扩展,只为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
这里的诀窍是DataTypes允许我们将常规数据类型提升到种类级别,并将它们的数据构造函数提升到类型级别,据我所知,在现代版本的GHC中,这已经不再是真正不同的事情了?对不起,我自己在这个问题上有点动摇。然后,我们只需定义一个helper类来获取单元的数据版本,以便我们可以基于它进行调度。这使我们可以尝试使用所有的newtype包装器,除了较少的newtype包装器和较少的实例声明,以及较少的命名函数
当然,另一件事是,在不同的单位转换之间仍然存在一个组合爆炸-你可以吸收它并手工编写所有的n^2公式,或者你可以尝试根据@chepner的评论对温度单位进行推广,但我不确定你想在两者之间转换的所有东西都是可能的。这种方法无法解决这个固有的问题,但它确实消除了newtype-per-unit方法带来的一些语法噪音
您的increaseTemp和DecreateTemp函数可以作为单个函数offsetTemperature实现,同时允许使用负数。虽然我认为让它们以相同的单位作为第二个参数,而不仅仅是一个双参数,这更有意义:
PS:温度可能不应该是Eq的一个例子-浮点相等是出了名的不可靠的可预测的,但可能不会做你想要的。我之所以把它放在这里,是因为它在你的例子中。这是对@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数据类型,可以重用它,而不是像我在这里所做的那样定义数据华氏温度等类型。它还将可能的温度类型限制为您在TemperatureUnit类型中定义的温度类型,这可能对您有利,也可能对您不利。例如,您可以得到额外的类型检查,即不能有TemperatureUnit Bool,这是很好的,但是这种错误很可能会被其他地方的编译器发现,尽管可能会有一个不太明显的错误。如果您要导出此功能,您可能需要一个开放的温度世界
类型,以便下游模块可以添加自己的
因此,如果您在其他地方还没有使用TemperatureUnit类型,我不使用数据类型会更简单、更灵活。这是@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数据类型,可以重用它,而不是像我在这里所做的那样定义数据华氏温度等类型。它还将可能的温度类型限制为您在TemperatureUnit类型中定义的温度类型,这可能对您有利,也可能对您不利。例如,您可以得到额外的类型检查,即不能有TemperatureUnit Bool,这是很好的,但是这种错误很可能会被其他地方的编译器发现,尽管可能会有一个不太明显的错误。如果要导出此功能,可能需要一个开放的温度类型世界,以便下游模块可以添加自己的温度类型
因此,如果您在其他地方还没有使用TemperatureUnit类型,我不使用数据类型会更简单、更灵活。这更适合codereview,请考虑一对类型data TemperatureScale=Fahrenheit | Celsius |。。。数据温度=温度-温度刻度加倍。此外,与其定义n^2个转换例程,不如定义2n个例程,将每个温度转换为单个标准标度。例如,不要定义华氏摄氏度,只需使用华氏开尔文摄氏度。这更适合codereview IMHO考虑一对类型data TemperatureScale=Fahrenheit | Celsius |。。。数据温度=温度-温度刻度加倍。此外,与其定义n^2个转换例程,不如定义2n个例程,将每个温度转换为单个标准标度。例如,不定义华氏摄氏度,只使用华氏开尔文摄氏度。我应用了@chepner的建议,将转换从n^2减少到2N。温度应该是等式的一个实例,它不应该使用浮点。固定点似乎很适合这样做:决定你不关心任何小于百万分之一度的东西,并使用一个按百万放大的整数而不是浮点。使用这种方法,你肯定应该有一个角色注释。@d如果这个角色是幻影,对吗?这显然不是代表性的,但我不完全明白名义的意思。我不认为Eq本质上是一个问题,只要你不允许NaN转化为温度,你需要它来得到更有用的Ord实例。最大的浮点问题是Num,而温度不是Num。你需要注意的是,任何算术都是近似的。我应用了@chepner的建议,将n^2到2n的转换减少。温度应该是Eq的一个实例,它不应该使用浮点。固定点似乎很适合这样做:决定你不关心任何小于百万分之一度的东西,并使用一个按百万放大的整数而不是浮点。使用这种方法,你肯定应该有一个角色注释。@d如果这个角色是幻影,对吗?这显然不是代表性的,但我不完全明白名义的意思。我不认为Eq本质上是一个问题,只要你不允许NaN转化为温度,你需要它来得到更有用的Ord实例。最大的浮点问题是Num,而温度不是Num。你只需要注意一个事实,任何算术都是近似的。你的TIO链接到我的答案。那么,使用类型应用程序,你可以将类型作为参数传递吗?为什么要使用代理呢?对不起,我正在试着理解你的帖子里的内容。看看标签。理论上,标签根本不需要任何参数,它是TemperatureUnit的每个实例的常数。然而,如果标签只有类型字符串,那么类型参数u将根本不涉及,这使得您想要从哪个TemperatureUnit实例中提取标签变得模糊不清。代理是处理这个问题的标准黑客;代理基本上与相同,但类型参数仅适用于骑行,与u在温度中的方式相同。到目前为止,这些都不需要类型应用程序。继续。如果你像@Cubic那样使用AllowAmbigUstypes,你根本不需要这些代理把戏,但是关闭歧义检查比只启用类型应用程序语法更可怕,我想展示一个具有最少可怕扩展的解决方案。FWIW AllowAmbigUstypes听起来很可怕,但我不记得曾经有过这样的情况:我遇到过一个不明确的逐类型错误,你的TIO链接到我的答案,你可以通过类型应用程序将类型作为参数传递?为什么要使用代理呢?对不起,我正在试着理解你的帖子里的内容。看看标签。理论上,标签根本不需要任何参数 -它是温度单位的一个常数。然而,如果标签只有类型字符串,那么类型参数u将根本不涉及,这使得您想要从哪个TemperatureUnit实例中提取标签变得模糊不清。代理是处理这个问题的标准黑客;代理基本上与相同,但类型参数仅适用于骑行,与u在温度中的方式相同。到目前为止,这些都不需要类型应用程序。继续。如果你像@Cubic那样使用AllowAmbigUstypes,你根本不需要这些代理把戏,但是关闭歧义检查比只启用类型应用程序语法更可怕,我想展示一个具有最少可怕扩展的解决方案。FWIW AllowAmbiguousTypes听起来很可怕,但我不记得曾经有过这样一个案例,我偶然发现了一个模棱两可的类型