Haskell 如何避免类型类实例的二次爆炸?
考虑:Haskell 如何避免类型类实例的二次爆炸?,haskell,typeclass,functional-dependencies,Haskell,Typeclass,Functional Dependencies,考虑: {-# OPTIONS -fglasgow-exts #-} data Second = Second data Minute = Minute data Hour = Hour -- Look Ma', a phantom type! data Time a = Time Int instance Show (Time Second) where show (Time t) = show t ++ "sec" instance Show (Time Minute) whe
{-# OPTIONS -fglasgow-exts #-}
data Second = Second
data Minute = Minute
data Hour = Hour
-- Look Ma', a phantom type!
data Time a = Time Int
instance Show (Time Second) where
show (Time t) = show t ++ "sec"
instance Show (Time Minute) where
show (Time t) = show t ++ "min"
instance Show (Time Hour) where
show (Time t) = show t ++ "hrs"
sec :: Int -> Time Second
sec t = Time t
minute :: Int -> Time Minute
minute t = Time t
hour :: Int -> Time Hour
hour t = Time t
class TimeAdder a b c | a b -> c where
add :: Time a -> Time b -> Time c
instance TimeAdder Second Second Second where
add (Time s1) (Time s2) = sec (s1 + s2)
instance TimeAdder Second Minute Second where
add (Time s) (Time m) = sec (s + 60*m)
instance TimeAdder Second Hour Second where
add (Time s) (Time h) = sec (s + 3600*h)
instance TimeAdder Minute Second Second where
add (Time m) (Time s) = sec (60*m + s)
instance TimeAdder Minute Minute Minute where
add (Time m1) (Time m2) = minute (m1 + m2)
instance TimeAdder Minute Hour Minute where
add (Time m) (Time h) = minute (m + 60*h)
instance TimeAdder Hour Second Second where
add (Time h) (Time s) = sec (3600*h + s)
instance TimeAdder Hour Minute Minute where
add (Time h) (Time m) = minute (60*h + m)
instance TimeAdder Hour Hour Hour where
add (Time h1) (Time h2) = hour (h1 + h2)
add (minute 5) (hour 2)
--125min
虽然我很高兴这种疯狂的东西能起作用,但我想知道如何避免时间加法器实例的二次爆炸。除非你有充分的理由,否则我会跳过类型类,使用一个简单的旧ADT:
data Time = Hour Int | Minute Int | Second Int
instance Show Time where
show (Hour x) = show x ++ "hrs"
show (Minute x) = show x ++ "min"
show (Second x) = show x ++ "sec"
add x y = fromSeconds (toSeconds x + toSeconds y)
toSeconds (Hour x) = 3600 * x
toSeconds (Minute x) = 60 * x
toSeconds (Second x) = x
fromSeconds x | mod x 3600 == 0 = Hour (div x 3600)
| mod x 60 == 0 = Minute (div x 60)
| otherwise = Second x
这样做的优点是能够进行类型类方法无法进行的某些简化,例如:
> add (Second 18) (Second 42)
1min
您可以这样做,但它不会给您函数依赖性
class TimeUnit a where
toSeconds :: a -> Int
fromSeconds :: Int -> a
instance TimeUnit (Time Second) where toSeconds = id; fromSeconds = id
instance TimeUnit (Time Minute) where toSeconds = (* 60); fromSeconds = (`quot` 60)
class TimeAdd a b c where
add :: a -> b -> c
instance (TimeUnit a, TimeUnit b, TimeUnit c) => TimeAdd a b c where
add a b = fromSeconds (toSeconds a + toSeconds b)
这些实例都相当简单。我会说这是模板Haskell的一个例子(尽管我会把如何做的解释留给愤怒地使用它的人)。进一步考虑hammar的建议,对于这个特定的例子,我会说,只需完全消除类型内容,改为使用智能构造函数
newtype Time = Sec Int
instance Show Time where
show (Sec n) = h ++ " hrs " ++ m ++ " min " ++ s ++ " sec"
where h = ...
m = ...
s = ...
sec :: Int -> Time
sec = Sec
min :: Int -> Time
min = sec . (*60)
hr :: Int -> Time
hr = min . (*60)
add (Sec n) (Sec m) = Sec (n+m)
当然,这不好玩,因为它没有幻影类型。有趣的练习:为
hr
,min
,sec
,制作镜头。在类型级别,我要做的是将幻影类型映射到类型级别的自然数,并使用“最小”操作找到正确的返回类型,然后让实例解析从那里开始
我将在这里使用类型族,但如果您喜欢函数依赖关系,也可以使用函数依赖关系
{-# LANGUAGE TypeFamilies, EmptyDataDecls, FlexibleInstances #-}
首先,我们需要一些类型级别的自然和最小操作
data Zero
data Succ n
type family Min a b
type instance Min Zero a = Zero
type instance Min a Zero = Zero
type instance Min (Succ a) (Succ b) = Succ (Min a b)
接下来,我们将定义幻影类型,并提供与类型级自然的映射:
data Second
data Minute
data Hour
type family ToIndex a
type instance ToIndex Hour = Succ (Succ Zero)
type instance ToIndex Minute = Succ Zero
type instance ToIndex Second = Zero
type family FromIndex a
type instance FromIndex (Succ (Succ Zero)) = Hour
type instance FromIndex (Succ Zero) = Minute
type instance FromIndex Zero = Second
接下来,输入Time
并显示实例。这些与原始代码中的相同
data Time a = Time Int
instance Show (Time Second) where
show (Time t) = show t ++ "sec"
instance Show (Time Minute) where
show (Time t) = show t ++ "min"
instance Show (Time Hour) where
show (Time t) = show t ++ "hrs"
sec :: Int -> Time Second
sec t = Time t
minute :: Int -> Time Minute
minute t = Time t
hour :: Int -> Time Hour
hour t = Time t
就像在我的ADT回答中一样,我们将使用秒作为中间单位:
class Seconds a where
toSeconds :: Time a -> Int
fromSeconds :: Int -> Time a
instance Seconds Hour where
toSeconds (Time x) = 3600 * x
fromSeconds x = Time $ x `div` 3600
instance Seconds Minute where
toSeconds (Time x) = 60 * x
fromSeconds x = Time $ x `div` 60
instance Seconds Second where
toSeconds (Time x) = x
fromSeconds x = Time x
现在剩下的就是定义add
函数
add :: (Seconds a, Seconds b, Seconds c,
c ~ FromIndex (Min (ToIndex a) (ToIndex b)))
=> Time a -> Time b -> Time c
add x y = fromSeconds (toSeconds x + toSeconds y)
神奇之处在于类型相等约束,它确保选择了正确的返回类型
此代码可以按照您的要求使用:
> add (minute 5) (hour 2)
125min
要添加另一个单位,比如说天
,您只需添加显示
、从索引
、到索引
和秒
的实例,即我们成功避免了二次爆炸。第一部分在Haskell 2010中不能这样做,因为实例化类型的限制是它们的形式
T t1 ... tn
其中t1…tn是不同的类型变量,并且最多有一个实例pro类型和类。在中,虽然对类型形式的限制有所解除,但关键的限制仍然是每个类和类型的构造函数最多只能有一个实例。
下面是一个展示部分的方法:
module Test where
data Seconds = Seconds
data Minutes = Minutes
data Hours = Hours
data Time u = Time Int
class TimeUnit u where
verbose :: u -> String
fromTime :: Time u -> u
instance TimeUnit Seconds where
verbose _ = "sec"
fromTime _ = Seconds
instance TimeUnit Minutes where
verbose _ = "min"
fromTime _ = Minutes
instance TimeUnit Hours where
verbose _ = "hrs"
fromTime _ = Hours
instance Show (TimeUnit u) => Time u where
show (o@Time t) = t.show ++ verbose (fromTime o)
main _ = do
println (Time 42 :: Time Seconds)
println (Time 42 :: Time Minutes)
println (Time 42 :: Time Hours)
fromTime
应用程序强制调用站点构造一个适当的字典,这样就可以将时间单位值设置为非即时的,或者看起来是这样
同样的技术也可以用于在不同的时间类型之间进行算术运算,通过创建一个使计算尽可能以最小单位进行的因子。好的观点!对于现实世界的应用程序,我肯定会这样做。但在我的示例中,我特别试图更好地理解幻影类型。你可以将哈马尔的代码转换回类型类,它不会有二次爆炸,因为它使用秒作为中间单位。@SjoerdVisscher你能详细说明一下吗?我不知道如何在返回类型中获得正确的幻影类型。@dave4420嗯,你是对的,返回类型将取决于整数值,这是不可能的。哇,这真是太难了。小时、分钟和秒对于这种类型安全性来说真的不是很好的候选者,为什么你会有一个函数,例如,只接受以秒为单位的时间?对于这种类型的安全性,更好的练习可能是,例如,物理单位。您可以将时间、质量、长度等作为幻象类型,并对速度、能量等进行类型安全计算。这也有助于实例数量的增加,因为并非所有类型都像您的时间示例中那样可互换。@shang:这是正确的。我应该提到,这只是一个玩具示例,可以更好地处理类型类和幻影类型。我知道,对于具有时间单位的实际应用程序,只有hammar的第一个答案更为实用。