【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 类型推断从子表达式开始,逐步推断完整表达式的类型:

示例 :从 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 分支 data data:DATA=string → else 分支类型:DATA
  • Step 3 :分析条件 n=k n: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 启用绑定元素的计算

相关推荐
李航19833 小时前
AI定制柜建模,需要详细的建模规范和标准流程
人工智能·python·计算机视觉·ai·ai编程
沙漠之主3 小时前
Python 教学设计资料:从入门到实战的完整课程方案
爬虫·python
山岚的运维笔记4 小时前
ComfyUI NVIDIA安装教程:官方便携版下载+run_nvidia_gpu.bat启动,8G显存Windows实操
运维·服务器·windows·笔记·prompt·aigc·comfyui
知识分享小能手4 小时前
C++ 学习教程,从入门到精通,C++ 友元、异常和其他特性 — 详细知识点(15)
开发语言·c++·学习
ebiobiz4 小时前
极海 APM32 使用 Nimmake 编译指南
c++·python·单片机·嵌入式硬件·mcu
慢云智慧空间5 小时前
从智能终端到空间AI,慢云科技如何重新定义智慧建筑的核心能力?
人工智能·python·科技
fanged5 小时前
Camera学习2(V4L2和Media Controller)(TODO)
学习
2601_962885725 小时前
如何用 Python 做股价历史分位选股?(筛出处于历史低位的股票)
开发语言·python
倔强的石头1065 小时前
【Linux指南】动静态库系列(十一):PLT 与延迟绑定:第一次调用动态库函数时发生了什么
linux·人工智能·python
梦帮科技5 小时前
vLLM / TensorRT-LLM 极限推理:PagedAttention 细粒度物理页表管理与连续批处理(Continuous Batching)实战
数据结构·人工智能·分布式·python·深度学习·算法·vllm