假设我创建了一个Person
类型:
type Age = Int
type Height = Int
data Person = Person Age Height
这一切都很好,但我想确保无法添加Age
和Height
,因为这没有任何意义。尝试这样做时我想要编译错误。
在它的当前形式中,这不会导致编译错误:
stupid (Person age height) = age + height
所以我考虑将newtype
甚至data
用于Age
和Height
:
newtype Age = Age Int
newtype Height = Height Int
data Person = Person Age Height
但仍然:
stupid (Person (Age age) (Height height)) = age + height
有没有办法防止年龄和身高被加入?或者我要求的是不合理的?
答案 0 :(得分:4)
通过使用构造函数Person
和Height
展开newtypes,实质上指示编译器忘记类型区别。这当然使得写age + height
是合法的,但这不应该归咎于newtype
,就像你砸窗户,跳出来受伤一样,不要责怪火车司机! SUP>†
防止这种错误操作的方法是不打开新类型。如果Age
和Height
是新类型,则以下将导致错误:
stupid (Person age height) = age + height
...不仅仅因为age
和height
是不同的类型,还因为它们实际上都不支持添加操作(它们不是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"
如果您没有向模块添加显式导出列表,则会导出所有内容,包括构造函数Age
和Height
。在上面的代码中,它们不导出,因此您只能访问此模块中的Int
值。实际上必须使用包装的Int
的所有操作都必须在此模块中。
现在,在您需要的地方导入此模块:
module Person where
import Quantities
data Person = Person Age Height
test (Height h) = () -- This will not compile, Height not in scope.
除Quantities
之外的任何其他模块都无法再解构Height
或Age
。