如何在Haskell中使用附加类型以实现附加类型安全

如何在Haskell中使用附加类型以实现附加类型安全,haskell,types,Haskell,Types,我是哈斯凯尔的新手,非常喜欢自己 作为练习,我编写了一个修改日期和时间的程序。特别是,我计算的时间包括分、秒和微秒。现在我发现,在调试过程中,我有很多错误,例如,我在不乘以60的情况下,将分增加到秒 为了将调试从运行时转移到编译时,我想到我可以使用“类型同义词加多态函数”执行如下操作: module Main where type SecX = Integer toMin :: SecX -> MinX toMin m = div m 60 type MinX = Integer toSe

我是哈斯凯尔的新手,非常喜欢自己

作为练习,我编写了一个修改日期和时间的程序。特别是,我计算的时间包括分、秒和微秒。现在我发现,在调试过程中,我有很多错误,例如,我在不乘以60的情况下,将分增加到秒

为了将调试从运行时转移到编译时,我想到我可以使用“类型同义词加多态函数”执行如下操作:

module Main where
type SecX = Integer
toMin :: SecX -> MinX
toMin m = div m 60
type MinX = Integer
toSec :: MinX -> SecX
toSec = (60 *)
main :: IO ()
main = do
  let x = 20 :: MinX
  let y = 20 :: SecX
  let z = x + y       -- should not compile
  print [x,y,z]
toMin :: SecX -> MinX
toMin (SecX m) = MinX $ div m 60

toSec :: MinX -> SecX
toSec (MinX m) = SecX $ 60 * m
但这种方法给了我两个问题:

  • 标记为“不应该编译”的行实际上是编译的,然后继续添加20分钟到20秒,得到40多岁的东西
  • 当我添加另一种类型的MuSecX达微秒时,我无法编译toMin和toSec的其他实例:

  • 我显然走错了路。我肯定我不是第一个尝试这样做的人,所以任何人都可以帮助我,最好是使用“规范的Haskell方式”

    类型
    只是一个弱名称别名。您需要的是
    newtype

    module Main where
    
    newtype SecX = SecX Integer deriving (Show)
    newtype MinX = MinX Integer deriving (Show)
    
    toMin :: SecX -> MinX
    toMin (SecX s) = MinX $ s * 60
    
    toSec :: MinX -> SecX
    toSec (MinX m) = SecX $ m `div` 60
    
    main :: IO ()
    main = do
      let x = MinX 20
      let y = SecX 20
      print x
      print y
      --let z = x + y       -- will not compile
      --print [x,y,z]
    
    newtype SecX = SecX Integer
    
    现在,值实际上被包装在类型中,
    整数
    部分(因此,例如,
    (+)
    )无法直接访问。现在,您可以自由地提供您可能希望专门处理包装器的所有操作——事实上,它是一种新类型


    newtype
    的工作原理与
    data
    类似,但不能有多个成员(只能“包装”一个类型的值),并且在优化阶段可以有效地进行删除。

    类型同义词不会保护您避免混合类型,这不是它们的用途。它们只是同一类型的不同名称。它们用于方便和/或用于记录。但是
    SecX
    Integer
    仍然是非常相同的类型

    要创建全新类型,请使用
    newtype

    module Main where
    
    newtype SecX = SecX Integer deriving (Show)
    newtype MinX = MinX Integer deriving (Show)
    
    toMin :: SecX -> MinX
    toMin (SecX s) = MinX $ s * 60
    
    toSec :: MinX -> SecX
    toSec (MinX m) = SecX $ m `div` 60
    
    main :: IO ()
    main = do
      let x = MinX 20
      let y = SecX 20
      print x
      print y
      --let z = x + y       -- will not compile
      --print [x,y,z]
    
    newtype SecX = SecX Integer
    
    如您所见,该类型现在有一个构造函数,可用于构造该类型的新值,以及通过模式匹配从中获取
    整数

    let x = SecX 20 
    let (SecX a) = x  -- here, a == 20
    
    MinX
    类似:

    newtype MinX = MinX Integer
    
    转换函数如下所示:

    module Main where
    type SecX = Integer
    toMin :: SecX -> MinX
    toMin m = div m 60
    type MinX = Integer
    toSec :: MinX -> SecX
    toSec = (60 *)
    main :: IO ()
    main = do
      let x = 20 :: MinX
      let y = 20 :: SecX
      let z = x + y       -- should not compile
      print [x,y,z]
    
    toMin :: SecX -> MinX
    toMin (SecX m) = MinX $ div m 60
    
    toSec :: MinX -> SecX
    toSec (MinX m) = SecX $ 60 * m
    
    现在这行代码确实无法编译

    let x = MinX 20
    let y = SecX 20 
    let z = x + y       -- does not compile
    
    但是等等!这也不再编译:

    let sec1 = SecX 20
    let sec2 = SecX 20 
    let sec3 = sec1 + sec2       -- does not compile either
    
    发生什么事了?嗯,
    sec1
    sec2
    不再只是
    整数(这是练习的重点),因此没有为它们定义函数
    (+)

    但是您可以定义它:函数
    (+)
    来自,因此为了使
    SecX
    支持此函数,
    SecX
    还需要一个
    Num
    的实例:

    instance Num SecX where
        (SecX a) + (SecX b) = SecX $ a + b
        (SecX a) * (SecX b) = SecX $ a * b
        abs (SecX a) = ...
        signum (SecX a) = ...
        fromInteger i = ...
        negate (SecX a) = ...
    
    哇,要实现的东西太多了!另外,乘以秒意味着什么?这有点尴尬,不是吗?这是因为类
    Num
    实际上是数字。人们期望它的实例真的像数字一样。这在几秒钟内是没有意义的,因为尽管您可以添加它们,但其他操作实际上没有多大意义

    在几秒钟内实现一个更好的方法是(甚至可能是)。Semigroup有一个操作
    ,其语义是“将这两个东西粘在一起,并得到另一个相同类型的东西作为回报”,这在几秒钟内效果非常好:

    instance Semigroup SecX where
        (SecX a) <> (SecX b) = SecX $ a + b
    
    或者,我们也可以让编译器自动为我们派生实例:

    newtype SecX = SecX Integer deriving Show
    newtype MinX = MinX Integer deriving Show
    
    但在这种情况下,show(SecX 42)=“SecX 42”
    (或者可能只是
    “42”
    ,具体取决于启用的扩展),而在我上面的手动实现中,show(SecX 42)=“42秒”
    。你的电话


    呸!现在我们终于可以进入第二个问题:转换函数

    通常的“基本”方法是对不同的函数使用不同的名称:

    minToSec :: MinX -> SecX
    secToMin :: SecX -> MinX
    minToMusec :: MinX -> MuSecX
    secToMusec :: SecX -> MuSecX
    ... and so on
    
    但是,如果您真的坚持让函数使用相同的名称,同时让它们使用不同的参数类型,这也是可能的。更一般地说,这称为“重载”,在Haskell中,创建重载函数的机制是我们的老朋友类型类。如上所述:我们已经为不同类型定义了函数
    ()
    。我们可以为此创建自己的类型类:

    class TimeConversions a where
        toSec :: a -> SecX
        toMin :: a -> MinX
        toMuSec :: a -> MuSecX
    
    然后添加其实现:

    instance TimeConversions SecX where
        toSec = id
        toMin (SecX a) = MinX $ a `div` 60
        toMuSec (SecX a) = MuSecX $ a * 1000000
    
    同样的,几分钟和几微秒

    用法:

    main = do
        let x = SecX 20
        let y = SecX 30
        let a = MinX 5
        let z = x <> y
        -- let u = x <> a  -- doesn't compile
        let v = x <> toSec a
    
        print [x, y, v]   -- ["20 seconds", "30 seconds", "320 seconds"]
        print a           -- "5 minutes"
        print (toMin x)   -- "0 minutes"
        print (toSec a)   -- "300 seconds"
    
    main=do
    设x=secx20
    设y=secx30
    设a=minx5
    设z=xy
    --设u=xa——不编译
    设v=x toSec a
    打印[x、y、v]-[“20秒”、“30秒”、“320秒”]
    打印--“5分钟”
    打印(toMin x)--“0分钟”
    打印(toSec a)-“300秒”
    

    最后:不要使用
    Integer
    ,使用
    Int
    Integer
    是任意精度,这意味着它的速度也较慢
    Int
    是32位或64位的值(取决于平台),我认为这对于您的目的应该足够了

    但对于真正的实现,我实际上首先建议使用浮点数(例如,
    Double
    )。这将使转换完全可逆和无损。对于整数,
    toMin(secx20)=minx0
    ——我们刚刚丢失了一些信息



    非常感谢。很好的回答,而且有帮助,但是绿色的蜱虫必须转到Fyodor。我看到你在那里做了什么:-“不要使用<代码>整数< /代码>,使用<代码> int >代码>“过早优化。一些黑暗的副作用编程是许多能力的路径,有些人认为是不自然的:-”LuQui有效。然而,在我的特殊情况下,我有比我的问题所暗示的更大的问题。我对Int v也有些犹豫。整数,因此,在我清理之前,程序中加入了很多toInteger和类似的东西,当然还有大量不必要的括号。