【问题标题】:What's the difference between the "data" and "type" keywords?“数据”和“类型”关键字有什么区别?
【发布时间】:2026-01-30 22:25:01
【问题描述】:

datatype 关键字总是让我感到困惑。

我想知道datatype有什么区别以及如何使用它们。

【问题讨论】:

    标签: haskell keyword


    【解决方案1】:

    type 声明了一个类型同义词。类型同义词是现有类型的新名称。例如,String 是这样定义的 in the standard library

    type String = [Char]
    

    StringChars 列表的另一个名称。 GHC 将在编译时用[Char] 替换程序中所有String 的用法。

    明确地说,String 字面意思是Chars 的列表。这只是一个别名。您可以在String 值上使用所有标准列表函数:

    -- length :: [a] -> Int
    ghci> length "haskell"
    7
    -- reverse :: [a] -> [a]
    ghci> reverse "functional"
    "lanoitcnuf"
    

    data 声明了一个新数据类型,与类型同义词不同,它不同于任何其他类型。数据类型有许多构造函数来定义你的类型的可能情况。例如,Bool 是这样定义的 in the standard library

    data Bool = False | True
    

    Bool 值可以是 TrueFalse。数据类型支持模式匹配,允许您对数据类型的值执行运行时案例分析。

    yesno :: Bool -> String
    yesno True = "yes"
    yesno False = "no"
    

    data 类型可以有多个构造函数(与Bool 一样),可以由其他类型参数化,可以在其中包含其他类型,并且可以递归引用自己。这是一个证明这一点的异常模型; Error a 包含 a 类型的错误消息,并且可能是导致它的错误。

    data Error a = Error { value :: a, cause :: Maybe (Error a) }
    type ErrorWithMessage = Error String
    
    myError1, myError2 :: ErrorWithMessage
    myError1 = Error "woops" Nothing
    myError2 = Error "myError1 was thrown" (Just myError1)
    

    重要的是要意识到data 声明了一个不同于系统中任何其他类型的新类型。如果String 被声明为data 类型包含Chars 列表(而不是类型同义词),您将无法在其上使用任何列表函数。

    data String = MkString [Char]
    myString = MkString ['h', 'e', 'l', 'l', 'o']
    myReversedString = reverse myString  -- type error
    

    还有另外一种类型声明:newtype。这很像data 声明——它引入了一种与任何其他类型分开的新数据类型,并且可以进行模式匹配——除非你被限制为具有单个字段的单个构造函数。换句话说,newtype 是封装了现有类型的 data 类型。

    重要的区别是newtype成本:编译器承诺newtype 的表示方式与其包装的类型相同。打包或解包newtype 没有运行时成本。这使得newtypes 有助于在值之间进行管理(而不是结构)区分。

    newtypes 可以很好地与类型类交互。例如,考虑Monoid,该类型具有组合元素(mappend)和特殊“空”元素(mempty)的方式。 Int 可以通过多种方式转换为Monoid,包括与 0 的加法和与 1 的乘法。我们如何选择将哪一种用于Monoid 的可能Int 实例?最好不要表达偏好,并使用newtypes 来启用任一使用,而无需运行时成本。转述the standard library

    -- introduce a type Sum with a constructor Sum which wraps an Int, and an extractor getSum which gives you back the Int
    newtype Sum = Sum { getSum :: Int }
    instance Monoid Sum where
        (Sum x) `mappend` (Sum y) = Sum (x + y)
        mempty = Sum 0
    
    newtype Product = Product { getProduct :: Int }
    instance Monoid Product where
        (Product x) `mappend` (Product y) = Product (x * y)
        mempty = Product 1
    

    【讨论】:

    • 一个可能的编辑:“最好不要冻​​结一个隐含的偏好并使用newtypes 表达每个可能的选择明确,没有运行时成本”。
    【解决方案2】:

    使用data,您可以创建 new 数据类型并为其声明构造函数:

    data NewData = NewDataConstructor
    

    使用type,您只需定义一个别名:

    type MyChar = Char
    

    type 的情况下,您可以将MyChar 类型的值传递给期望Char 的函数,反之亦然,但对于data MyChar = MyChar Char,您不能这样做。

    【讨论】:

      【解决方案3】:

      type 的工作方式与let 类似:它允许您为某事物指定一个可重用的名称,但该名称将始终有效,就像您已内联定义一样。所以

      type ℝ = Double
      
      f :: ℝ -> ℝ -> ℝ
      f x y = let x2 = x^2
              in x2 + y
      

      行为方式与

      完全相同
      f' :: Double -> Double -> Double
      f' x y = x^2 + y
      

      如:您可以在代码中的任何位置将f 替换为f',反之亦然;什么都不会改变。

      OTOH,datanewtype 创建了一个不透明的抽象。它们更像是 OO 中的类构造函数:即使某些值实现 只是根据单个数字,它不一定表现 像这样的数字。例如,

      newtype Logscaledℝ = LogScaledℝ { getLogscaled :: Double }
      
      instance Num LogScaledℝ where
        LogScaledℝ a + LogScaledℝ b = LogScaledℝ $ a*b
        LogScaledℝ a - LogScaledℝ b = LogScaledℝ $ a/b
        LogScaledℝ a * LogScaledℝ b = LogScaledℝ $ a**b
      

      在这里,虽然Logscaledℝ 在数据方面仍然只是一个Double 数字,但它的行为显然与Double 不同。

      【讨论】: