15

考虑:

{-# 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

虽然我很高兴像这样疯狂的东西能奏效,但我想知道如何TimeAdder避免实例的二次爆炸。

4

6 回答 6

13

除非你有充分的理由,否则我会跳过类型类并使用普通的旧 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
于 2011-12-22T14:31:06.990 回答
9

你可以做这样的事情,但它不会给你功能依赖。

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)
于 2011-12-22T16:46:03.750 回答
6

我在类型级别执行此操作的方法是将幻像类型映射到类型级别自然数并使用“最小”操作来找到正确的返回类型,然后让实例解析从那里完成这项工作。

我将在这里使用类型族,但如果您更喜欢这些,它可能可以通过函数依赖来完成。

{-# 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类型和Show实例。这些与您的原始代码相同。

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

要添加另一个单元,例如Days,您只需添加、和的实例Show,即我们已经成功避免了二次爆炸。FromIndexToIndexSeconds

于 2011-12-22T20:21:48.270 回答
2

第一部分在 Haskell 2010 中不能以这种方式完成,因为对实例化类型的限制是它们的形式为

T t1 ... tn

其中 t1...tn 是不同的类型变量,并且最多有一个实例 pro 类型和类。在Frege中,虽然对类型形式的限制有所解除,但关键限制仍然是每个类和类型构造函数最多一个实例 。不过,这是一种展示部分的方法:

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字典,以便可以从无到有地生成 TimeUnit 值,或者它看起来是这样。

相同的技术可用于在不同的时间类型之间进行算术运算,方法是创建一个使计算以最小单位成为可能的因子。

于 2011-12-23T10:56:34.347 回答
1

将 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制作镜片

于 2011-12-22T18:33:34.277 回答
0

这些实例都是相当样板的。我会说这是 Template Haskell 的一个案例(尽管我会将如何做到这一点的解释留给那些在愤怒中使用它的人)。

于 2011-12-22T18:21:46.517 回答