我是 Haskell 的新手,并且非常享受。
作为练习,我编写了一个修改日期和时间的程序。特别是,我正在做涉及分钟、秒和微秒的计算。现在我发现,在调试时,我有很多错误,例如,我将分钟添加到秒而不乘以 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]
但这种方法给我带来了两个问题: type MuSecX = Integer
toSec :: MuSecX -> SecX
toSec m = div m 1000000
toMin :: MuSecX -> MinX
toMin m = div m 60000000
我显然在这里走错了路。我确定我不是第一个尝试做这样的事情的人,所以任何人都可以提供帮助,最好是“Canonical Haskell Way”?
最佳答案
类型同义词不会保护您免受混合类型的影响,这不是它们的用途。它们实际上只是相同类型的不同名称。它们用于方便和/或文档。但是 SecX
和 Integer
仍然是完全相同的类型。
为了创建一个全新的类型,使用 newtype
:
newtype SecX = SecX Integer
如您所见,该类型现在有一个构造函数,可用于构造该类型的新值,以及通过模式匹配从中获取 Integer
:let x = SecX 20
let (SecX a) = x -- here, a == 20
与 MinX
类似:newtype MinX = MinX Integer
转换函数如下所示: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
不再只是 Integer
s(这是练习的重点),因此没有为它们定义函数 (+)
。但是你可以定义它:函数
(+)
来自 type class Num
,所以为了让 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
(或者甚至 Monoid
)。 Semigroup 有一个操作 <>
,其语义是“将这些东西中的两个粘合在一起,并得到另一个相同类型的东西作为返回”,它在几秒钟内运行良好:instance Semigroup SecX where
(SecX a) <> (SecX b) = SecX $ a + b
现在这将编译:let sec1 = SecX 20
let sec2 = SecX 20
let sec3 = sec1 <> sec2 -- compiles now, and sec3 == SecX 40
同样对于分钟:instance Semigroup MinX where
(MinX a) <> (MinX b) = MinX $ a + b
可是等等!我们还是有麻烦!现在
print [x, y, z]
不再编译。好吧,它无法编译的第一个原因是列表
[x, y, z]
现在包含不同类型的元素,这是不可能发生的。但是好吧,因为它只是用于测试,所以我们可以先执行 print x
,然后再执行 print y
,没关系。但这仍然无法编译,因为 function
print
要求它的参数有一个 class Show
实例——这是函数 show
所在的地方,它用于将值转换为字符串以进行打印。当然,我们可以为我们的类型实现它:
class Show SecX where
show (SecX a) = show a <> " seconds"
class Show MinX where
show (MinX a) = show a <> " minutes"
或者,我们可以让编译器自动为我们派生实例:newtype SecX = SecX Integer deriving Show
newtype MinX = MinX Integer deriving Show
但在这种情况下 show (SecX 42) == "SecX 42"
(或者可能只是 "42"
取决于启用的扩展),而我在 show (SecX 42) == "42 seconds"
上面手动实现。你的来电。呼!现在我们终于可以继续讨论第二个问题:转换函数。
通常的“基本”方法是为不同的函数使用不同的名称:
minToSec :: MinX -> SecX
secToMin :: SecX -> MinX
minToMusec :: MinX -> MuSecX
secToMusec :: SecX -> MuSecX
... and so on
但是如果你真的坚持为函数保留相同的名称,同时让它们使用不同的参数类型,那也是可能的。更一般地说,这称为“重载”,在 Haskell 中,创建重载函数的机制是我们的老 friend 类型类。看上面:我们已经为不同类型定义了函数 (<>)
。我们可以为此创建自己的类型类: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"
最后:不要使用
Integer
,使用 Int
。 Integer
是任意精度,这意味着它也更慢。 Int
是 32 位或 64 位值(取决于平台),我认为这应该足以满足您的目的。但是对于真正的实现,我实际上首先建议使用浮点数(例如
Double
)。这将使转换完全可逆和无损。用整数,toMin (SecX 20) == MinX 0
- 我们只是失去了一些信息。
关于haskell - 如何在 Haskell 中使用额外的类型来获得额外的类型安全,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62953924/