在Haskell中的Kilo Mega Giga之间转换的函数的通用实现

时间:2019-06-08 17:06:00

标签: haskell

假设我有以下代码:

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE TypeOperators #-}
module Lib
( someFunc
) where
import GHC.Generics
data DataAmount = KB Double | MB Double | GB Double deriving Generic
data Speed = KBs Double | MBs Double | GBs Double deriving Generic

convertToKB x = case x of
            (KB _ )-> x
            (MB k )-> KB (1000.0*k)            
            (GB k )-> KB (1000000.0*k)
convertToKBs x = case x of
             (KBs _) -> x
             (MBs k) -> KBs (1000.0*k)
             (GBs k) -> KBs (1000000.0*k)
class ConvertToK a where
 convertToK :: a->a

class ConvertToK' f where
 convertToK' :: f p -> ?

instance (ConvertToK' f,ConvertToK' g) => ConvertToK' (f :+: g) where
 convertToK' (L1 x) = ?
 convertToK' (R1 x) = ?

timeDiv (KB x) (KBs z) | z>0 = x/z
someFunc :: IO ()
someFunc = do
         putStrLn "Gime the amount of data:"
         dat <- readLn
         putStrLn "Gime 1 for KB 2 for MB 3 for GB:"
         unit <- readLn 
         let dataAmount = case unit of
                            1 -> KB dat
                            2 -> MB dat
                            3 -> GB dat
                            _ -> KB dat
         putStrLn "Gime speed of data:"
         speed <- readLn
         putStrLn "Gime 1 for KB/s 2 for MB/s 3 for GB/s:"
         speedunit <- readLn 
         let speedAmount = case speedunit of
                                1 -> KBs speed
                                2 -> MBs speed
                                3 -> GBs speed
                                _ -> KBs speed
         let speedAmountKBs = convertToKBs speedAmount
         let dataAmountKB = convertToKB dataAmount
         let result = timeDiv dataAmountKB speedAmountKBs
         putStrLn $ "You need " ++ show result ++ " seconds"

请注意,这里有3个问号,表示我不知道该怎么写。我只想创建一个转换函数以在Kilo,Mega和Giga之间进行转换,前提是一切都将转换为Kilo。例如,如果我有1 GB /秒,则它将变为1000000 KB /秒。我已经创建了两个这样的函数,每秒转换为千字节的convertToKB和转换为千字节的convertToKBs。两者的逻辑是相同的,如果某事是基洛什么也不做,如果某事是Mega乘以1000,如果Giga乘以1000000。我尝试使用泛型来做到这一点,但是我不能这样做,因为我需要使用名称数据构造函数,如果名称以“ K”开头,则如果以“ M” ...不执行任何操作。所有介绍Generics的示例以及hackage文档中的所有示例都与将类型转换为Bit或Bool的编码功能有关。在此示例中,遍历整个数据结构,并且将编码函数无差别地应用于各处。我还在派生泛型的程序包中发现了一个ConNames函数,但是没有如何使用它的示例。请帮忙。

4 个答案:

答案 0 :(得分:2)

我通常赞成在类型级别强制执行诸如单元等效性之类的事情。但是您还没有在这里做任何事情,所以我认为您当前的方法对于获得保证的水平来说太复杂了。

您可以从以下非常简单的代码中获得类似的保证:

someFunc :: IO ()
someFunc = do
         putStrLn "Gime the amount of data:"
         dat <- readLn
         putStrLn "Gime 1 for KB 2 for MB 3 for GB:"
         datunit <- readLn
         putStrLn "Gime speed of data:"
         speed <- readLn
         putStrLn "Gime 1 for KB/s 2 for MB/s 3 for GB/s:"
         speedunit <- readLn
         let result = (dat * 1000^datunit) / (speed * 1000^speedunit)
         putStrLn $ "You need " ++ show result ++ " seconds"

答案 1 :(得分:1)

请注意,这不是解决此问题的好方法。

但是,如果您真的想使用GHC.Generics定义泛型convertToK,请按照以下步骤操作。

我们需要很多扩展和一些模块:

{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wall #-}

import Control.Applicative
import Data.Maybe
import Generics.Deriving.ConNames
import GHC.Generics

我们将定义一个Prefix数据类型,由以下给出:

data Prefix = K | M | G deriving (Show, Read, Eq)

我们的目标是为Scalable类型的类定义一个泛型转换函数,该函数使用三个泛型函数:(1)prefix获得一个词项的Prefix ; (2)value使Double隐藏在内部,无论其前缀如何; (3)makeK建立正确类型的公斤值。可以通过以下通用函数轻松定义通用转换:

convertToK :: (Scalable a) => a -> a
convertToK x = case prefix x of
  K -> x
  M -> makeK (1000 * v)
  G -> makeK (1000000 * v)
  where v = value x

这是带有这些函数及其签名的类。

class Scalable a where
  prefix :: a -> Prefix   -- get the unit prefix
  value  :: a -> Double   -- get value regardless of prefix
  makeK  :: Double -> a   -- create a "kilo" value (i.e., the "kilo" constructor)

我们可以用prefix作弊,因为generic-deriving已经提供了conNameOf函数来获取术语构造函数的名称。我们可以使用此类中的以下默认实现将第一个字符提取并read转换为Prefix值:

  -- within class Scalable
  default prefix :: (Generic a, ConNames (Rep a)) => a -> Prefix
  prefix = read . take 1 . conNameOf

对于value泛型函数,value' :: f x -> Double函数将以通常的方式分派给Value'函数(在下面的GHC.Generics类型类中定义):

  -- within class Scalable
  default value :: (Generic a, Value' (Rep a)) => a -> Double
  value = value' . from

makeK函数稍微复杂一些。它在MakeK'类型类中的通用版本具有签名Double -> Maybe (f x),表示如果递归地找到正确的构造函数,则它可能可能创建一个千位值。因此,此默认定义仅使makeK适应该签名。下面会更清楚。

  -- within class Scalable
  default makeK :: (Generic a, MakeK' (Rep a)) => Double -> a
  makeK = to . fromJust . makeK'

Value'类是一个相对简单的泛型函数:

class Value' f where
  value' :: f x -> Double

我们通过沿着该术语表示的任何分支递归来处理求和类型:

instance (Value' f, Value' g) => Value' (f :+: g) where
  value' (L1 x) = value' x
  value' (R1 x) = value' x

最终,我们将递归到Double并将其返回:

instance Value' (K1 c Double) where
  value' (K1 x) = x

当然,我们不需要任何元信息,但是我们需要一个实例来跳过它:

instance (Value' f) => Value' (M1 i t f) where
  value' (M1 x) = value' x

请注意,除了Double之外,我们没有为V1,U1和K1保留实例。我们还省略了(:*:)个产品类型。我们不打算将此类用于包含任何这些形式的类型。

现在,我们转到MakeK'类的定义。这个结构的结构大不相同,因为我们没有解构一个具体的术语,而是通过寻找以Double为开头的构造函数,尝试从class MakeK' f where makeK' :: Double -> Maybe (f x) build 一个具体术语。 “ K”并使用它。

instance (MakeK' f, MakeK' g) => MakeK' (f :+: g) where
  makeK' n = L1 <$> makeK' n <|> R1 <$> makeK' n

第一个关键点是如何处理总和类型。我们尝试通过将“ K”项构建为总和的左分支来构建其总和类型。如果成功(通过返回“ Just”值),我们就知道已经找到并使用了“ K”构造函数。否则,我们尝试使用正确的分支。 (如果同样失败,则递归中必须有一些更高级别的分支才能成功,因此我们只需返回“ Nothing”即可使其工作。)

makeK'

第二个关键点是我们如何找到“ K”构造函数。我们使用以下实例浏览“ C1”节点上的构造函数元数据。设置为重叠,因为它应优先于忽略非构造函数元数据的常规元数据实例。您可以看到isK依赖于布尔值isK,表明我们找到了“ K”构造函数。如果Nothing为假,我们将停止搜索并返回Double。否则,我们递归到内容中。基本上,构造函数元数据充当一种看门人的角色,它仅允许来自“ K”构造函数的Nothing进入,并使所有其他构造函数instance {-# OVERLAPPING #-} (Constructor c, MakeK' f) => MakeK' (C1 c f) where makeK' n | isK = M1 <$> makeK' n | otherwise = Nothing 进入。这就是我们最后得到正确的基于“ K”的术语的方式。它可能看起来有些倒退,但这似乎是正确的方法:

isK

函数undefined本身有点棘手。请记住,我们并没有解构一个实际术语。取而代之的是,我们正在考虑是否构建一个,因此我们在此处仅使用conName占位符作为其类型,以便可以对其调用isK以获得该分支的构造函数名称。如果其第一个字母为“ K”,则将 where isK = head (conName (undefined :: C1 c f x)) == 'K' 设置为true。

instance MakeK' f => MakeK' (M1 i t f) where
  makeK' n = M1 <$> makeK' n

如上所述,我们需要忽略非构造函数元数据:

Double

,我们需要在找到Double时进行处理。请注意,我们在这里无条件构造它。递归中更远的构造函数元数据已经决定我们是正确构造函数的instance MakeK' (K1 c Double) where makeK' n = Just $ K1 n

Scalable

无论如何,毕竟,我们可以定义数据类型并使它们成为data DataAmount = KB Double | MB Double | GB Double deriving (Generic, Show) data Speed = KBs Double | MBs Double | GBs Double deriving (Generic, Show) instance Scalable DataAmount instance Scalable Speed 类的实例:

timeDiv (KB x) (KBs z) | z>0 = x/z
someFunc :: IO ()
someFunc = do
  putStrLn "Gime the amount of data:"
  dat <- readLn
  putStrLn "Gime 1 for KB 2 for MB 3 for GB:"
  unit <- readLn
  let dataAmount = case unit of
                     1 -> KB dat
                     2 -> MB dat
                     3 -> GB dat
                     _ -> KB dat
  putStrLn "Gime speed of data:"
  speed <- readLn
  putStrLn "Gime 1 for KB/s 2 for MB/s 3 for GB/s:"
  speedunit <- readLn
  let speedAmount = case speedunit of
                         1 -> KBs speed
                         2 -> MBs speed
                         3 -> GBs speed
                         _ -> KBs speed
  let speedAmountKBs = convertToK speedAmount
  let dataAmountKB = convertToK dataAmount
  let result = timeDiv dataAmountKB speedAmountKBs
  putStrLn $ "You need " ++ show result ++ " seconds"

程序的其余部分如下所示:

Scalable

这种方法显然有很多错误:

  • 写起来很麻烦而且很复杂。您需要很多实例才能使它值得。
  • 效率很低,因为转换需要多次通过表示树。
  • 输入不安全。首先,如果我们在不遵守命名约定的数据类型上定义convertToK实例,则会导致运行时错误。其次,在您的程序中,所传递的不同单元中没有类型安全性。如果您删除一个或两个timeDiv调用,该程序仍将键入check,但当{-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -Wall #-} import Control.Applicative import Data.Maybe import Generics.Deriving.ConNames import GHC.Generics data Prefix = K | M | G deriving (Show, Read, Eq) convertToK :: (Scalable a) => a -> a convertToK x = case prefix x of K -> x M -> makeK (1000 * v) G -> makeK (1000000 * v) where v = value x class Scalable a where prefix :: a -> Prefix -- get the unit prefix default prefix :: (Generic a, ConNames (Rep a)) => a -> Prefix prefix = read . take 1 . conNameOf value :: a -> Double -- get value regardless of prefix default value :: (Generic a, Value' (Rep a)) => a -> Double value = value' . from makeK :: Double -> a -- create a "kilo" value (i.e., the "kilo" constructor) default makeK :: (Generic a, MakeK' (Rep a)) => Double -> a makeK = to . fromJust . makeK' class Value' f where value' :: f x -> Double instance (Value' f, Value' g) => Value' (f :+: g) where value' (L1 x) = value' x value' (R1 x) = value' x instance Value' (K1 c Double) where value' (K1 x) = x instance (Value' f) => Value' (M1 i t f) where value' (M1 x) = value' x class MakeK' f where makeK' :: Double -> Maybe (f x) instance (MakeK' f, MakeK' g) => MakeK' (f :+: g) where makeK' n = L1 <$> makeK' n <|> R1 <$> makeK' n instance {-# OVERLAPPING #-} (Constructor c, MakeK' f) => MakeK' (C1 c f) where makeK' n | isK = M1 <$> makeK' n | otherwise = Nothing where isK = head (conName (undefined :: C1 c f x)) == 'K' instance MakeK' f => MakeK' (M1 i t f) where makeK' n = M1 <$> makeK' n instance MakeK' (K1 c Double) where makeK' n = Just $ K1 n data DataAmount = KB Double | MB Double | GB Double deriving (Generic, Show) data Speed = KBs Double | MBs Double | GBs Double deriving (Generic, Show) instance Scalable DataAmount instance Scalable Speed timeDiv (KB x) (KBs z) | z>0 = x/z someFunc :: IO () someFunc = do putStrLn "Gime the amount of data:" dat <- readLn putStrLn "Gime 1 for KB 2 for MB 3 for GB:" unit <- readLn let dataAmount = case unit of 1 -> KB dat 2 -> MB dat 3 -> GB dat _ -> KB dat putStrLn "Gime speed of data:" speed <- readLn putStrLn "Gime 1 for KB/s 2 for MB/s 3 for GB/s:" speedunit <- readLn let speedAmount = case speedunit of 1 -> KBs speed 2 -> MBs speed 3 -> GBs speed _ -> KBs speed let speedAmountKBs = convertToK speedAmount let dataAmountKB = convertToK dataAmount let result = timeDiv dataAmountKB speedAmountKBs putStrLn $ "You need " ++ show result ++ " seconds" 尝试使用未转换的值时如果模式匹配失败,则{{1}}可能会生成运行时错误。

无论如何,完整的参考程序是:

{{1}}

答案 2 :(得分:0)

好的,这是第二个“答案”,试图提供我认为是解决此问题的更好方法。解决方案2可能值得认真对待。解决方案#3-#5显示了越来越复杂(并且越来越类型安全)的数据类型前缀表示方式。

无论如何,这是我对您的要求的理解。

  1. 您想以各种基本单位(例如,每秒字节和字节)表示各种物理测量(例如,数据量和传输速度),并带有各种“度量前缀”(千位,兆)。
  2. 对于计算(例如,计算传输时间),您希望能够以简单,统一的方式处理输入参数上度量前缀的任何混合形式。例如,您不想写:

    timeDiv (KB x) (KBs z) | z > 0 = x / z
    timeDiv (MB x) (KBs z) | z > 0 = x*1000 / z
    ...all 9 combinations...
    timeDiv (GB x) (GBs z) | z > 0 = x / z
    
  3. 您也不想为每个可能的单元分别编写一个convertToKXXX函数。

此外,尽管它不是您要求的明确内容,但我要补充一点:

  1. 您要使其以基本单位键入安全,这意味着timeDiv不能将两个DataAmounts分开,也不能倒退(例如,将Speed除以`DataAmount)。
  2. 您想使它在前缀中输入安全,这意味着您应该通过在转换前忘记转换GB而无法得到错误的答案或使程序崩溃到timeDiv

请注意,您当前的方法在第3点上失败了(这就是为什么您首先问这个问题的原因),但在第5点上也失败了。例如,没有什么可以阻止您编写:

badMain = print $ timeDiv (GB 1000) (MBs 100)

它可以很好地编译,然后在运行时给出一个非穷尽的模式错误,因为两个参数尚未转换为公斤。

那么,有什么更好的解决方案?

解决方案1:以基本单位统一表示

这是一个显而易见的解决方案,很容易忽略。如果仅在核心逻辑的输入和输出“边界”中需要度量标准前缀,则实际上可能不需要将度量标准前缀表示为数据类型的一部分。也就是说,考虑是否可以用一种类型的标准单位来表示不同物理量的值,每种类型只有一个真正的构造函数:

newtype DataAmount = B Double  -- in bytes
newtype Speed = Bs Double      -- in bytes per second

这使定义类型安全的timeDiv变得容易(很好,相对类型安全,因为我们仍然拒绝负速度)。实际上,我们还应该引入一种时间类型:

newtype Time = S Double deriving (Show)    -- in seconds

timeDiv :: DataAmount -> Speed -> Time
timeDiv (B x) (Bs z) | z > 0     = S (x / z)
                     | otherwise = error "timeDiv: non-positive Speed"

为进行缩放,我们为前缀引入一种类型(其中I表示没有前缀的“身份”):

data Prefix = I | K | M | G deriving (Show, Read)

和一个类型类,用于处理以前缀为单位的值的输入和输出。类型类仅需要与假定为非前缀单位的Double值之间进行转换:

class Scalable a where
  toScalable :: Double -> a
  fromScalable :: a -> Double

以及一些繁琐的实例模板:

instance Scalable DataAmount where
  toScalable = B
  fromScalable (B x) = x
instance Scalable Speed where
  toScalable = Bs
  fromScalable (Bs x) = x
instance Scalable Time where
  toScalable = S
  fromScalable (S x) = x

然后,我们可以定义:

fromPrefix :: (Scalable a) => Prefix -> Double -> a
fromPrefix I x = toScalable x
fromPrefix K x = toScalable (1e3 * x)
fromPrefix M x = toScalable (1e6 * x)
fromPrefix G x = toScalable (1e9 * x)

toPrefix :: (Scalable a) => Prefix -> a -> Double
toPrefix I x = fromScalable x
toPrefix K x = fromScalable x / 1e3
toPrefix M x = fromScalable x / 1e6
toPrefix G x = fromScalable x / 1e9

允许我们编写如下内容:

-- what is time in kiloseconds to transfer 100G over 10MB/s?
doStuff = print $ toPrefix K $ timeDiv (fromPrefix G 100) (fromPrefix M 10)

,我们将按照以下方式重写您的主程序(进行修改,以利用Read的{​​{1}}实例:

Prefix

解决方案2:忘记类型类

实际上,即使上述解决方案也可能是过度设计的。您无需类型类即可完成所有操作。尝试像以前一样定义类型和前缀以及someFunc :: IO () someFunc = do putStrLn "Gime the amount of data:" dat <- readLn putStrLn "Gime K for KB, M for MB, G for GB:" unit <- readLn let dataAmount = fromPrefix unit dat putStrLn "Gime speed of data:" speed <- readLn putStrLn "Gime K for KB/s M for MB/s G for GB/s:" speedunit <- readLn let speedAmount = fromPrefix speedunit speed let S result = timeDiv dataAmount speedAmount putStrLn $ "You need " ++ show result ++ " seconds"

timeDiv

但使用:

newtype DataAmount = B Double deriving (Show)  -- in bytes
newtype Speed = Bs Double deriving (Show)      -- in bytes per second
newtype Time = S Double deriving (Show)        -- in seconds
data Prefix = I | K | M | G deriving (Show, Read)
timeDiv :: DataAmount -> Speed -> Time
timeDiv (B x) (Bs z) | z > 0     = S (x / z)
                     | otherwise = error "timeDiv: non-positive Speed"

这允许:

fromPrefix :: Double -> Prefix -> (Double -> a) -> a
fromPrefix x p u = u (scale p x)
  where scale I = id
        scale K = (1e3*)
        scale M = (1e6*)
        scale G = (1e9*)

,您可以将neatFunc :: IO () -- divide 100 GB by 100 MB/s neatFunc = print $ timeDiv (fromPrefix 100 G B) (fromPrefix 10 M Bs) 重写为:

someFunc

如果没有类型类(例如,提供someFunc :: IO () someFunc = do putStrLn "Gime the amount of data:" dat <- readLn putStrLn "Gime K for KB, M for MB, G for GB:" unit <- readLn let dataAmount = fromPrefix dat unit B putStrLn "Gime speed of data:" speed <- readLn putStrLn "Gime K for KB/s M for MB/s G for GB/s:" speedunit <- readLn let speedAmount = fromPrefix speed speedunit Bs let S result = timeDiv dataAmount speedAmount putStrLn $ "You need " ++ show result ++ " seconds" ),则很难写toPrefix,但也许足够了:

fromScalable

因此您可以通过在unPrefix :: Prefix -> Double -> Double unPrefix I x = x unPrefix K x = x/1e3 unPrefix M x = x/1e6 unPrefix G x = x/1e9 构造函数上使用以下方式手动进行模式匹配来计算毫秒:

S

解决方案3:前缀的共享表示形式

如果您确定确实希望将前缀作为数据表示的一部分,那么避免大量不必要的样板的最简单方法是将表示物理量的类型与表示前缀值的类型分开。也就是说,让我们定义一个无单位但前缀为example1 = print $ ks -- answer in kiloseconds where ks = let S s = timeDiv (fromPrefix 100 G B) (fromPrefix 10 M Bs) in unPrefix K s 的类型,该类型可以在不同的物理量之间共享,例如:

Value

然后,我们的物理量是围绕data Value = Value Prefix Double deriving (Show) data Prefix = I | K | M | G deriving (Show, Read) 而不是Value的包装。我们可以使用基本单位(Double表示字节等)来命名构造函数:

B

newtype DataAmount = B Value newtype Speed = Bs Value newtype Time = S Value deriving (Show) 类型而不是convertToKconvertToI定义Value(或为了简单起见,DataAmount转换为基本单位):< / p>

Speed

现在,我们可以定义convertToI :: Value -> Value convertToI v@(Value I _) = v convertToI (Value K x) = Value I (x*1e3) convertToI (Value M x) = Value I (x*1e6) convertToI (Value G x) = Value I (x*1e9) 的版本,该版本只能在无前缀的单元上运行:

timeDivI

以及可以处理任何前缀的更通用的版本:

timeDivI :: DataAmount -> Speed -> Time
timeDivI (B (Value I x)) (Bs (Value I z))
  | z > 0      = S (Value I (x/z))
  | otherwise = error "timeDiv: non-positive Speed"

,我们可以将您的timeDiv :: DataAmount -> Speed -> Time timeDiv (B bytes) (Bs bps) = timeDivI (B (convertToI bytes)) (Bs (convertToI bps)) 改写为:

someFunc

这很好。它满足要求1-4,并且非常接近要求5。someFunc :: IO () someFunc = do putStrLn "Gime the amount of data:" dat <- readLn putStrLn "Gime K for KB, M for MB, G for GB:" unit <- readLn let dataAmount = B (Value unit dat) putStrLn "Gime speed of data:" speed <- readLn putStrLn "Gime K for KB/s M for MB/s G for GB/s:" speedunit <- readLn let speedAmount = Bs (Value speedunit speed) let s = timeDiv dataAmount speedAmount putStrLn $ "You need time " ++ show s 类型不安全(与上述timeDivI相同),但是我们可以将其隐藏在badMain中safe where函数类型下的子句,该子句处理所有可能的输入。基本上,这为用户的函数提供了良好的类型安全性,但并未为开发它们提供太多的类型安全性。

解决方案4:使用DataKinds表示类型的前缀

我们可以通过使用timeDiv将前缀提高到类型级别来提高类型安全性。这是以显着增加复杂性为代价的。

借助某些扩展程序:

DataKinds

我们可以定义一组前缀的{-# LANGUAGE DataKinds, KindSignatures #-} 类型:

Value

通过“标签”类型为前缀编制索引:

newtype Value (p :: Prefix) = Value Double

这使我们可以定义以前的一组物理量:

data Prefix = I | K | M | G deriving (Show, Read)

现在,类型newtype DataAmount p = B (Value p) newtype Speed p = Bs (Value p) newtype Time p = S (Value p) 是千兆字节的数据量,而DataAmount G是(无前缀)秒的时间值。

与您的原始Time I函数等效,或多或少:

timeDiv

这是安全类型。您不能无意间以千兆字节的数据量或千字节/秒的速度调用它,也不能将返回值误用千分之一秒使用-所有这些都会在编译时失败。但是,虽然很容易定义单个转换函数,例如:

timeDiv :: DataAmount K -> Speed K -> Time I
timeDiv (B (Value kb)) (Bs (Value kbs)) = S (Value (kb/kbs))

尝试定义处理所有前缀的常规convertMToK :: Value M -> Value K convertMToK (Value m) = Value (1e3*m)

convertToK

最终变得困难(即不可能)。

相反,我们需要以一种可以在运行时提取前缀信息的方式定义convertToK :: Value p -> Value K ,但要以类型安全的方式。这需要使用GADT,因此让我们尝试使用更多扩展名:

Value

,并将{-# LANGUAGE DataKinds, GADTs, KindSignatures, RankNTypes, StandaloneDeriving #-} 定义为具有每个前缀的构造函数的GADT:

Value

我们的物理量定义如下:

data Value (p :: Prefix) where
  IValue :: Double -> Value I
  KValue :: Double -> Value K
  MValue :: Double -> Value M
  GValue :: Double -> Value G
data Prefix = I | K | M | G deriving (Show, Read)
deriving instance Show (Value p)

但是该GADT允许我们像这样定义newtype DataAmount p = B (Value p) newtype Speed p = Bs (Value p) newtype Time p = S (Value p) deriving (Show) 函数:

convertToI

现在,我们可以定义类型安全的convertToI :: Value p -> Value I convertToI i@(IValue _) = i -- no conversion needed convertToI (KValue x) = IValue (1e3*x) convertToI (MValue x) = IValue (1e6*x) convertToI (GValue x) = IValue (1e9*x) ,该类型安全类型适用于timeDivI除以DataAmount的任何基数(无前缀):

Speed

和通用的(且类型安全的)timeDivI :: DataAmount I -> Speed I -> Time I timeDivI (B (IValue bytes)) (Bs (IValue bps)) | bps > 0 = S (IValue (bytes / bps)) | otherwise = error "TODO: replace with enterprisey exception" 可以处理带有timeDiv的任何输入前缀带有convertToI的任何输出前缀(如下所示) convertFromI的含义):

KnownPrefix

事实证明timeDiv :: (KnownPrefix p3) => DataAmount p1 -> Speed p2 -> Time p3 timeDiv (B bytes) (Bs bps) = let S v = timeDivI (B (convertToI bytes)) (Bs (convertToI bps)) in S (convertFromI v) 很难写。它需要使用单例。 (要了解原因,请尝试编写函数convertFromI并了解您可以得到多少...)

无论如何,单例被定义为GADT:

convertFromI :: Value I -> Value p

我们可以编写一个data SPrefix p where SI :: SPrefix I SK :: SPrefix K SM :: SPrefix M SG :: SPrefix G deriving instance Show (SPrefix p) 版本,该版本接受一个显式单例以执行正确的转换:

convertFromI'

然后,我们可以通过使用标准类型类技巧来避免实际提供显式单例的需求:

convertFromI' :: SPrefix p -> Value I -> Value p
convertFromI' SI v = v
convertFromI' SK (IValue base) = KValue (base/1e3)
convertFromI' SM (IValue base) = MValue (base/1e6)
convertFromI' SG (IValue base) = GValue (base/1e9)

写:

class    KnownPrefix p where singPrefix :: SPrefix p
instance KnownPrefix I where singPrefix = SI
instance KnownPrefix K where singPrefix = SK
instance KnownPrefix M where singPrefix = SM
instance KnownPrefix G where singPrefix = SG

这个基础架构很棒(有些讽刺意味)。观察:

convertFromI :: (KnownPrefix p) => Value I -> Value p
convertFromI = convertFromI' singPrefix

此打印:

awesomeFunc = do
  let dat   = B (GValue 1000) :: DataAmount G  -- 1000 gigabytes
      speed = Bs (MValue 100) :: Speed M       -- 100 megabytes
      -- timeDiv takes args w/ arbitrary prefixes...
      time1 = timeDiv dat speed :: Time I  -- seconds
      -- ...and can return values w/ arbitrary prefixes.
      time2 = timeDiv dat speed :: Time K  -- kiloseconds
      -- ...
  print (time1, time2)

它也非常安全。只是尝试打破它...

尽管如此,尽管看起来很复杂,但很可能这是处理生产代码中单位前缀表示形式的最佳类型安全方式。类型安全性和可重复使用的转换功能是很大的好处。

不幸的是,当在编译时知道前缀时,这种方法最有效。要重写您的> awesomeFunc (S (IValue 10000.0),S (KValue 10.0)) ,我们需要一种表示someFunc的前缀,直到运行时才知道其前缀。标准方法是一种存在类型,它同时包含前缀(作为singeton)和值:

Value

要使用data SomeValue where SomeValue :: SPrefix p -> Value p -> SomeValue deriving instance Show SomeValue 术语,我们希望有一种方法可以从SomeValueDouble创建这种类型的值:

Prefix

,我们会发现定义一个有助于在需要someValue :: Double -> Prefix -> SomeValue someValue x I = SomeValue SI (IValue x) someValue x K = SomeValue SK (KValue x) someValue x M = SomeValue SM (MValue x) someValue x G = SomeValue SG (GValue x) 的地方使用SomeValue的函数很有帮助:

Value

现在我们可以写:

withSomeValue :: SomeValue -> (forall p . Value p -> a) -> a
withSomeValue sv f = case sv of
  SomeValue SI v -> f v
  SomeValue SK v -> f v
  SomeValue SM v -> f v
  SomeValue SG v -> f v

该解决方案的一个缺点是我们无法将someFunc :: IO () someFunc = do putStrLn "Gime the amount of data:" dat <- readLn putStrLn "Gime K for KB, M for MB, G for GB:" unit <- readLn let dataAmount = someValue dat unit :: SomeValue putStrLn "Gime speed of data:" speed <- readLn putStrLn "Gime K for KB/s M for MB/s G for GB/s:" speedunit <- readLn let speedAmount = someValue speed speedunit :: SomeValue withSomeValue dataAmount $ \d -> withSomeValue speedAmount $ \s -> do let S (KValue ks) = timeDiv (B d) (Bs s) :: Time K putStrLn $ "You need " ++ show ks ++ " kiloseconds" 直接解析为dataAmount类型,因为不存在与DataAmount等价的SomeDataAmount存在。结果,在将SomeValue定义为任意dataAmount到将其传递给{{1}之前用SomeValue构造函数包装它之间,存在类型安全性“差距” }。换句话说,我们在要求4方面做得不好。一种解决方案是定义BtimeDiv,依此类推,但这非常繁琐。另一个解决方案是在类型级别将更多信息提升为“标签”。

解决方案5:将数量和单位提升到类型水平

如果上述所有内容看起来都太简单了,那么真正的工业强度的“企业”解决方案将是在单个通用SomeDataAmount类型中以类型级别表示物理量,其单位和其前缀。

具有大量语言扩展:

SomeSpeed

我们将定义一组Value类型的类型,这些类型由物理量和前缀标记。 {-# LANGUAGE DataKinds, GADTs, KindSignatures, PolyKinds, RankNTypes, StandaloneDeriving, TypeFamilies #-} 将是GADT,以允许在运行时检查前缀:

Value

单位在哪里?好吧,因为物理量决定了单位,所以我们将使用类型族将Value映射到data Value (q :: Quantity) (p :: Prefix) where IValue :: Double -> Value q I KValue :: Double -> Value q K MValue :: Double -> Value q M GValue :: Double -> Value q G data Quantity = DataAmount | Speed | Time | FileSize data Prefix = I | K | M | G deriving (Show, Read) deriving instance Show (Value q p) 。这确实允许不同的物理数量类型(例如QuantityUnit)共享单位:

DataAmount

像以前一样,FileSize GADT允许我们定义一个data Unit = B | Bs | S deriving (Show) type family QuantityUnit q where QuantityUnit DataAmount = B QuantityUnit FileSize = B QuantityUnit Speed = Bs QuantityUnit Time = S 转换为基本单位:

Value

现在,我们可以定义一个类型安全的convertToI,它适用于以秒为单位的任何基本(无前缀)字节除法,无论涉及哪种物理量(只要其单位正确):

convertToI :: Value q p -> Value q I
convertToI i@(IValue _) = i   -- no conversion needed
convertToI   (KValue x) = IValue (1e3*x)
convertToI   (MValue x) = IValue (1e6*x)
convertToI   (GValue x) = IValue (1e9*x)

此外,这是一个通用的类型安全timeDivI,可以处理任何输入和输出前缀:

timeDivI :: (QuantityUnit bytes ~ B, QuantityUnit bps ~ Bs, QuantityUnit secs ~ S)
         => Value bytes I -> Value bps I -> Value secs I
timeDivI (IValue bytes) (IValue bps)
  | bps > 0   = IValue (bytes / bps)
  | otherwise = error "TODO: replace with enterprisey exception"

和以前一样,timeDiv需要采用单例方法:

timeDiv :: (QuantityUnit bytes ~ B, QuantityUnit bps ~ Bs, QuantityUnit secs ~ S, KnownPrefix p3)
         => Value bytes p1 -> Value bps p2 -> Value secs p3
timeDiv bytes bps = convertFromI $ timeDivI (convertToI bytes) (convertToI bps)

此基础架构比以前更强大:

convertFromI

此打印:

data SPrefix p where
  SI :: SPrefix I
  SK :: SPrefix K
  SM :: SPrefix M
  SG :: SPrefix G
deriving instance Show (SPrefix p)
convertFromI' :: SPrefix p -> Value q I -> Value q p
convertFromI' SI v = v
convertFromI' SK (IValue base) = KValue (base/1000)
convertFromI' SM (IValue base) = MValue (base/1000)
convertFromI' SG (IValue base) = GValue (base/1000)

class    KnownPrefix p where singPrefix :: SPrefix p
instance KnownPrefix I where singPrefix = SI
instance KnownPrefix K where singPrefix = SK
instance KnownPrefix M where singPrefix = SM
instance KnownPrefix G where singPrefix = SG

convertFromI :: (KnownPrefix p) => Value q I -> Value q p
convertFromI = convertFromI' singPrefix

同样,要重写您的awesomerFunc = do let dat = GValue 1000 :: Value DataAmount G -- 1000 gigabytes of data fs = MValue 15 :: Value FileSize M -- 15 megabytes in file speed = MValue 100 :: Value Speed M -- 100 MB/s -- timeDiv works with DataAmount... time1 = timeDiv dat speed :: Value Time I -- seconds -- ...and FileSize, with args having arbitrary prefixes... time2 = timeDiv fs speed :: Value Time K -- kiloseconds -- ...and can return values w/ arbitrary prefixes. print (time1, time2) ,我们需要一个存在的版本:

> awesomerFunc
(IValue 10000.0,KValue 1.5e-4)
>

现在我们可以写:

someFunc

节目列表

以下是最简单(#2)和最复杂(#5)解决方案的程序清单:

data SomeValue q where
  SomeValue :: SPrefix p -> Value q p -> SomeValue q
deriving instance Show (SomeValue q)

someValue :: Double -> Prefix -> SomeValue q
someValue x I = SomeValue SI (IValue x)
someValue x K = SomeValue SK (KValue x)
someValue x M = SomeValue SM (MValue x)
someValue x G = SomeValue SG (GValue x)

withSomeValue :: SomeValue q -> (forall p . Value q p -> a) -> a
withSomeValue sv f = case sv of
  SomeValue SI v -> f v
  SomeValue SK v -> f v
  SomeValue SM v -> f v
  SomeValue SG v -> f v
someFunc :: IO ()
someFunc = do
  putStrLn "Gime the amount of data:"
  dat <- readLn
  putStrLn "Gime K for KB, M for MB, G for GB:"
  unit <- readLn
  let dataAmount = someValue dat unit :: SomeValue DataAmount
  putStrLn "Gime speed of data:"
  speed <- readLn
  putStrLn "Gime K for KB/s M for MB/s G for GB/s:"
  speedunit <- readLn
  let speedAmount = someValue speed speedunit :: SomeValue Speed
  withSomeValue dataAmount $ \d -> withSomeValue speedAmount $ \s -> do
    let KValue ks = timeDiv d s :: Value Time K
    putStrLn $ "You need " ++ show ks ++ " kiloseconds"

答案 3 :(得分:0)

这不是一个完整的答案,但值得深思。 @ K.A.Buhr启发了我,我使用一种通用类型来表示Kilo,Mega等,然后我使用SYB创建了一个通用转换。但是我认为这不是完全类型安全的,这是我的代码:

{-# LANGUAGE DeriveDataTypeable #-}
module Lib
( someFunc
) where
import Data.Generics.SYB
import Data.Generics.Uniplate.Data
import Data.Typeable
import Data.Data
someFunc :: IO ()
someFunc = do
        let speed = Sp (M 11.0)
        let (Sp c) = convert speed
        let k = case c of
                 K x -> x
                 M x -> x
                 G x -> x
        print k
 data KMBG = K Double|M Double | G Double deriving(Data,Typeable)

 data Speed = Sp KMBG deriving(Data,Typeable)
 data Size = Ss Double deriving (Data,Typeable)


 baseConvert (K x) = K x
 baseConvert (M x) = K (1000*x)
 baseConvert (G x) = K (1000000*x)


 convert :: (Data a)=>a->a
 convert = everywhere (mkT baseConvert)

我们可以限制convert仅在以KMBG作为其缩放前缀的类型上使用吗?