一看就懂的 Haskell 教程 - 自定义类型(ADT、newtype 与 type)

一、代数数据类型(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℃"(自定义格式)

总结

  1. ADT(data)是 Haskell 自定义类型的核心,支持和/乘积/递归类型,适配复杂业务场景;
  2. type 是语法糖,仅提升可读性,无类型隔离;
  3. newtype 是零成本包装,解决基础类型隔离问题,运行效率无损耗;
  4. 选择原则:可读性→type,基础类型隔离→newtype,复杂结构→data
  5. 自定义类型需结合类型类(派生/手动实现),才能充分复用 Haskell 的多态能力。
相关推荐
Anita_Sun2 天前
一看就懂的 Haskell 教程 - 类型类
haskell
Anita_Sun3 天前
一看就懂的 Haskell 教程 - 标准库类型类
haskell
Anita_Sun4 天前
一看就懂的 Haskell 教程 - 类型推断机制
后端·haskell
Anita_Sun4 天前
一看就懂的 Haskell 教程 - 类型签名
后端·haskell
Lupino1 个月前
aio_periodic 重构与优化实战:构建高性能 Python 定时任务客户端
python·haskell
Lupino1 个月前
第十章:范畴之巅与逻辑的终点
haskell
Lupino1 个月前
第八章:万流归宗与开宗立派
haskell
Lupino1 个月前
第六章:化神之境与万法同源
haskell
Lupino1 个月前
第九章:类型黑魔法与推演天机
haskell