首页
/ polars-arrow 设计原则:从源码读懂数组相等性、错误处理与逻辑/物理类型分层

polars-arrow 设计原则:从源码读懂数组相等性、错误处理与逻辑/物理类型分层

2026-09-05 17:49:44作者:史锋燃Gardner

Polars 的数据内核建立在 polars-arrow 这个 crate 之上,而 crates/polars-arrow/src/README.md 是该 crate 的顶层设计文档,它定义了贯穿整个 crate 的四条设计铁律:数组相等性的唯一定义来源、错误分类封装约定、逻辑类型与物理类型的严格分层,以及未定义行为(UB)的唯一合法入口。阅读本文后,你将理解 Polars 底层列式数据格式的设计约束如何在 crates/polars-arrow/src/array/equal/mod.rscrates/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
}

其判定逻辑严格遵循文档给出的"逻辑相等"定义,两个条件缺一不可:

  1. 数据类型必须相等:首行直接比较 lhs.dtype() != rhs.dtype(),不同类型(例如 Utf8LargeUtf8)直接判不等;
  2. 每个元素逐一相等:按物理类型分派到 16 个类型专属实现模块。

该目录下的文件一一对应各物理类型的实现:primitive.rsboolean.rsutf8.rsbinary.rslist.rsstruct_.rsdictionary.rsunion.rsmap.rsnull.rsfixed_size_list.rsfixed_size_binary.rsbinary_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 ArrayBox<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. an enum);
  • 逻辑类型 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 原语序列加一个布尔位图——同一份物理内存可以承载 Date32TimestampDuration 等多种逻辑类型,仅靠 ArrowDataType 这个"变量"加以区分。这正是文档要求"逻辑类型用变量(enum)而非泛型"的原因:逻辑语义是运行时数据(随 Schema 走),而物理布局是编译期结构(随泛型单态化)。此外 ArrowDataType::Extension 变体还允许用元数据扩展自定义类型,提供 to_storage / to_storage_recursive 将其还原为底层逻辑类型。

物理类型:泛型参数承载内存布局

物理类型定义数据在内存中的表示,在 crates/polars-arrow/src/datatypes/physical_type.rs 中声明为 PhysicalType 枚举(NullBooleanPrimitive(PrimitiveType)Utf8BinaryListStructDictionary 等),供运行时分派;而真正的布局则固化在泛型参数里:

  • PrimitiveArray<T: NativeType>:数值原语,T 决定位宽与对齐;
  • Utf8Array<O: Offset> / BinaryArray<O: Offset>:Oi32i64,即 Arrow 的"小/大"变体(Utf8LargeUtf8);
  • 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 承诺的集中区;
  • computebitmapscalartypesoffset 等——建立在前述约束之上的算子与辅助设施。

小结

crates/polars-arrow/src/README.md 虽然篇幅不长,但四条 MUST/MAY 条款构成了 polars-arrow 的完整设计契约:相等性收敛到 array/equal 的单一 equal 函数;错误按来源封装为 External / Io,未完成功能可用 NotYetImplementedunimplemented! 表达;逻辑类型用 datatypes 模块中的 enum 变量表达、物理类型用泛型表达;FFI 被明确为全 crate 唯一被允许的 UB 来源。理解这四条约束,是进一步阅读 Polars 列式内核、IPC/FFI 互操作与上层 polars-core / polars-ops 实现的前提。

登录后查看全文
热门项目推荐
相关项目推荐