Haskell:减少样板

时间:2019-04-30 15:49:30

标签: 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   = 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可以做什么?或暗示它仅适用于已声明实例的类型。

2 个答案:

答案 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

Try it online!

这里的窍门是DataKinds允许我们将常规数据类型提升为类型级别,并将其数据构造函数提升为类型级别(据我了解,在现代版本的GHC中,这已经不再是真正的不同了)对不起,我本人对此感到有些困惑。然后,我们仅定义一个帮助器类以获取单元的数据版本,以便我们可以基于它进行调度。这使我们可以尝试使用所有新类型包装器进行操作,除了更少的新类型包装器(更少的实例声明和更少的命名函数)。

当然,另一件事是,在不同的单位转换之间仍然存在组合爆炸式增长-您可以将其吸收并手动编写所有n^2公式,也可以尝试将其概括化(可能可以根据@chepner的评论使用温度单位,但我不确定您是否想在所有类型之间进行转换)。这种方法无法解决固有的问题,但确实可以消除您使用newtype-每单元方法带来的一些语法噪声。

您的increaseTempdecreaseTemp函数可以实现为单个函数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

Try it online!


据我所知,这种情况下数据类型方法的优点是,如果出于其他目的需要TemperatureUnit数据类型,则可以重用它,而不必同时定义{{1} }等类型,就像我在这里所做的那样。它还将可能的温度类型限制为您在data Fahrenheit类型中定义的温度类型,这可能对您有利或不利。例如,最好进行一次额外的类型检查,使您无法使用TemperatureUnit,但是这种错误很可能会被其他地方的编译器捕获,尽管错误可能不太明显。而且,如果要导出此功能,则可能需要开放的温度类型世界,以便下游模块可以添加自己的温度类型。

因此,如果您还没有在其他地方使用TemperatureUnit Bool类型,则IMO不使用数据类型会更简单,更灵活。