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

Ste*_*enC 64 haskell types type-systems

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

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

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

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

Chr*_*one 58

newtypes的主要用途是:

  1. 用于定义类型的替代实例.
  2. 文档.
  3. 数据/格式正确性保证.

我正在开发一个应用程序,我现在广泛使用newtypes.newtypes在Haskell中是一个纯粹的编译时概念.例如,使用下面的unwrappers,unFilename (Filename "x")编译为与"x"相同的代码.运行时间绝对是零.有data类型.这使它成为实现上述目标的一种非常好的方式.

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

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

算法引用正确的东西非常重要,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)
Run Code Online (Sandbox Code Playgroud)

假设我有这个函数从已上传的文件中清除文件名:

-- | 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 "-_."
Run Code Online (Sandbox Code Playgroud)

从那以后我生成一个唯一的文件名:

-- | Generate a unique filename.
uniqueFilename :: SanitizedFilename -- ^ Sanitized filename.
               -> IO UniqueFilename -- ^ Unique filename.
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • 在我的情况下,我不导出构造函数,因为我不想从任何旧的整数创建任意值,它应该只来自数据库.我可以安全地打开一个并使用整数. (2认同)

Pau*_*son 19

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

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

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

newtype Query = Query String
Run Code Online (Sandbox Code Playgroud)

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

  • 我知道您正在考虑自己的设计实践:我只想提供一些实际的例子.说"newtype FilePath"的代价是程序员时间; 转换函数只是为了让类型检查器保持开心并且没有实现.重点是,如果您反复进出新类型,那么您就没有真正的额外安全性,只需要进行大量混淆函数调用.因此,在设计库时,您需要考虑应用程序员的观点. (3认同)

Cur*_*son 17

对于简单的X = Y声明,type是文档; newtype是型式检查; 这就是为什么newtype要比较的原因data.

我经常使用newtype你描述的目的:确保以与其他类型相同的方式存储(并且经常被操纵)的东西不会与其他东西混淆.通过这种方式,它只是一个稍微有效的data声明; 没有特别的理由选择一个而不是另一个.请注意,对于GHC的GeneralizedNewtypeDeriving扩展,您可以自动派生类,例如Num,允许您的温度或日元加上和减去,就像您可以使用Ints或其下面的任何内容.但是,人们希望对此有点小心; 通常一个人不会将温度乘以另一个温度!

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

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

type PlotDataGen t = PlotSeries t -> [String]
Run Code Online (Sandbox Code Playgroud)

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

newtype偶尔会在type不可能的地方使用,例如需要递归类型定义的地方,但我发现这是相当罕见的.所以看起来,至少在这个特定的项目中,大约40%的"原始"类型定义是newtypes,60%是types.newtype以前的几个定义都是类型,并且由于您提到的确切原因而被明确转换.

所以简而言之,是的,这是一个常见的习语.


GS *_*ica 10

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