polars-arrow 设计原则:从源码读懂数组相等性、错误处理与逻辑/物理类型分层
Polars 的数据内核建立在 polars-arrow 这个 crate 之上,而 crates/polars-arrow/src/README.md 是该 crate 的顶层设计文档,它定义了贯穿整个 crate 的四条设计铁律:数组相等性的唯一定义来源、错误分类封装约定、逻辑类型与物理类型的严格分层,以及未定义行为(UB)的唯一合法入口。阅读本文后,你将理解 Polars 底层列式数据格式的设计约束如何在 crates/polars-arrow/src/array/equal/mod.rs、crates/polars-arrow/src/datatypes/mod.rs 等源码中落地,以及这些约束为什么是跨语言 FFI 互操作安全的基础。
文档定位:crate 级设计文档体系
README 开头声明了自身的文档治理规则:本文档描述该模块(即整个 crate)的设计,而每个子模块 MAY(可以)拥有自己的设计文档,描述该模块的具体细节,如果存在,则 MUST(必须)放在该模块自己的 README.md 中。
这一约定在当前仓库中确有体现,例如:
这种"crate 一份总纲 + 模块各自一份细则"的分层文档结构,使得设计约束可以就近维护、随模块演进。下文逐条拆解这四条总纲,并给出源码级证据。
一、Equality:数组相等性只有一个"事实来源"
README 第一条设计原则指出:Arrow 规范并未定义数组相等性。本 crate 遵循规范的意图,但不保证与例如 C++ 实现中的定义逐位一致。因此 crate 内部必须自建一个权威定义:
关于两个数组是否相等,只存在唯一的事实来源(single source of truth),即
array/equal模块中定义的相等性算子。实现代码 MUST 使用这些算子来断言相等,以保证所有测试遵循同一套数组相等性定义。
源码实现:统一的 equal 入口
在 crates/polars-arrow/src/array/equal/mod.rs 中,核心入口是一个公开函数 equal(约 L199-L297):
/// Logically compares two [`Array`]s.
/// Two arrays are logically equal if and only if:
/// * their data types are equal
/// * each of their items are equal
pub fn equal(lhs: &dyn Array, rhs: &dyn Array) -> bool {
if lhs.dtype() != rhs.dtype() {
return false;
}
// 按 lhs.dtype().to_physical_type() 匹配 16 种物理类型,
// 分别 downcast 到具体数组类型后调用各子模块的 equal
}
其判定逻辑严格遵循文档给出的"逻辑相等"定义,两个条件缺一不可:
- 数据类型必须相等:首行直接比较
lhs.dtype() != rhs.dtype(),不同类型(例如Utf8与LargeUtf8)直接判不等; - 每个元素逐一相等:按物理类型分派到 16 个类型专属实现模块。
该目录下的文件一一对应各物理类型的实现:primitive.rs、boolean.rs、utf8.rs、binary.rs、list.rs、struct_.rs、dictionary.rs、union.rs、map.rs、null.rs、fixed_size_list.rs、fixed_size_binary.rs、binary_view.rs 等。
面向所有持有方式的 PartialEq 覆盖
mod.rs 前半部分(L19-L197)为各种数组持有方式成对实现了 PartialEq,例如:
impl PartialEq for dyn Array + '_ {
fn eq(&self, that: &dyn Array) -> bool { equal(self, that) }
}
impl PartialEq<dyn Array> for std::sync::Arc<dyn Array + '_> { /* ... */ }
impl PartialEq<dyn Array> for Box<dyn Array + '_> { /* ... */ }
impl<T: NativeType> PartialEq<PrimitiveArray<T>> for PrimitiveArray<T> {
fn eq(&self, other: &Self) -> bool { primitive::equal::<T>(self, other) }
}
注意所有 PartialEq 实现的最终落地都是同一个自由函数 equal——这正是"单一事实来源"的工程落实:无论是 &dyn Array、Box<dyn Array>、Arc<dyn Array> 还是具体的 PrimitiveArray<T>、Utf8Array<O>、DictionaryArray<K>,断言相等时都会汇入同一套判定路径,测试与生产代码不可能出现两套语义。对需要自定义相等语义的类型,该文件还通过 #[cfg(feature = "proptest")] 提供了 impl_array_eq 等辅助宏,统一 proptest 策略中的相等性断言。
二、Error handling:错误的三类封装约定
README 对错误处理给出了三条 MUST/MAY 约定:
| 约定 | 含义 |
|---|---|
外部依赖产生的错误 MUST 封装在 External 变体中 |
第三方库报错不直接穿透 |
IO 产生的错误 MUST 封装在 Io 变体中 |
IO 失败单独归类 |
功能未实现时 MAY 返回 NotYetImplemented,MAY 以 unimplemented! panic |
两种"未完成"姿态都合法 |
这套约定把错误按来源而非内容分类:来自外部 crate 的错误归 External,来自文件/流读写的错误归 Io,两者在调用链上被本 crate 自身的错误类型隔离,避免外部依赖的错误类型侵入公共 API。对"尚未实现"的功能,文档允许两种风格——优雅地返回 NotYetImplemented 变体,或者直接 unimplemented!() panic。
从源码结构看,polars-arrow 作为底层 crate,自身定义了一套精简的 ArrowError(其变体按 External / Io / Memory 等来源划分),而上层 crates/polars-error 中的统一 PolarsError 会将其映射到 Compute 等分类,形成"底层按来源分类、顶层按领域分类"的两级错误体系。这一分层使得上层 DataFrame 引擎在处理底层计算错误时,无需感知 Arrow 层的错误细节。
三、Logical 与 Physical 类型的严格分离
README 的第三条原则是整个 crate 类型系统的基石:
- 物理类型 MUST 通过泛型实现(physical types MUST be implemented via generics);
- 逻辑类型 MUST 通过变量实现,其取值例如
enum(logical types MUST be implemented via variables, whose value is e.g. anenum); - 逻辑类型 MUST 声明并实现在
datatypes模块中。
逻辑类型:datatypes 模块中的 ArrowDataType
逻辑类型即"语义类型",它规定数据应如何被表示。crates/polars-arrow/src/datatypes/mod.rs 中声明了 ArrowDataType 枚举(L40 起),注释说明"每个变体唯一标识一个逻辑类型,并定义数据的特定语义",每个变体都可通过 ArrowDataType::to_physical_type 映射到对应的物理类型。例如:
pub enum ArrowDataType {
Null,
Boolean,
Int8, Int16, Int32, Int64, Int128,
UInt8, UInt16, UInt32, UInt64, UInt128,
Float16, Float32, Float64,
Timestamp(TimeUnit, Option<PlSmallStr>),
Date32, Date64,
Time32(TimeUnit),
Time64(TimeUnit),
// ... Duration、Interval、List、Struct、BinaryView 等
}
Timestamp(TimeUnit, Option<PlSmallStr>) 是逻辑/物理分离的典型:它的逻辑语义是"带时区信息的时间戳",但内存中的物理表示只是一个 i64 原语序列加一个布尔位图——同一份物理内存可以承载 Date32、Timestamp、Duration 等多种逻辑类型,仅靠 ArrowDataType 这个"变量"加以区分。这正是文档要求"逻辑类型用变量(enum)而非泛型"的原因:逻辑语义是运行时数据(随 Schema 走),而物理布局是编译期结构(随泛型单态化)。此外 ArrowDataType::Extension 变体还允许用元数据扩展自定义类型,提供 to_storage / to_storage_recursive 将其还原为底层逻辑类型。
物理类型:泛型参数承载内存布局
物理类型定义数据在内存中的表示,在 crates/polars-arrow/src/datatypes/physical_type.rs 中声明为 PhysicalType 枚举(Null、Boolean、Primitive(PrimitiveType)、Utf8、Binary、List、Struct、Dictionary 等),供运行时分派;而真正的布局则固化在泛型参数里:
PrimitiveArray<T: NativeType>:数值原语,T决定位宽与对齐;Utf8Array<O: Offset>/BinaryArray<O: Offset>:O取i32或i64,即 Arrow 的"小/大"变体(Utf8与LargeUtf8);ListArray<O: Offset>、DictionaryArray<K: DictionaryKey>同理,偏移量与字典键宽均为编译期常量。
equal 函数中的分派宏 with_match_primitive_type_full!(primitive, |$T| ...)(见 crates/polars-arrow/src/array/equal/mod.rs)就是这种模式的典型用法:运行时拿到逻辑类型,经 to_physical_type 收窄到具体物理类型,再在编译期展开成 primitive::equal::<$T> 的泛型实例。
四、唯一合法的未定义行为来源:FFI
README 最后一条原则最为强硬:
只存在一个且仅一个可接受的未定义行为来源:FFI。通过指针传递的数据不可能被证明是安全的(只有规范给出的承诺)。
Arrow 的 C Data Interface 允许 Rust 与 C++/Python 等其他实现零拷贝交换数组,而指针内容是否合规(偏移越界、位图对齐、null 计数正确)无法在 Rust 类型系统中证明——跨边界交换只能依赖规范约定的"契约式承诺"。这正是 README 将 FFI 定为唯一 UB 入口的含义:crate 内部其余所有代码都必须保持 Safe 语义,不安全的口子只在 crates/polars-arrow/src/ffi 中开口。
crates/polars-arrow/src/ffi/bridge.rs 中的 align_to_c_data_interface 展示了出口侧的防御:交换前将 Box<dyn Array> 按 16 种物理类型逐一 downcast,对存在 offset 的切片先物化为无偏移的 to_ffi_aligned(),保证交给 C ABI 的结构满足规范对指针/长度/偏移的全部要求。入口侧则由 crates/polars-arrow/src/ffi/array.rs 完成 FFI_ArrowArray 的解析。这条"出口对齐、入口信任规范"的通道,是 polars 的 Rust 引擎与 Python 端 py-polars 共享列数据而不拷贝的前提。
五条原则在模块结构中的落点
结合 crates/polars-arrow/src/lib.rs 的模块清单,可以看到设计原则与目录结构的对应关系:
datatypes——逻辑类型(ArrowDataType)与物理类型(PhysicalType)的声明地,落实第三条原则;array(含array/equal子模块)——全部数组类型及其唯一相等性定义,落实第一条原则;ffi/legacy/io——FFI 与 IO 边界,是错误分类(Io)与 UB 承诺的集中区;compute、bitmap、scalar、types、offset等——建立在前述约束之上的算子与辅助设施。
小结
crates/polars-arrow/src/README.md 虽然篇幅不长,但四条 MUST/MAY 条款构成了 polars-arrow 的完整设计契约:相等性收敛到 array/equal 的单一 equal 函数;错误按来源封装为 External / Io,未完成功能可用 NotYetImplemented 或 unimplemented! 表达;逻辑类型用 datatypes 模块中的 enum 变量表达、物理类型用泛型表达;FFI 被明确为全 crate 唯一被允许的 UB 来源。理解这四条约束,是进一步阅读 Polars 列式内核、IPC/FFI 互操作与上层 polars-core / polars-ops 实现的前提。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00