规范化与差异比较
逐字节确定的序列化(编解码器与确定性)意味着_同一个数据集_总是输出相同的字节。规范化是更强的性质:两个_不同的、彼此同构的内存数据集_——即在空节点重新标记意义下相同的图——会规范化为相同的字节。
RDFC-1.0
PurRDF 在内核中原生实现了 W3C RDF 数据集规范化(RDFC-1.0),并依据 W3C rdf-canon 测试夹具套件进行测试(65 个向量——64 个求值用例加 1 个负例——全部通过;参见 docs/CONFORMANCE.md)。
入口点是 canonicalize(另有 canonicalize_with 变体用于选择哈希),它产出规范的空节点标签,并在上一层的 purrdf-rdf 中,基于冻结的 IR 产出规范的扁平 N-Quads:
#![allow(unused)]
fn main() {
use purrdf::{canonicalize, parse_dataset};
let ds = parse_dataset(b"_:source <https://example.org/name> \"Alice\" .", "application/n-triples", None)?;
let renamed = parse_dataset(b"_:renamed <https://example.org/name> \"Alice\" .", "application/n-triples", None)?;
let canon = canonicalize(&ds);
// 规范标签在不同运行、不同宿主与不同语言绑定之间保持稳定。
assert_eq!(canon.nquads, canonicalize(&renamed).nquads);
Ok::<(), Box<dyn std::error::Error>>(())
}
当需要为图确定内容身份时,使用规范化:哈希、签名、去重,或比较由不同写出器产生的数据集。
带源归属的原生重新标记
当下一阶段消费的是原生数据集时,使用 canonical_relabel。它保留 RDF 1.2 项、具体化节点、注解、图声明与源位置,而无须呈现并重新解析一份规范文档。
当编译器或分析器必须把源节点与该结果关联起来时,使用 canonical_relabel_with_mapping。它在同一次重建中、经过一次规范标签搜索之后,记录实际的项映射:
#![allow(unused)]
fn main() {
use purrdf::{canonical_relabel_with_mapping, datasets_isomorphic, parse_dataset, TermRef};
let source = parse_dataset(b"<https://example.org/alice> <https://example.org/knows> _:friend .", "application/n-triples", None)?;
let source_node = source.term_id_by_iri("https://example.org/alice").ok_or("source IRI not interned")?;
let result = canonical_relabel_with_mapping(&source)?;
let output_node = result.map_term(source_node).ok_or("source node not retained")?;
let canonical_value = result.dataset().resolve(output_node);
assert!(matches!(canonical_value, TermRef::Iri("https://example.org/alice")));
// 在此把源回执与所有者元数据关联到 canonical_value。
let canonical_dataset = result.into_dataset();
assert!(datasets_isomorphic(source.as_ref(), &canonical_dataset));
Ok::<(), Box<dyn std::error::Error>>(())
}
source_node 必须属于确切的那个输入数据集;output_node 属于本结果。来自另一数据集、索引相同的 ID 无法被检测出来。该映射包括嵌套的三元组组成部分、字面量数据类型、复合嵌入的空节点与已驻留的 IRI、具体化节点与注解项,以及空的命名图。被重写省略的未使用字典条目返回 None。
该映射为每个源项使用四个字节,查找为常数时间。现有的 canonical_relabel API 不会分配它。该映射不是持久的身份或证明凭证:应把已解析的 RDF 身份与经过认证的输入和输出回执一起持久化,绝不要持久化原始的 TermId 值。源角色与立场(standpoint)归属仍然是调用方的元数据,而不是规范标签的推论。
在 RDF 1.2 构造之上:两种形式,以不同的名字区分
RDFC-1.0 没有具体化节点、注解或三元组项的概念。纯 RDF 1.1 的数据集在 PurRDF 中处处以相同方式规范化;携带这些构造的数据集则以两种形式之一规范化,而所持有的是哪一种,本身就是身份的一部分:
- 扁平形式——
purrdf-rdf中的canonical_flat_nquads先把陈述层重写为普通的rdf:reifies/ 注解三元组,再在符合规范的 RDFC-1.0 下对_该_三元组集合进行规范化。CLI 的convert --canonical、wasm 的Dataset.canonicalize()以及 W3C 一致性关卡运行的都是这一形式。 purrdf-rdfc12v2 profile——purrdf-core中的canonicalize保留陈述层,并改为将其降级到保留的urn:purrdf:rdfc:命名空间中(任何已经携带该命名空间的输入都会被拒绝,除非它以该 profile 自身所输出的两种形态之一写成——这些形态会折叠回陈述层,这正是规范化在其自身输出上具有幂等性的原因)。它仅在 RDF 1.1 子集上与 RDFC-1.0 逐字节一致,对其输出所取的摘要不得标记为 RDFC-1.0;CANON_PROFILE_ID/CANON_PROFILE_VERSION在运行时指明该 profile。规范性文本是docs/RDF12-CANON-PROFILE.md。
同构
datasets_isomorphic(a, b) 判定两个冻结的数据集是否在 RDF 结构上同构:即在同一个空节点双射下具有相同的四元组、具体化节点绑定与注解。规范化给出等价的裁决——两个数据集同构,当且仅当它们的规范化结果相等——但直接检查是测试与测试框架中更方便的形式。例如,PurRDF 自己的一致性测试框架使用 RDFC-1.0 同构来比较 SHACL Rules 的输出图与预期的推理图。
Diff
dataset_diff(a, b) 在两个冻结的数据集之间产生结构化 diff,其中包括一个 isomorphic 裁决。对于面向人工审阅的流程,purrdf-rdf 还提供按主语的对称 CBD(Symmetric-CBD)提取(“describe”)以及一个便于审阅的 Turtle 归一化器,从而使图的变更读起来就像代码变更。
选择合适的工具
| 需求 | 使用 |
|---|---|
| 同一数据集 → 相同字节 | 任何原生序列化器(始终成立) |
| 同一_图_(在空节点意义下)→ 相同字节,RDF 1.1 子集 | RDFC-1.0(canonicalize 或 canonical_flat_nquads,此处二者相同) |
| 带具体化节点/注解的同一_图_ → 相同字节 | canonical_flat_nquads(在扁平化层上运行 RDFC-1.0)或 canonicalize(purrdf-rdfc12 profile)——二者不可互换 |
| 「这两个数据集是同一张图吗?」 | datasets_isomorphic |
| 「这两个数据集之间有什么变化?」 | dataset_diff + describe/normalize |
| 图的内容寻址传输 | GTS(BLAKE3 内容 id) |
API 细节见 docs.rs/purrdf-core(canonicalize、datasets_isomorphic 与 dataset_diff 条目)与 docs.rs/purrdf-rdf(describe 与归一化)。