haskell - 如何在 Haskell 中使用额外的类型来获得额外的类型安全

标签 haskell types

我是 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]
但这种方法给我带来了两个问题:
  • 标记为“should not compile”的那行实际上编译了,然后继续添加 20 分钟到 20 秒以给出 40 个东西
  • 当我为微秒添加更多类型的 MuSecX 时,我无法编译额外的 toMin 和 toSec 实例:
  •     type MuSecX = Integer  
        toSec :: MuSecX -> SecX  
        toSec m = div m 1000000  
        toMin :: MuSecX -> MinX  
        toMin m = div m 60000000  
    
    我显然在这里走错了路。我确定我不是第一个尝试做这样的事情的人,所以任何人都可以提供帮助,最好是“Canonical Haskell Way”?

    最佳答案

    类型同义词不会保护您免受混合类型的影响,这不是它们的用途。它们实际上只是相同类型的不同名称。它们用于方便和/或文档。但是 SecXInteger 仍然是完全相同的类型。
    为了创建一个全新的类型,使用 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
    
    这是怎么回事?好吧, sec1sec2 不再只是 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 ,使用 IntInteger 是任意精度,这意味着它也更慢。 Int 是 32 位或 64 位值(取决于平台),我认为这应该足以满足您的目的。
    但是对于真正的实现,我实际上首先建议使用浮点数(例如 Double )。这将使转换完全可逆和无损。用整数,toMin (SecX 20) == MinX 0 - 我们只是失去了一些信息。

    关于haskell - 如何在 Haskell 中使用额外的类型来获得额外的类型安全,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62953924/

    相关文章:

    types - 在内部使用列表时未定义相互递归类型

    javascript - Node 红色 : TypeError: Cannot read property 'nodes' of undefined

    haskell - 避免在 Hughes 的列表仿函数实例中使用 unsafeCoerce

    windows - Windows上Haskell中的Unicode控制台I/O

    haskell - 什么时候指定空导出列表会有用?

    haskell - ST monad 是如何工作的?

    Haskell 对值进行临时多态性,计算临时多态性列表的长度

    serialization - 当 TypeNameHandling 为 Auto 时,为什么 Json.NET 不包含根对象的 $type ?

    for循环中的Scala类型不匹配错误

    表上的 PHP PDO 查询错误具有 json 数据类型 (MySQL 5.7.8-rc)