假设我有以下代码:
{-# 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函数,但是没有如何使用它的示例。请帮忙。
答案 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显示了越来越复杂(并且越来越类型安全)的数据类型前缀表示方式。
无论如何,这是我对您的要求的理解。
对于计算(例如,计算传输时间),您希望能够以简单,统一的方式处理输入参数上度量前缀的任何混合形式。例如,您不想写:
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
您也不想为每个可能的单元分别编写一个convertToKXXX
函数。
此外,尽管它不是您要求的明确内容,但我要补充一点:
timeDiv
不能将两个DataAmounts
分开,也不能倒退(例如,将Speed
除以`DataAmount)。GB
而无法得到错误的答案或使程序崩溃到timeDiv
。请注意,您当前的方法在第3点上失败了(这就是为什么您首先问这个问题的原因),但在第5点上也失败了。例如,没有什么可以阻止您编写:
badMain = print $ timeDiv (GB 1000) (MBs 100)
它可以很好地编译,然后在运行时给出一个非穷尽的模式错误,因为两个参数尚未转换为公斤。
那么,有什么更好的解决方案?
这是一个显而易见的解决方案,很容易忽略。如果仅在核心逻辑的输入和输出“边界”中需要度量标准前缀,则实际上可能不需要将度量标准前缀表示为数据类型的一部分。也就是说,考虑是否可以用一种类型的标准单位来表示不同物理量的值,每种类型只有一个真正的构造函数:
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
实际上,即使上述解决方案也可能是过度设计的。您无需类型类即可完成所有操作。尝试像以前一样定义类型和前缀以及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
如果您确定确实希望将前缀作为数据表示的一部分,那么避免大量不必要的样板的最简单方法是将表示物理量的类型与表示前缀值的类型分开。也就是说,让我们定义一个无单位但前缀为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)
类型而不是convertToK
和convertToI
定义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
函数类型下的子句,该子句处理所有可能的输入。基本上,这为用户的函数提供了良好的类型安全性,但并未为开发它们提供太多的类型安全性。
我们可以通过使用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
术语,我们希望有一种方法可以从SomeValue
和Double
创建这种类型的值:
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方面做得不好。一种解决方案是定义B
和timeDiv
,依此类推,但这非常繁琐。另一个解决方案是在类型级别将更多信息提升为“标签”。
如果上述所有内容看起来都太简单了,那么真正的工业强度的“企业”解决方案将是在单个通用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)
。这确实允许不同的物理数量类型(例如Quantity
和Unit
)共享单位:
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作为其缩放前缀的类型上使用吗?