【CPN 学习笔记(三)】------ Chap3 CPN ML 编程语言 上半部分 §3.1 ~ §3.3
教材:《Coloured Petri Nets》Jensen & Kristensen, Springer 2009
本篇覆盖:§3.1 函数式编程 · §3.2 颜色集构造器 · §3.3 表达式与类型推断

一、章节概述
第三章介绍 CPN ML 编程语言------在 CPN 模型中定义颜色集、声明变量、编写弧表达式和守卫的语言基础。
| 章节 | 内容 | 本篇 |
|---|---|---|
| §3.1 | 函数式编程思想 | ✅ |
| §3.2 | 颜色集构造器 | ✅ |
| §3.3 | 表达式与类型推断 | ✅ |
| §3.4 | 函数定义与多态 | 下篇 |
| §3.5 | 递归与列表操作 | 下篇 |
| §3.6 | 模式匹配 | 下篇 |
| §3.7 | 启用绑定元素的计算 | 下篇 |
二、§3.1 函数式编程
2.1 CPN ML 的来历
CPN ML 基于函数式编程语言 Standard ML(SML),在其基础上扩展了颜色集和多重集的支持。CPN Tools 使用的是 SML/NJ 实现。
选择 SML 的原因:CP-net 的数学定义中本身就用到了类型、变量、绑定、表达式求值等概念,这些与函数式语言的基础天然吻合。
ML 是什么意思?
ML 全称是 Meta Language(元语言)。
1970年代,爱丁堡大学的研究人员在开发一个定理证明系统(LCF),需要一种语言来描述证明策略------也就是"用来写操控其他语言的语言",所以叫"元语言"。
后来这个语言本身发展成了一门独立的编程语言,叫 ML。Standard ML(SML)是它的标准化版本,CPN ML 就是在 SML 基础上加了颜色集和多重集的扩展。
所以 CPN ML = Coloured Petri Net Meta Language,名字里的 ML 就是这个历史来源,跟"机器学习(Machine Learning)"没有任何关系。
2.2 函数式 vs 命令式:核心差别
| 维度 | 命令式(Python / Java) | 函数式(CPN ML / SML) |
|---|---|---|
| 计算方式 | 执行赋值语句,修改内存 | 对表达式求值,不修改状态 |
| 迭代 | for 循环 / while 循环 | 递归函数 |
| 变量 | 可以被反复赋值 | 一旦绑定值,不可改变 |
| 函数地位 | 函数是特殊构造 | 函数是一等公民,可以传递 |
| 类型检查 | 运行时(Python)或编译时(Java) | 编译时推断,强类型 |
💡 Python 类比:CPN ML 中的变量绑定更像 Python lambda 表达式的参数------绑定之后不能改变,函数根据输入计算出新值,而不是「改变」旧值。
在 CPN 模型里,变迁发生时将弧表达式中的变量绑定 到 token 的颜色值,然后对表达式求值------这正是函数式的运作方式。
2.3 强类型与类型推断
CPN ML 是强类型的:每个表达式都有类型,类型不匹配在编译时就报错。但通常不需要手动写类型注解------系统会自动推断:
sml
(* CPN ML 类型推断示例 *)
val x = 42 (* x : int,推断为整数 *)
val s = "hello" (* s : string,推断为字符串 *)
val f = fn n => n+1 (* f : int -> int,推断为函数 *)
💡 Python 对比:Python 也推断类型,但运行时才检查。CPN ML 在编译时就检查,保证仿真不会出现类型错误崩溃。
三、§3.2 颜色集(Colour Sets)
3.1 基本类型(Basic Types)
CPN ML 继承了 Standard ML 的四种基本类型,可直接用作颜色集:
| CPN ML 关键字 | 含义 | Python 类比 |
|---|---|---|
int |
所有整数 | int |
string |
所有文本字符串 | str |
bool |
true / false | bool |
unit |
只含一个值 () |
None(单例) |
sml
colset NO = int; (* 序列号颜色集 *)
colset DATA = string; (* 数据内容颜色集 *)
colset BOOL = bool; (* 布尔颜色集 *)
colset UNIT = unit; (* 单元颜色集,token 只有一种颜色 () *)
💡
unit类型只有一个值(),对应 Python 的None。当库所颜色集是UNIT时,只需关心「有没有 token」,不用关心 token 的具体值。第七章用UNIT限制网络缓冲区容量就是这个用法。
3.2 颜色集构造器一览
| 构造器 | 语法 | 作用 | Python 类比 |
|---|---|---|---|
product |
product A * B |
元组(两个或更多类型组合) | tuple |
record |
record f1:A * f2:B |
带字段名的结构体 | dict / dataclass |
union |
union C1:A + C2:B |
可区分的联合类型 | Union + 枚举标签 |
with |
`with v1 | v2 | v3` |
list |
list A |
同类型元素的有序列表 | list |
3.3 product 颜色集(元组)
定义:把两个或多个颜色集组合为一个元组。
sml
colset NO = int;
colset DATA = string;
colset NOxDATA = product NO * DATA; (* 数据包:序号 × 内容 *)
(* 合法的 NOxDATA 值 *)
(1, "COL") (* 序号1,内容"COL" *)
(3, "ED ") (* 序号3,内容"ED " *)
访问各分量用 #1、#2...... 运算符:
sml
#1 (3, "ED ") (* → 3 取第一个分量 *)
#2 (3, "ED ") (* → "ED " 取第二个分量 *)
💡 Python 对比:
pythonNOxDATA = tuple[int, str] p = (3, "ED ") p[0] # ≈ #1 p → 3 p[1] # ≈ #2 p → "ED "教材建议:product 分量不要超过 4~5 个,多了用 record 更清晰。
3.4 record 颜色集(带字段名的结构)
定义 :像 Python 的 dataclass,每个字段有名字。
sml
colset DATAPACK = record seq:NO * data:DATA;
(* 合法的 DATAPACK 值(字段顺序不重要)*)
{seq=1, data="COL"}
{data="COL", seq=1} (* 和上面等价 *)
访问字段用 #字段名 运算符:
sml
#seq {seq=1, data="COL"} (* → 1 取 seq 字段 *)
#data {seq=1, data="COL"} (* → "COL" 取 data 字段 *)
💡 Python 对比:
pythonfrom dataclasses import dataclass @dataclass class DATAPACK: seq: int data: str dp = DATAPACK(seq=1, data="COL") dp.seq # ≈ #seq dp → 1 dp.data # ≈ #data dp → "COL"product vs record:product 更简洁,record 字段有名字更易读。超过 2 个字段推荐用 record。
3.5 union 颜色集(可区分联合类型)
定义 :把两种不同颜色集合并,用构造子标签区分来自哪种颜色集。
改进协议中,网络上既有数据包(DATAPACK)也有确认包(ACKPACK),用 union 统一为一个颜色集 PACKET:
sml
colset ACKPACK = NO;
colset DATAPACK = record seq:NO * data:DATA;
colset PACKET = union Data:DATAPACK + Ack:ACKPACK;
(* 合法的 PACKET 值 *)
Data({seq=1, data="COL"}) (* 数据包,用 Data 构造子包裹 *)
Ack(2) (* 确认包,用 Ack 构造子包裹 *)
使用 case 表达式区分处理:
sml
case p of
Data({seq=n, data=d}) => (* 处理数据包 *)
| Ack(k) => (* 处理确认包 *)
💡 Python 对比:
pythonfrom typing import Union from dataclasses import dataclass @dataclass class DataPacket: seq: int data: str PACKET = Union[DataPacket, int] # int 代表 ACK 序号 # 使用时需判断类型(Python 3.10+ match) match p: case DataPacket(seq=n, data=d): ... case int(k): ...⚠️ 注意:
DATA(颜色集名)和Data(构造子名)大小写不同------CPN ML 是大小写敏感的。
3.6 enumeration 颜色集(枚举)
定义 :用 with 关键字列举所有值,值只有名字,没有附带数据。
sml
colset RESULT = with success | failure | duplicate;
(* RESULT 只有三种值:success、failure、duplicate *)
应用示例------表示数据包传输的三种结果:
sml
case res of
success => 1`pack (* 成功:传一个 token *)
| duplicate => 2`pack (* 复制:传两个 token *)
| failure => empty (* 丢包:不传 *)
💡 Python 对比:
pythonfrom enum import Enum class RESULT(Enum): success = 1 failure = 2 duplicate = 3
3.7 list 颜色集(列表)
定义 :用 list 关键字定义某个颜色集的列表类型。
sml
colset DATAPACKS = list NOxDATA; (* 数据包的列表 *)
colset ACKPACKS = list NO; (* 确认序号的列表 *)
(* 合法的 DATAPACKS 值 *)
[] (* 空列表 *)
[(1,"COL"), (1,"COL"), (2,"OUR")] (* 三元素列表 *)
两个重要的列表操作符:
sml
(* ^^ 是列表连接(append)*)
[(1,"COL")] ^^ [(2,"OUR")] (* → [(1,"COL"),(2,"OUR")] *)
(* :: 是列表构造(cons:把元素加到列表头部)*)
(1,"COL") :: [(2,"OUR")] (* → [(1,"COL"),(2,"OUR")] *)
(* 在弧表达式中用 :: 「拆解」列表:*)
(* 弧表达式 p::rest 会把列表的第一个元素绑定到 p,剩余部分绑定到 rest *)
💡 Python 对比:
pythonDATAPACKS = list[tuple[int, str]] # ^^ ≈ list1 + list2 (Python 列表拼接) # :: ≈ [head] + rest (Python 列表前插)用途 :在库所上放一个「列表 token」实现队列(FIFO)------新元素用
^^追加到末尾,旧元素用::从头部取出,保证先进先出,数据包不会「超车」。列表 token 实现队列,
^^和::怎么配合?先说清楚为什么需要这个。
第二章的协议,库所 A 上每个数据包是独立的 token:
A 上有 3 个 token: (1,"COL") (1,"COL") ← 重传的 (2,"OUR")这时候
TransmitPacket可以任意选一个 来传,于是(2,"OUR")完全可能比两个(1,"COL")先传出去------数据包发生了"超车",顺序乱了。解决方案 :把库所 A 上的多个 token 合并成一个列表 token,强制按顺序处理:
A 上只有 1 个 token: [(1,"COL"), (1,"COL"), (2,"OUR")] ↑ 队列头,先进先出现在看
^^和::分别怎么用:
^^负责"入队"(加到尾部)
SendPacket发送新数据包时,把它追加到列表末尾:sml
sml(* 弧表达式 *) datapacks ^^ [(n,d)] (* 假设 datapacks = [(1,"COL"),(1,"COL")] *) (* (n,d) = (2,"OUR") *) (* 结果 → [(1,"COL"),(1,"COL"),(2,"OUR")] *)Python 等价:
python
pythondatapacks + [(n, d)] # 列表拼接,新元素加到末尾
::负责"出队"(从头部取)
TransmitPacket传输数据包时,弧表达式写p::datapacks1,这不是在构造列表,而是在模式匹配拆解列表:sml
sml(* 弧表达式 p::datapacks1 出现在「输入弧」上 *) (* 含义:从库所 A 的列表 token 里,把第一个元素绑定到 p,剩余部分绑定到 datapacks1 *) (* 假设库所 A 上的 token 是 [(1,"COL"),(1,"COL"),(2,"OUR")] *) (* 匹配后:p = (1,"COL"),datapacks1 = [(1,"COL"),(2,"OUR")] *)Python 等价:
python
pythonp, *datapacks1 = [(1,"COL"),(1,"COL"),(2,"OUR")] # p = (1,"COL") # datapacks1 = [(1,"COL"),(2,"OUR")]完整的一次传输过程
初始状态,库所 A 上的 token: [(1,"COL"), (1,"COL"), (2,"OUR")] TransmitPacket 发生: 输入弧 p::datapacks1 拆解列表 → p = (1,"COL") ← 取出队头,准备传输 → datapacks1 = [(1,"COL"),(2,"OUR")] ← 剩余部分 传输成功(success=true): → p 被追加到库所 B 的列表末尾 → datapacks1 放回库所 A 传输后: 库所 A:[(1,"COL"), (2,"OUR")] ← 队头已被取走 库所 B:[(1,"COL")] ← 新到达的包每次只能取队头,后面的包绝对不会跑到前面------超车问题彻底解决。
还有一个细节:当库所 A 的列表是空列表
[]时,p::datapacks1无法匹配(因为空列表没有头),所以TransmitPacket自动不启用------这就是"缓冲区为空时不能传输"的自然表达,不需要额外的守卫。
3.8 颜色集综合示例:改进协议完整声明
sml
colset NO = int;
colset DATA = string;
(* product:数据包 = 序号 × 数据 *)
colset NOxDATA = product NO * DATA;
(* record:同样是数据包,但字段有名字 *)
colset DATAPACK = record seq:NO * data:DATA;
(* 确认包等价于一个序号 *)
colset ACKPACK = NO;
(* union:网络上的「统一包类型」*)
colset PACKET = union Data:DATAPACK + Ack:ACKPACK;
(* enumeration:传输结果的三种情况 *)
colset RESULT = with success | failure | duplicate;
(* list:队列式网络缓冲区 *)
colset DATAPACKS = list NOxDATA;
colset ACKPACKS = list NO;
对应的 Python 等价写法(仅帮助理解,非 CPN 语法):
python
from typing import Union
from dataclasses import dataclass
from enum import Enum
NO = int
DATA = str
NOxDATA = tuple[int, str]
@dataclass
class DATAPACK:
seq: int
data: str
ACKPACK = int
# union:用构造子区分两种类型
PACKET = Union[DATAPACK, int] # int 代表 Ack 的序号
class RESULT(Enum):
success = 1
failure = 2
duplicate = 3
DATAPACKS = list[NOxDATA]
ACKPACKS = list[int]
四、§3.3 表达式与类型推断
4.1 弧表达式的类型要求
| 位置 | 类型要求 |
|---|---|
| 弧表达式(每次传一个 token) | 等于所连接库所的颜色集类型 |
| 弧表达式(可传多个 token) | 该颜色集的多重集类型(ms) |
| 初始标记 | 颜色集类型,或该颜色集的多重集 |
| 守卫(guard) | bool 类型,或 bool 列表 |
4.2 类型推断实例
sml
(* 变量声明 *)
var n : NO;
var d, data : DATA;
var success : BOOL;
(* 弧表达式 (n,d) *)
(* 因为 n:NO,d:DATA,推断出 (n,d) 的类型是 NO*DATA = NOxDATA ✓ *)
(* 弧表达式 1`(n,d) *)
(* 1`(n,d) 是一个多重集,类型是 NOxDATA ms ✓ *)
(* 常见弧表达式 *)
if success then 1`(n,d) else empty
(* then 分支:NOxDATA ms *)
(* else 分支:any ms(空多重集属于任意多重集类型)*)
(* 整体类型:NOxDATA ms ✓ *)
⚠️ 常见错误:忘记在 then 分支写 `1``
sml(* 错误写法 *) if success then (n,d) else empty (* then 分支类型:NOxDATA(不是多重集)*) (* else 分支类型:a ms(多重集)*) (* 两者类型不同 → 编译报错!*) (* 正确写法 *) if success then 1`(n,d) else empty
4.3 if-then-else 和 case 表达式
两种条件表达式都可用于弧表达式和守卫中:
if-then-else:
sml
if n = k
then data ^ d (* ^ 是字符串拼接 *)
else data
(* 类型推断:then/else 分支都是 string = DATA ✓ *)
case 表达式(多分支时更清晰):
sml
case res of
success => 1`pack
| duplicate => 2`pack
| failure => empty
等价的嵌套 if 写法(更难读):
sml
if res = success then 1`pack
else if res = duplicate then 2`pack
else empty
case 的另一种用法------用来计算系数:
sml
(case res of
success => 1
| duplicate => 2
| failure => 0) `pack
(* 系数为 0 时等价于 empty,不产生 token *)
💡 Python 对比 :
case res of ...≈ Python 3.10+ 的match语句:
pythonmatch res: case RESULT.success: result = 1 * pack case RESULT.duplicate: result = 2 * pack case RESULT.failure: result = empty_multiset
case p of Data(...) | Ack(...)在干什么?先从问题根源说起。
PACKET = union Data:DATAPACK + Ack:ACKPACK定义了一个联合类型。这意味着一个PACKET类型的 token,可能是数据包,也可能是确认包,放在同一个库所里,你从外面看不出来它到底是哪种。构造子
Data(...)和Ack(...)就是给 token 贴的标签,区分它的身份:
Data({seq=1, data="COL"}) ← 这是一个数据包,标签是 Data Ack(2) ← 这是一个确认包,标签是 Ack
case p of ...就是剥开标签,同时把里面的内容绑定到变量:sml
smlcase p of Data({seq=n, data=d}) => (* 现在 n=1, d="COL",处理数据包 *) | Ack(k) => (* 现在 k=2,处理确认包 *)用 Python 来理解最直接:
python# CPN ML 的 union 构造子,相当于 Python 里用类来包装 class Data: def __init__(self, seq, data): self.seq = seq self.data = data class Ack: def __init__(self, k): self.k = k p = Data(seq=1, data="COL") # 创建一个数据包 token # case p of ... 相当于 Python 的 match: match p: case Data(seq=n, data=d): print(f"数据包:序号{n},内容{d}") # n=1, d="COL" case Ack(k=k): print(f"确认包:序号{k}")为什么要这样设计? 因为改进协议里网络的四个库所 A、B、C、D 现在颜色集都是
PACKET,既放数据包也放确认包。当ReceivePacket变迁从库所 B 取 token 时,它需要知道"这是数据包还是确认包",以及"序号是多少、内容是什么"。case一次搞定这两件事:判断类型 + 解包数据。
4.4 类型推断的自底向上过程
CPN ML 类型推断从子表达式开始,逐步推断完整表达式的类型:
示例 :从
ReceivePacket到DataReceived的弧表达式:if n=k then data^d else data
- Step 1 :分析
then分支data^d^需要两个 string,data:DATA=string,d:DATA=string→ then 分支类型:DATA- Step 2 :分析
else分支datadata:DATA=string→ else 分支类型:DATA- Step 3 :分析条件
n=kn:NO=int,k:NO=int,=要求两边类型相同 ✓ → 条件类型:bool- Step 4 :整体类型 → DATA ✓,与
DataReceived库所颜色集匹配 ✓
五、上半部分总结
| 章节 | 核心概念 | 关键词 |
|---|---|---|
| §3.1 函数式编程 | 求值表达式而非执行赋值;函数是一等公民;编译时类型检查 | 表达式、类型推断、强类型 |
| §3.2 颜色集 | product(元组)、record(结构体)、union(标记联合)、with(枚举)、list(列表) | colset、构造子、^^、:: |
| §3.3 类型与表达式 | 弧/守卫/初始标记都有类型约束;if-then-else 和 case;自底向上推断 | ms、case、类型一致性 |
📌 截图提示:本部分对应教材 Fig 3.1(改进协议 CPN 模型,含 record 和 union 颜色集);Fig 3.2 / 3.3 为 TransmitPacket 的标记 M₁ 和 M₂(三种绑定:成功/丢包/复制)。建议在 CPN Tools 中打开改进协议模型截图插入。
下篇预告:§3.4 函数定义与多态 · §3.5 递归与列表操作 · §3.6 模式匹配 · §3.7 启用绑定元素的计算