一、代数数据类型(ADT):Haskell 自定义类型的核心
1. 设计背景
Haskell 内置的基础类型(Int、String)和复合类型(列表、元组)仅能满足简单场景,复杂业务(如树形结构、业务实体、表达式解析)需要语义化、结构化的自定义数据结构,ADT 正是为此设计的核心语法。
2. 核心设计:代数与集合论的结合
ADT 基于"和类型(Sum Type)"与"乘积类型(Product Type)"组合设计,对应集合论的"并集"和"笛卡尔积",是 Haskell 类型系统的精髓:
- 和类型(Sum Type) :构造器互斥(二选一/多选一),对应集合的并集(
A ∪ B); - 乘积类型(Product Type) :构造器带多个参数(同时包含),对应集合的笛卡尔积(
A × B); - 核心关键字 :
data(定义 ADT 的核心关键字)。
3. 实战示例
(1)纯和类型:互斥枚举场景
ini
-- 星期:7个互斥选项(典型和类型)
data Weekday = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday
deriving (Eq, Show) -- 派生Eq/Show类型类,直接使用==/show方法
-- 使用示例
isWeekend :: Weekday -> Bool
isWeekend Saturday = True
isWeekend Sunday = True
isWeekend _ = False -- 穷尽匹配,编译器校验无遗漏
isWeekend Monday -- 输出 False
isWeekend Sunday -- 输出 True
(2)纯乘积类型:复合实体场景
sql
-- 坐标点:同时包含x和y(典型乘积类型)
data Point = Point Int Int -- 构造器Point带2个Int参数
deriving (Eq, Show)
-- 人员信息:混合基础类型和字符串
data Person = Person String Int String -- 姓名/年龄/地址
deriving (Eq, Show)
-- 使用示例
p1 = Point 10 20 -- 构造乘积类型值
p2 = Person "张三" 25 "北京"
-- 模式匹配提取参数
getAge :: Person -> Int
getAge (Person _ age _) = age
getAge p2 -- 输出 25
(3)混合类型:复杂业务场景
sql
-- 图形:圆(半径)/矩形(宽高)二选一(和类型+乘积类型混合)
data Shape = Circle Float | Rectangle Float Float
deriving (Eq, Show)
-- 计算面积:模式匹配不同构造器
area :: Shape -> Float
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h
area (Circle 5) -- 输出 78.53982
area (Rectangle 4 5) -- 输出 20.0
(4)递归 ADT:树形结构场景
sql
-- 二叉树:空树/节点(值+左子树+右子树),递归定义
data Tree a = Empty | Node a (Tree a) (Tree a)
deriving (Eq, Show)
-- 构建树并遍历
tree = Node 1 (Node 2 Empty Empty) (Node 3 Empty Empty)
-- 前序遍历:模式匹配递归ADT
preOrder :: Tree a -> [a]
preOrder Empty = []
preOrder (Node x left right) = [x] ++ preOrder left ++ preOrder right
preOrder tree -- 输出 [1,2,3]
4. ADT 核心配套:模式匹配
模式匹配是 ADT 的"专属操作",用于匹配不同构造器并提取参数,与 ADT 深度协同:
(1)核心语法
rust
-- 1. 函数参数匹配(最常用)
getName :: Person -> String
getName (Person name _ _) = name
-- 2. case表达式匹配(分支逻辑)
describeShape :: Shape -> String
describeShape s = case s of
Circle r -> "圆,半径:" ++ show r
Rectangle w h -> "矩形,宽:" ++ show w ++ " 高:" ++ show h
-- 3. do表达式匹配(Monad场景)
handleTree :: Tree Int -> IO ()
handleTree tree = do
case tree of
Empty -> putStrLn "空树"
Node x _ _ -> putStrLn $ "根节点值:" ++ show x
(2)关键注意点
-
穷尽匹配:编译器会检查是否覆盖所有构造器,遗漏会报警告(避免逻辑漏洞);
-
嵌套匹配:复杂 ADT 可嵌套匹配,简化代码:
sql-- 嵌套匹配树形结构 getRoot :: Tree Int -> Maybe Int getRoot (Node x Empty Empty) = Just x -- 匹配叶子节点 getRoot (Node x _ _) = Just x -- 匹配非空节点 getRoot Empty = Nothing -- 匹配空树
二、type 关键字:类型别名
1. 设计目标
简化复杂类型的书写,提升代码可读性,不生成新类型,仅为原类型起"别名"。
2. 核心特性
- 编译期还原为原类型,无运行时开销;
- 无类型隔离:别名和原类型可互相赋值,编译器视为同一类型。
3. 实战示例
ini
-- 1. 简化嵌套类型
type IntList = [Int] -- 列表别名
type StringMap a = [(String, a)] -- 字符串键值对别名
-- 使用示例:和原类型完全等价
f :: IntList -> Int
f = sum
f [1,2,3] -- 输出 6,与sum [1,2,3]无区别
-- 2. 业务语义命名(提升可读性)
type Age = Int
type Score = Int
-- 但无类型隔离:Age和Score可混用
age :: Age
age = 25
score :: Score
score = age -- 无编译错误,编译器视为Int
4. 适用场景 & 局限
- 适用:简化高阶函数类型、嵌套容器类型、业务语义命名;
- 局限:无法实现类型隔离,易导致逻辑错误(如 Age 和 Score 混用)。
三、newtype 关键字:零成本类型包装
1. 设计背景
解决 type 的"类型隔离"问题,同时避免 data 定义 ADT 的运行时开销。
2. 核心设计
- 仅支持单构造器 + 单字段,包装现有基础类型;
- 生成全新独立类型,编译器严格区分;
- 零成本:编译期擦除包装层,运行效率与原类型一致。
3. 实战示例
sql
-- 1. 基础类型语义隔离(核心场景)
newtype Age = Age Int -- 单构造器Age,单字段Int
newtype Score = Score Int -- 全新类型,与Age/Int严格区分
deriving (Eq, Show) -- 派生类型类
-- 使用示例:类型隔离,无法混用
age :: Age
age = Age 25
-- score = age -- 编译错误:Score与Age类型不匹配
score = Score 90 -- 正确
-- 2. 提取包装值(模式匹配)
getAge :: Age -> Int
getAge (Age a) = a
getAge age -- 输出 25
-- 3. 类型类特化包装
newtype Reverse a = Reverse a
deriving (Eq, Show)
-- 为Reverse自定义Ord实例(反转排序)
instance Ord a => Ord (Reverse a) where
compare (Reverse x) (Reverse y) = compare y x
-- 使用示例:反转排序
sort [Reverse 3, Reverse 1, Reverse 2] -- 输出 [Reverse 3,Reverse 2,Reverse 1]
4. 与 data 的核心区别
| 特性 | newtype | data(ADT) |
|---|---|---|
| 构造器限制 | 仅单构造器 + 单字段 | 无限制(多构造器/多字段) |
| 运行时成本 | 零成本(编译期擦除) | 有额外开销(存储构造器) |
| 类型类实例 | 可继承原类型实例(需扩展) | 需手动派生/定义 |
| 适用场景 | 基础类型隔离、类型类特化 | 复杂结构(和/乘积/递归) |
四、type vs newtype vs data:对比与最佳实践
1. 核心差异总结
| 维度 | type | newtype | data(ADT) |
|---|---|---|---|
| 是否生成新类型 | 否(别名) | 是(独立类型) | 是(独立类型) |
| 运行时成本 | 无 | 无(零成本) | 有 |
| 构造器限制 | 无(仅别名) | 单构造器 + 单字段 | 无限制 |
| 类型隔离 | 无 | 有 | 有 |
| 类型类支持 | 继承原类型实例 | 需手动派生/定义 | 需手动派生/定义 |
2. 选择原则(核心)
- 仅需可读性 ,无需类型隔离 →
type; - 需类型隔离 ,且包装基础类型 →
newtype; - 需复杂结构 (多构造器/递归/混合类型)→
data(ADT)。
3. 工程实践
sql
-- 组合使用示例:业务实体设计
-- 1. type:简化复杂类型
type Address = String
type Phone = String
-- 2. newtype:基础类型隔离
newtype UserId = UserId Int deriving (Eq, Show)
-- 3. data(ADT):复杂业务实体
data User = User UserId String Age [Address] -- 混合newtype/type/基础类型
deriving (Eq, Show)
-- 使用示例
user = User (UserId 1001) "张三" (Age 25) ["北京", "上海"]
五、自定义类型与类型类的协同
自定义类型需实现/派生类型类才能复用其行为,是 Haskell 类型系统的核心联动:
sql
-- 为ADT派生核心类型类
data Color = Red | Green | Blue
deriving (Eq, Ord, Show) -- 直接派生,无需手动实现
-- 为newtype手动实现类型类
newtype Temperature = Temperature Float
-- 手动实现Show,自定义输出格式
instance Show Temperature where
show (Temperature t) = show t ++ "℃"
temp = Temperature 25.5
show temp -- 输出 "25.5℃"(自定义格式)
总结
- ADT(
data)是 Haskell 自定义类型的核心,支持和/乘积/递归类型,适配复杂业务场景; type是语法糖,仅提升可读性,无类型隔离;newtype是零成本包装,解决基础类型隔离问题,运行效率无损耗;- 选择原则:可读性→
type,基础类型隔离→newtype,复杂结构→data; - 自定义类型需结合类型类(派生/手动实现),才能充分复用 Haskell 的多态能力。