关于类型安全的Haskell类型与newtype

时间:2009-06-13 20:34:49

标签: haskell types type-systems

我知道在Haskell中newtype经常与data进行比较,但我从更多的设计观点而不是技术问题中提出这种比较。

在不完全/ OO语言中,存在反模式“primitive obsession”,其中原始类型的大量使用降低了程序的类型安全性并且引入了相同类型值的意外互换性,否则意图用于不同的目的。例如,很多东西都可以是String,但是如果编译器可以静态地知道我们的名字是什么以及我们想要成为地址中的城市,那就太好了。

那么,多久一次,Haskell程序员使用newtype来给出其他原始值的类型区别? type的使用引入了别名,并为程序的可读性提供了更清晰的语义,但不会阻止意外地交换值。当我学习haskell时,我注意到类型系统和我遇到的任何类型系统一样强大。因此,我认为这是一种自然而常见的做法,但我没有看到很多或任何关于newtype的使用的讨论。

当然很多程序员都会采用不同的方式,但这在haskell中是否常见?

4 个答案:

答案 0 :(得分:58)

newtypes的主要用途是:

  1. 用于定义类型的替代实例。
  2. 文档。
  3. 数据/格式正确性保证。
  4. 我正在开发一个应用程序,我现在广泛使用newtypes。 Haskell中的newtypes是纯粹的编译时概念。例如。使用下面的unwrappers,unFilename (Filename "x")编译为与“x”相同的代码。运行时间绝对是零。有data种类型。这使它成为实现上述目标的一种非常好的方式。

    -- | A file name (not a file path).
    newtype Filename = Filename { unFilename :: String }
        deriving (Show,Eq)
    

    我不想意外地将其视为文件路径。它不是文件路径。它是数据库中某处的概念文件的名称。

    算法引用正确的东西非常重要,newtypes有助于此。这对于安全性也非常重要,例如,考虑将文件上传到Web应用程序。我有这些类型:

    -- | A sanitized (safe) filename.
    newtype SanitizedFilename = 
      SanitizedFilename { unSafe :: String } deriving Show
    
    -- | Unique, sanitized filename.
    newtype UniqueFilename =
      UniqueFilename { unUnique :: SanitizedFilename } deriving Show
    
    -- | An uploaded file.
    data File = File {
       file_name     :: String         -- ^ Uploaded file.
      ,file_location :: UniqueFilename -- ^ Saved location.
      ,file_type     :: String         -- ^ File type.
      } deriving (Show)
    

    假设我有这个功能可以清除已上传文件的文件名:

    -- | Sanitize a filename for saving to upload directory.
    sanitizeFilename :: String            -- ^ Arbitrary filename.
                     -> SanitizedFilename -- ^ Sanitized filename.
    sanitizeFilename = SanitizedFilename . filter ok where 
      ok c = isDigit c || isLetter c || elem c "-_."
    

    现在我从中生成一个唯一的文件名:

    -- | Generate a unique filename.
    uniqueFilename :: SanitizedFilename -- ^ Sanitized filename.
                   -> IO UniqueFilename -- ^ Unique filename.
    

    从任意文件名生成唯一文件名是危险的,应首先对其进行清理。同样,唯一的文件名因此通过扩展始终是安全的。我现在可以将文件保存到磁盘,如果我愿意,可以将该文件名放在我的数据库中。

    但是要包装/打开很多东西也很烦人。从长远来看,我认为特别值得避免价值不匹配。 ViewPatterns有所帮助:

    -- | Get the form fields for a form.
    formFields :: ConferenceId -> Controller [Field]
    formFields (unConferenceId -> cid) = getFields where
       ... code using cid ..
    

    也许你会说在一个函数中展开它是一个问题 - 如果你错误地将cid传递给函数会怎么样?不是问题,使用会议ID的所有功能都将使用ConferenceId类型。出现的是一种在编译时被强制使用的功能到功能级别的合同系统。挺棒的。所以,我尽可能经常使用它,特别是在大系统中。

答案 1 :(得分:19)

我认为这主要是情况问题。

考虑路径名。标准前奏有“type FilePath = String”,因为为方便起见,您希望能够访问所有字符串和列表操作。如果您有“newtype FilePath = FilePath String”,那么您将需要filePathLength,filePathMap等,否则您将永远使用转换函数。

另一方面,考虑SQL查询。 SQL注入是一个常见的安全漏洞,因此有类似

的内容是有意义的
newtype Query = Query String

然后添加额外的函数,通过转义引号字符将字符串转换为查询(或查询片段),或以相同的方式填充模板中的空格。这样,您就不会在不通过引用转义函数的情况下意外地将用户参数转换为查询。

答案 2 :(得分:17)

对于简单的X = Y声明,type是文档; newtype是类型检查;这就是newtypedata进行比较的原因。

我经常使用newtype来达到您描述的目的:确保以与其他类型相同的方式存储(并经常操作)的内容不会与其他内容混淆。这样,它就像一个稍微高效的data声明一样工作;没有特别的理由选择一个而不是另一个。请注意,使用GHC的GeneralizedNewtypeDeriving扩展名,您可以自动派生Num等类,允许添加和减去温度或日元,就像使用Int或其他任何内容一样。在他们下面。但是,人们希望对此有点小心;通常一个人不会将温度乘以另一个温度!

为了了解这些事情的使用频率,在我正在进行的一个相当大的项目中,我有大约122次使用data,39次使用newtype和96次使用type

但就“简单”类型而言,这个比例比这个更接近,因为type的96次使用中的32个实际上是函数类型的别名,例如

type PlotDataGen t = PlotSeries t -> [String]

你会注意到两个额外的复杂性:首先,它实际上是一个函数类型,而不仅仅是一个简单的X = Y别名,其次是它的参数化:PlotDataGen是我应用于的类型构造函数另一种创建新类型的类型,例如PlotDataGen (Int,Double)。当你开始做这种事情时,type不再只是文档,而是实际上是一个函数,虽然在类型级别而不是数据级别。

<newtype偶尔会在type不可用的情况下使用,例如需要递归类型定义的地方,但我发现这种情况相当罕见。所以看起来,至少在这个特定项目中,大约40%的“原始”类型定义是newtype s,60%是type s。有些newtype定义曾经是类型,并且由于您提到的确切原因而被明确转换。

简而言之,是的,这是一个经常成语。

答案 3 :(得分:10)

我认为将newtype用于类型区分是很常见的。在许多情况下,这是因为您想要提供不同类型的类实例,或隐藏实现,但只是想要防止意外转换也是一个明显的理由。