防止对自定义数据类型的值进行操作

时间:2016-11-22 22:08:35

标签: haskell

假设我创建了一个Person类型:

type Age = Int
type Height = Int
data Person = Person Age Height

这一切都很好,但我想确保无法添加AgeHeight,因为这没有任何意义。尝试这样做时我想要编译错误。

在它的当前形式中,这不会导致编译错误:

stupid (Person age height) = age + height

所以我考虑将newtype甚至data用于AgeHeight

newtype Age = Age Int
newtype Height = Height Int
data Person = Person Age Height

但仍然:

stupid (Person (Age age) (Height height)) = age + height

有没有办法防止年龄和身高被加入?或者我要求的是不合理的?

2 个答案:

答案 0 :(得分:4)

通过使用构造函数PersonHeight展开newtypes,实质上指示编译器忘记类型区别。这当然使得写age + height是合法的,但这不应该归咎于newtype,就像你砸窗户,跳出来受伤一样,不要责怪火车司机! SUP>†

防止这种错误操作的方法是不打开新类型。如果AgeHeight是新类型,则以下导致错误:

stupid (Person age height) = age + height

...不仅仅因为ageheight是不同的类型,还因为它们实际上都不支持添加操作(它们不是Num个实例)。现在没问题,只要您不需要对这些值执行任何操作。

然而,在某些时候你需要对这些值执行某些操作。这应该要求用户解包实际的newtype构造函数。

对于像年龄和身高这样的物理量,有几种方法可以安全地输入:

  • 添加VectorSpace个实例。这将允许您将一个高度添加到另一个高度,并按分数缩放高度,但不会将高度添加到完全不同的高度。

    import Data.AdditiveGroup
    import Data.VectorSpace
    
    instance Additive Height where
      Height h₀ ^+^ Height h₁ = Height $ h₀+h₁
      zeroV = Height 0
    instance VectorSpace Height where
      type Scalar Height = Double
      μ *^ Height h = Height . round $ μ * fromIntegral h
    

    对于某些科学用例,这个简单的实例非常方便,但可能对您的应用程序没有用。

  • 添加有意义的特定于单位的访问者。在现代的Haskell中,那些将是lenses

    import Control.Lens
    
    heightInCentimetres :: Lens' Height Int
    heightInCentimetres = lens (\(Height h)->h) (\(Height _) h->Height h)
    ageInYears :: Lens' Age Int
    ageInYears = lens (\(Age a)->a) (\(Age _) a->Age a)
    

    通过这种方式,如果某人需要与某人年龄的实际年数一起工作或修改它,他们可以这样做,但他们需要明确指出这些是他们所谈论的岁月。当然,再次将年份和高度作为整数进行访问可以避免类型区分并将年龄与高度混合,但这很难发生。

  • 一直到library for physical quantities

    import Data.Metrology.SI
    
    type Height = Length
    type Age = Time
    

    这允许您执行all kinds of physically legit operations同时防止荒谬的。

跳出火车实际上更危险。更像是unsafeCoerce,它真正指示编译器将所有类型安全性抛出窗口并且很容易导致崩溃或demons flying out of your nose

答案 1 :(得分:0)

这实际上是一种常见的模式。将要保护的数据类型放入自己的模块中:

module Quantities (
    Age, Height,   -- NOTE: constructors not exported
    prettyHeight   -- ...
) where

newtype Age = Age Int
newtype Height = Height Int

-- any operations that need to see the int, e.g.:
prettyHeight :: Height -> String
prettyHeight (Height h) = show h ++ "cm"

如果您没有向模块添加显式导出列表,则会导出所有内容,包括构造函数AgeHeight。在上面的代码中,它们导出,因此您只能访问此模块中的Int值。实际上必须使用包装的Int的所有操作都必须在此模块中。

现在,在您需要的地方导入此模块:

module Person where

import Quantities

data Person = Person Age Height

test (Height h) = ()  -- This will not compile, Height not in scope.

Quantities之外的任何其他模块都无法再解构HeightAge