如何在Haskell中使用附加类型以实现附加类型安全
我是哈斯凯尔的新手,非常喜欢自己 作为练习,我编写了一个修改日期和时间的程序。特别是,我计算的时间包括分、秒和微秒。现在我发现,在调试过程中,我有很多错误,例如,我在不乘以60的情况下,将分增加到秒 为了将调试从运行时转移到编译时,我想到我可以使用“类型同义词加多态函数”执行如下操作:如何在Haskell中使用附加类型以实现附加类型安全,haskell,types,Haskell,Types,我是哈斯凯尔的新手,非常喜欢自己 作为练习,我编写了一个修改日期和时间的程序。特别是,我计算的时间包括分、秒和微秒。现在我发现,在调试过程中,我有很多错误,例如,我在不乘以60的情况下,将分增加到秒 为了将调试从运行时转移到编译时,我想到我可以使用“类型同义词加多态函数”执行如下操作: module Main where type SecX = Integer toMin :: SecX -> MinX toMin m = div m 60 type MinX = Integer toSe
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
但这种方法给了我两个问题:
我显然走错了路。我肯定我不是第一个尝试这样做的人,所以任何人都可以帮助我,最好是使用“规范的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和类似的东西,当然还有大量不必要的括号。