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听起来很可怕,但我不记得曾经有过这样一个案例,我偶然发现了一个模棱两可的类型