【CPN 学习笔记(三)】—— Chap3 CPN ML 编程语言 上半部分 3.1 ~ 3.3

【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 对比

python 复制代码
NOxDATA = 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 对比

python 复制代码
from 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 对比

python 复制代码
from 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 对比

python 复制代码
from 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 对比

python 复制代码
DATAPACKS = 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

python 复制代码
datapacks + [(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

python 复制代码
p, *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 语句:

python 复制代码
match 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

sml 复制代码
case 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 类型推断从子表达式开始,逐步推断完整表达式的类型:

示例 :从 ReceivePacketDataReceived 的弧表达式:if n=k then data^d else data

  • Step 1 :分析 then 分支 data^d ^ 需要两个 string,data:DATA=stringd:DATA=string → then 分支类型:DATA
  • Step 2 :分析 else 分支 data data:DATA=string → else 分支类型:DATA
  • Step 3 :分析条件 n=k n:NO=intk:NO=int= 要求两边类型相同 ✓ → 条件类型:bool
  • Step 4 :整体类型 → DATA ✓,与 DataReceived 库所颜色集匹配 ✓

五、上半部分总结

章节 核心概念 关键词
§3.1 函数式编程 求值表达式而非执行赋值;函数是一等公民;编译时类型检查 表达式、类型推断、强类型
§3.2 颜色集 product(元组)、record(结构体)、union(标记联合)、with(枚举)、list(列表) colset、构造子、^^::
§3.3 类型与表达式 弧/守卫/初始标记都有类型约束;if-then-else 和 case;自底向上推断 mscase、类型一致性

📌 截图提示:本部分对应教材 Fig 3.1(改进协议 CPN 模型,含 record 和 union 颜色集);Fig 3.2 / 3.3 为 TransmitPacket 的标记 M₁ 和 M₂(三种绑定:成功/丢包/复制)。建议在 CPN Tools 中打开改进协议模型截图插入。


下篇预告:§3.4 函数定义与多态 · §3.5 递归与列表操作 · §3.6 模式匹配 · §3.7 启用绑定元素的计算

相关推荐
Dream of maid2 小时前
Python(11) 进程与线程
开发语言·python
财经资讯数据_灵砚智能2 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年4月7日
大数据·人工智能·python·信息可视化·语言模型·自然语言处理·ai编程
航Hang*2 小时前
第3章:Linux系统安全管理——第1节:Linux 防火墙部署(firewalld)
linux·服务器·网络·学习·系统安全·vmware
宋小米的csdn2 小时前
网络知识学习路线(实用向)
网络·学习
南境十里·墨染春水2 小时前
linux学习进展 基础命令 vi基础命令
linux·运维·服务器·笔记·学习
Xudde.3 小时前
班级作业笔记报告0x08
笔记·学习·安全·web安全
Yqlqlql3 小时前
# Python : Word 文档标注工具
python
迷路爸爸1803 小时前
Docker 入门学习笔记 05:卷到底是什么,为什么容器删了数据却还能保留
笔记·学习·docker
chools3 小时前
Java后端拥抱AI开发之个人学习路线 - - Spring AI【第四期】(Tool + MCP)
java·人工智能·学习·spring