基本数据类型、类型推断与类型别名

类型系统是 Haskell 的核心。Haskell 是静态强类型语言,编译器会做严格的类型检查,并基于 Hindley-Milner 体系自动推断类型。习惯用类型来思考问题,是写好 Haskell 的基础。

常见内置基本数据类型

布尔型(Bool)

布尔类型仅包含两个合法值:True 与 False。定义在标准库中:

data Bool = False | True

常用逻辑运算:与(&&)、或(||)、非(not)。Bool 不是语言内置的特殊类型,只是一个有两个构造器的普通数据类型,条件分支则由下一章的模式匹配完成。

数值类型系统

Haskell 将整数与实数细分为多种具体类型:

  • Int:固定精度的有符号整数(通常对齐机器字长,在 64 位系统上为 64 位整型,范围约为 -2⁶³ ~ 2⁶³-1)。它直接对应机器整数,运算速度最快。

  • Integer:任意精度的整数,只要内存足够就不会溢出。常用于密码学、大数精确计算等领域。

  • Float:单精度 32 位 IEEE 754 浮点数。

  • Double:双精度 64 位 IEEE 754 浮点数,是一般浮点计算的默认选择。

字面量的类型是怎么定下来的

有这么多数值类型,写下 5 的时候它到底是哪一种?答案是:先不定。整数字面量的类型是 Num a => a,带小数点的字面量是 Fractional a => a,具体是哪个类型由使用它的地方决定。和一个 Int 相加,它就是 Int;参与除法,它就只能是浮点类型:

ghci> :t 5
5 :: Num a => a
ghci> :t 5 + length [1, 2]
5 + length [1, 2] :: Int
ghci> :t 5 / 2
5 / 2 :: Fractional a => a

如果一直没有任何线索,编译器会按默认规则把 Num 约束定为 Integer,把 Fractional 约束定为 Double。这就是 GHCi 里 print 5 能直接工作的原因,也是本章后面 5 + 3.14 的例子只有在显式标注 :: Int 之后才报错的原因。

实际选型很简单:整数默认用 Integer,不用考虑溢出;做下标、计数或者性能敏感的循环时用 Int;浮点数用 Double;Float 几乎只在与外部接口(图形、二进制格式)对接时才需要。Int 的溢出是静默的,容易在大数上出错:

ghci> 2 ^ 64 :: Int
0
ghci> 2 ^ 64 :: Integer
18446744073709551616

字符与字符串

  • Char:单个 Unicode 字符,用单引号表示,如 'a'、'中'、'λ'。

  • String:双引号表示的字符串,在底层它是一个普通的字符链表 [Char]。

复合容器类型:元组与列表

  • 元组(Tuple):异构、固定长度容器。例如 ("Alice", 28, True) :: (String, Int, Bool)。 二元组是最常见的元组,标准库提供了访问函数:

    • fst :: (a, b) -> a:提取第一个元素。

    • snd :: (a, b) -> b:提取第二个元素。

  • 单元类型(Unit / ()):空元组 () 既是一个类型,也是该类型的唯一合法值。它代表“没有任何实质信息”,类似于 C/Java 中的 void。

  • 列表(List):同构、动态长度容器。例如 [1, 2, 3] :: [Int]。

类型别名(Type Synonyms)

除了使用 data 声明全新类型,Haskell 允许使用 type 关键字为现有类型声明一个更具业务语义的别名(Synonym):

-- 标准库中 String 的本质就是类型别名:
type String = [Char]

-- 在业务代码中赋予语义:
type PhoneNumber = String
type UserID      = Integer
type Coordinates = (Double, Double)

备注

类型别名的特征与限制: 类型别名在编译期完全等价于其底层类型,仅仅是为了提升代码可读性。编译器不会阻止你将一个普通的 String 传给期待 PhoneNumber 的函数。 此外,类型别名不能被部分应用(除非开启相关语言扩展)。如果需要一个与底层类型真正区分开、可以拥有独立类型类实例的新类型,应当使用 newtype。newtype 与 data 会在代数数据类型一章详细介绍。

类型阶数:Kind(类型的类型)

正如普通表达式与值拥有“类型”(Type)一样,类型自身也有自己的分类,称为阶数(Kind)。

在 GHCi 中使用 :kind 或简写 :k 可以查询任何类型的阶数:

  1. 具体类型(Kind 为 * 或 Type): 凡是能够直接承载实际运行期值的类型,其 Kind 均为 *:

    ghci> :k Int
    Int :: *
    ghci> :k Bool
    Bool :: *
    ghci> :k [Char]
    [Char] :: *
    
  2. 类型构造器(高阶 Kind): 正如函数接收一个值并产出另一个值(类型形如 a -> b),某些类型必须接收其他具体类型作为参数,才能组合成一个完整类型。这种需要参数的类型被称为类型构造器:

    ghci> :k []
    [] :: * -> *
    ghci> :k Maybe
    Maybe :: * -> *
    ghci> :k Either
    Either :: * -> * -> *
    

    Maybe 的 Kind 是 * -> *,意味着它必须接收一个具体类型(例如 Int :: *),才能变成一个可以存储具体值的完整类型(Maybe Int :: *)。

    下图展示类型构造器逐个接收参数、最终得到具体类型的过程。Either 接收一个参数后仍然是一个 * -> * 的类型构造器:

            graph LR
      M["Maybe :: * -#gt; *"] -- "应用于 Int :: *" --> MI["Maybe Int :: *"]
      E["Either :: * -#gt; * -#gt; *"] -- "应用于 String :: *" --> ES["Either String :: * -#gt; *"]
      ES -- "应用于 Int :: *" --> ESI["Either String Int :: *"]
        

    Kind 为 * 的节点才可以有运行期的值,中间的 Either String 这一步在后面讲 Functor 实例时会再遇到。

后面讲 Functor、Monad 等类型类时会用到 Kind 的概念。

函数类型、柯里化与部分应用

在类型签名中,符号 :: 读作“具有……类型”。函数类型用右结合箭头 -> 表达:

add :: Int -> Int -> Int
add x y = x + y

因为箭头在类型层面上是右结合的,所以:

Int -> Int -> Int  等价于  Int -> (Int -> Int)

这就是第一章讲的柯里化(Currying):函数本质上都只接收一个参数,多参数函数通过一层层返回新函数实现。因此可以随时进行部分应用(Partial Application):

ghci> add5 = add 5
ghci> :t add5
add5 :: Int -> Int
ghci> add5 10
15

多态性:参数多态与约束多态

参数多态(Parametric Polymorphism)

当一个函数可以适用于任何数据类型时,我们在签名中使用小写字母表示的类型变量:

-- 恒等函数:适用于任何类型 a
id :: a -> a
id x = x

-- 恒常函数:丢弃第二个参数,返回第一个参数
const :: a -> b -> a
const x _ = x

约束多态(Constrained Polymorphism)

如果类型变量需要具备某种能力(例如支持加法或比较大小),就在胖箭头 => 左侧标注类型类约束。这就是第一章说的“处理一类类型的函数”,类型类本身在下一章展开:

-- 要求类型 a 必须实现 Num 类型类
square :: Num a => a -> a
square x = x * x

数值类型转换与 fromIntegral

从命令式语言转过来的人常在这里碰壁:Haskell 没有隐式类型提升(Implicit Coercion)。

在 C 或 Python 中,5 + 3.14 会自动将整数 5 提升为浮点数完成相加。但在 Haskell 中:

ghci> x = 5 :: Int
ghci> y = 3.14 :: Double
ghci> x + y
<interactive>: error:
    • Couldn't match expected type 'Int' with actual type 'Double'

此外,普通除法运算符 (/) :: Fractional a => a -> a -> a 只能用于浮点数,不能用于 Int:

ghci> (5 :: Int) / 2
<interactive>: error:
    • No instance for (Fractional Int) arising from a use of '/'

fromIntegral

为了在整数与浮点数之间进行计算,必须显式使用 fromIntegral:

fromIntegral :: (Integral a, Num b) => a -> b

它能将任何整数类型(如 Int、Integer)转换为任何数值类型:

ghci> fromIntegral (5 :: Int) + (3.14 :: Double)
8.14
ghci> fromIntegral (5 :: Int) / 2
2.5

常见类型错误

1. 类型不匹配错误

Couldn't match expected type 'X' with actual type 'Y'
  • 原因:函数预期接受类型为 X 的实参,但调用者传递了类型为 Y 的表达式。

  • 解决思路:检查入参位置,使用 :t 分别查看目标参数与输入表达式的类型。

2. 缺失类型类实例错误

No instance for (Show (Int -> Int)) arising from a use of 'print'
  • 原因:试图打印一个函数 (Int -> Int),但函数类型没有实现 Show 类型类。

  • 解决思路:确认是否给多参数函数少传了参数,导致表达式本身还是一个未执行完毕的中间函数。

小结

  • Haskell 在编译期做类型检查,并基于 Hindley-Milner 体系自动推断类型。

  • 值的分类是 Type(Kind 为 *),类型的分类是 Kind(如 * -> *)。

  • type 声明别名,与原类型完全等价;需要独立的新类型时用 newtype 或 data。

  • 数值类型之间没有隐式转换,混合运算要用 fromIntegral 显式转换。