Luyu Cheng

Create New Types in Haskell

haskell
syntax
grammar

在 Haskell 中,我们有几种方法定义新类型?

data 关键字

这是我学 Haskell 后第一个接触的类型定义方法,其类型声明{体}^(body)有两种写法:{类型构造器}^(type constructor)和{记录}^(record)语法。

{类型构造器}^(type constructor)由构造器名和跟在后面的类型组成,直觉上有点像一个具有名字的{元组}^(tuple)类型。在没有太多字段的时候,或者字段不需要名字的时候,这种方法很好用。在模式匹配中,我们也可以直接用构造器的语法来{解构}^(destruct)。

data Vector a = Vector a a

length :: Floating a => Vector a -> a
length (Vector x y) = sqrt (x * x + y * y)

{记录}^(record)语法比较特别,看上去像是在 C 和 C++ 的 struct 一样定义结构体。但其中的字段其实并不能用 object.property 的形式来访问,字段名实际上是函数。

data Address = Address { city :: String, street :: String }

-- Run `:t city` and you will get
-- city :: Address -> String

最后一点,data 关键字可以快速声明{联合类型}^(tagged union),或者说{和类型}^(sum type)。大家常用的 Maybe 就是这样声明出来的。

data Maybe a = Just a
             | Nothing

type 关键字

type 关键字的作用是定义类型别名。其用法就像 C 和 C++ 的 typedef 一样。但因为可以带有类型参数,在表达能力上更像 C++ 11 后的 using 关键字

type Point a = (a, a)

origin :: Point Float
origin = (0.0, 0.0)

并且新的名字的类型和旧的类型仍然兼容,其可以用在为旧类型所写的函数中,也可以用旧类型的构造器进行解构。

zero = fst origin

-- Destructing like tuples
distance :: Floating a => Point a -> Point a -> a
distance (x1, y1) (x2, y2) =
    let dx = x1 - x2
        dy = y1 - y2
    in sqrt (dx * dx + dy * dy)

newtype 关键字

除了 datatype,Haskell 还有个叫做 newtype 的关键字。其用法和 data 相同,但有两个限制:

  1. 只能有一个{构造器}^(constructor),所以不能声明{联合类型}^(tagged union);
  2. 构造器中只能有且仅有一个{字段}^(field)。

举个例子,下面两个声明,GHC 就会报错。

newtype Maybe a = Just a | Nothing
newtype Vector a = Vector a a

换言之,把你代码中的 newtype 都替换成 data 后代码也可以正常编译,但反过来,如果把 data 都替换成 newtype,就不能编译了。那么喜欢刨根问底的读者可能就会问了:为什么要设计这个 newtype 关键字呢?都用 data 关键字不就行了吗?

newtype 可以保证这个类型对类型检查器是零开销的。newtype 声明的类型构造器在编译时会直接被又划掉。我们知道 Haskell 在默认情况下是{懒惰求值}^(lazy evaluation)的,所以大量的只有一个字段的类型构造器可能会消耗运行时大量的空间,特别是当这些类型嵌套后,可能会生成很多懒惰执行的代码。所以 Haskell 的一篇 wiki 文章中也推荐人们把只有一个构造器且构造器中只有一个字段的类型用 newtype 来声明。