# Symbolica 2.0：适用于 Python 和 Rust 的可编程符号系统

- 来源：Hacker News 热门（buzzing.cc 中文翻译）
- 作者：mmastrac
- 发布时间：2026-06-07 20:59
- AIHOT 分数：67
- AIHOT 链接：https://aihot.virxact.com/items/cmq3thx1f01pjsla9rsrrlqom
- 原文链接：https://symbolica.io/posts/symbolica_2_0_release

## AI 摘要

Symbolica 2.0 发布，这是一款可编程符号系统，支持 Python 和 Rust 语言。该版本在 Hacker News 上获得 100 点热度。

## 正文

引言

Symbolica 是一个面向 Python 和 Rust 的高性能符号计算框架。你可以用它来操作符号表达式，并将其转化为用于计算雅可比矩阵、数值优化、积分等任务的快速数值内核。

今天迎来 Symbolica 2.0 版本发布，带来了许多令人兴奋的新功能和改进。本次发布的主题是“可编程符号”：Symbolica 的更多行为现在可以由用户自定义。这使得用户可以定义像内置对象一样能进行化简、求导、展开、打印和计算求值的数学对象。

自 1.0 版本以来，Symbolica 在多个方向上进行了改进：

更简单的 Rust API，增加了更多运算符重载和构建器风格的 API

符号注册系统，支持命名空间、别名、标签、用户数据和自定义钩子

重新设计的求值器接口，支持双精度浮点运算和 JIT 编译

更丰富的笔记本和文档输出，包括 HTML 输出、图形与多项式显示、Typst 输出、彩色打印，以及更结构化的多行格式化

新增内置数学函数，包括伽马函数、多对数函数、贝塞尔函数、黎曼 ζ 函数，以及相关的级数/求值钩子

请参阅迁移指南了解这些变化以及如何更新你的代码。

更好的输出

Symbolica 增加了自动换行输出模式，并使用彩色括号，类似于代码的样式。这使得阅读大型嵌套表达式更加容易。在 Jupyter 或 Marimo 等笔记本中，默认输出为彩色 HTML 模式，你可以轻松切换到 LaTeX 模式。现在也支持 Typst 输出。

改进的 Rust API

最明显的变化之一是普通 Rust 程序需要的导入更少，冗长的类型路径也更少。新的预导入模块汇集了大多数用户需要的常用 trait、宏、域和求值器类型。Rust 的人机工程学也得到了改进，增加了额外的重载、自动类型转换、构建器模式，以及符号上的 call 方法：

Symbolica 2.0

Symbolica 1.0

use symbolica::prelude::*;

fn main() {
let (x, y, f) = symbol!("x", "y", "f");
let e: Atom = 2 + (x + 1).pow(-2) + f.call((1 + y, y)) / 3;
let s = e.series(x, 0, 1).unwrap();

println!("{e}"); // -> 2+1/3*f(1+y,y)+1/(1+x)^2
println!("{s}"); // -> 3+1/3*f(1+y,y)-2*x+𝒪(x^2)
}

use symbolica::{
atom::{Atom, AtomCore},
function, symbol,
};

fn main() {
let (x, y, f) = symbol!("x", "y", "f");
let e = Atom::num(2) + (Atom::var(x) + 1).npow(-2) + function!(f, Atom::num(1) + y, y) / 3;
let s = e.series(x, Atom::num(0), 1.into(), true).unwrap();

println!("{e}"); // -> 2+1/3*f(1+y,y)+1/(1+x)^2
println!("{s}"); // -> 3+1/3*f(1+y,y)-2*x+𝒪(x^2)
}

用于设置和包含多个参数的函数的结构现在采用构建器模式。例如，高性能数值评估器的构造现在看起来像这样：

Symbolica 2.0

Symbolica 1.0

use symbolica::prelude::*;

fn main() -> Result<(), EvaluationError> {
let mut evaluator = parse!("x^2 + 2*x + 1 + f(x)")
.evaluator(&[parse!("x")])
.add_function(symbol!("f"), vec![symbol!("y")], parse!("cos(y + 1)"))?
.horner_iterations(2)
.build()?
.map_coeff(&|c| c.re.to_f64());

println!("{}", evaluator.evaluate_single(&[3.0]));
Ok(())
}

use symbolica::{atom::AtomCore, evaluate::{FunctionMap, OptimizationSettings}, parse, symbol};

fn main() {
let mut fn_map = FunctionMap::new();
fn_map
.add_function(
symbol!("f"),
"f".to_string(),
vec![symbol!("y")],
parse!("cos(y + 1)"),
)
.unwrap();

let params = vec![parse!("x")];
let optimization_settings = OptimizationSettings {
horner_iterations: 2,
..OptimizationSettings::default()
};
let mut evaluator = parse!("x^2 + 2*x + 1 + f(x)")
.evaluator(&fn_map, &params, optimization_settings)
.unwrap()
.map_coeff(&|c| c.re.to_f64());

println!("{}", evaluator.evaluate_single(&[3.0]));
}

借助构建器模式，原本属于不同子结构（例如 `add_function` 属于 `FunctionMap`，`horner_iterations` 属于 `OptimizationSettings`）的设置参数现在可以更流畅地一起设置。另外请注意，可能失败的操作现在会返回专门的错误类型。

可编程符号

在 Symbolica 1.0 中，符号已经可以携带重要的代数属性：

Python

Rust

from symbolica import *

x, y = S("x", "y")
dot = S("dot", is_symmetric=True, is_linear=True)

print(dot(3*x + y, x + 2*y)) # -> 3*dot(x,x)+7*dot(x,y)+2*dot(y,y)

use symbolica::prelude::*;

fn main() {
let dot = symbol!("dot"; Symmetric, Linear);
println!("{}", parse!("dot(3*x+y, x+2*y)")); // -> 3*dot(x,x)+7*dot(x,y)+2*dot(y,y)
}

在 2.0 中，符号现在可以安装更多钩子，这些钩子在代数生命周期的特定点运行：

钩子 使用场景

规范化 在符号或函数被规范化时重写它。

打印 重写 Symbolica、LaTeX 或自定义模式的输出。

导数 定义自定义求导规则。

级数 教会 Symbolica 函数在奇点附近的行为。

求值 为评估器注册数值实现。

例如，让我们定义一个 gamma 函数。由于 gamma 在 0 处有一个极点，我们无法在 0 附近进行泰勒展开。利用级数钩子，我们可以告诉 Symbolica 如何通过恒等式 \(\Gamma(a + 1) = a \Gamma(a)\) 在该点附近对该函数进行正则化：

Python

Rust

from typing import Sequence
from symbolica import *

x = S("x")

def regularize_gamma(args: Sequence[Series]) -> tuple[Expression, Expression] | None:
if args[0].get_coefficient(0) == 0:
a = args[0].to_expression()
return (1 / a, S("gamma")(a + 1))

gamma = S(
"gamma",
derivative=lambda t, _: t * E("digamma")(t[0]),
series=regularize_gamma,
)
print(gamma(x).series(x, 0, 0)) # gamma(1)*x^-1+gamma(1)*digamma(1)+𝒪(x^1)

use symbolica::prelude::*;

fn main() {
let _gamma = symbol!(
"gamma",
der = |f, index, out| {
if index == 0 {
let arg = f.as_fun_view().unwrap().get(0);
**out = symbol!("gamma").call(arg) * symbol!("digamma").call(arg);
}
},
series = |args| {
if args[0].coefficient(0.into()) == Atom::Zero {
let a = args[0].to_atom();
Some((1 / &a, symbol!("gamma").call(a + 1)))
} else {
None
}
}
);

let x = symbol!("x");
println!("{}", parse!("gamma(x)").series(x, 0, 0).unwrap());
}

在这个例子中，内置的 gamma 函数进一步化简为 1/x-γ。

评估器：从表达式树到编译内核

表达式求值是自 1.0 以来工程改动最大的部分。

高层流程仍然相同：Symbolica 将一个表达式重写为一个小指令程序，对其进行优化，然后针对不同的数值输入多次执行评估。关于其工作原理的详细信息，请参见这篇博客文章。

符号的评估器

让我们定义一个自定义符号 cosh，并教会 Symbolica 如何针对不同的数值域对其进行求值。我们可以通过注册一个求值钩子来实现：

Python

Rust

import cmath
import math
from symbolica import *

x = S("x")
cosh = S(
"cosh",
eval={
"float": lambda args: math.cosh(args[0]),
"complex": lambda args: cmath.cosh(args[0]),
"cpp": "template<typename T> T python_cosh2(T a) { return std::cosh(a); }",
},
)

expr = cosh(1/2) + x
expr.to_float() # -> 1.1276259652063807 + x

use symbolica::prelude::*;

fn main() {
let cosh = symbol!(
"cosh",
eval = EvaluationInfo::new()
.register(|args: &[f64]| args[0].cosh())
.register(|args: &[Complex<f64>]| args[0].cosh())
);

let x = symbol!("x");
let expr = cosh.call(Atom::num(1) / 2) + x;
println!("{}", expr.to_float(16)); // -> 1.127625965206381+x
}

由于已知如何求值 cosh，Symbolica 在调用 `to_float` 时会查找合适的评估器。

JIT 编译

除了生成自定义的 ASM、C++ 和 CUDA 代码外，Symbolica 现在还可以即时编译求值器。这使用了 Shahriar Iravanian 开发的 symjit crate，我们与他紧密合作，使集成过程对 Symbolica 而言显得原生自然。

JIT 路径支持自定义求值器钩子，现在已成为 Python 的默认求值后端。在实践中，它的性能与自定义 ASM 后端相当，同时将编译时间控制在合理范围内。

双精度浮点算术

Symbolica 2.0 新增了双精度浮点求值路径。双精度浮点算术将数字存储为两个 f64 的未求和结果，提供大约 106 位精度（相当于 31 位十进制数字，普通双精度为 16 位），同时速度比任意精度 Float 算术快 3 倍以上。在 Python 中，`evaluate_with_prec(..., 32)` 会自动采用此路径。

特殊函数

新的钩子系统带来的另一个重大进展是，Symbolica 拥有了更丰富的数学词汇表。Symbolica 现在支持多伽马函数、多重对数函数、贝塞尔函数、黎曼ζ函数、几何函数及其反函数，并提供了求值钩子以及围绕极点的 Laurent/Puiseux 级数行为。

某些特殊值会立即归一化：

from symbolica import *

print(E("gamma(1/2)")) # 𝜋^(1/2)
print(E("polylog(-3, z)")) # z*(1+4*z+z^2)/(1-z)^4

所有特殊函数都可进行求值：

g = E("gamma(5/6)")
print(g) # gamma(5/6)
print(g.to_float()) # 1.128787029908126

Symbolica 可以在优化的求值器内部使用特殊函数：

from symbolica import *
import numpy as np

x = S('x')
e = E('pi + zeta(3) + gamma(x)')
ev = e.evaluator([x])

print(e) # gamma(x)+zeta(3)+𝜋
print(ev.evaluate(np.array([[2.0]]))) # [[5.34364956]]

在导出代码时，诸如 \(\pi\) 和 \(\zeta(3)\) 等常量会被替换为数值，因此生成的代码无需依赖特殊函数库。

更快的操作

并非所有重要变化都体现在公共 API 中。表达式操作底层机制也获得了显著改进：

模式匹配可以证明结构不匹配并提前终止

项排序现在可以基于字节切片比较进行（归一化速度提升 30%）

Horner 方案可应用于线性表达式结构（内存减少超过 2 倍）

公共对消除使用的内存大幅减少

多项式 GCD 计算得到改进，包括一种新的模块化算法

这些合并的改进使实际用例的性能提升了 2 到 10000 倍。对于这些改进，用户反馈是无价的。如果您有计算缓慢的情况，请与我们分享，以便我们让 Symbolica 变得更快。

技术深度解析：类型擦除的回调评估

本节面向技术好奇者。它解释了 Symbolica 的评估钩子如何在不牺牲易用性或性能的情况下支持多个数值域。

评估钩子 API 有一个有趣的内部问题：单个符号可能有多个数值实现，每个实现具有不同的 Rust 类型，该类型实现了一个特定 trait，其成员函数形式为 fn(&[T]) -> T，其中 T 是某个数值域。

例如，一个函数可能针对以下情况有不同的快速实现：

|args: &[f64]| -> f64
|args: &[Complex<f64>]| -> Complex<f64>
|args: &[Float]| -> Float
|args: &[Complex<Float>]| -> Complex<Float>

这些函数类型不能全部直接存储在一个同构字段¹中。Symbolica 通过注册时对回调进行类型擦除，并在构建数值域求值器时恢复具体类型来解决此问题。

¹ 所有可能变体的枚举不可行，因为用户可以定义自己的类型 T 并为其注册回调。

核心形状是：

pub trait ExternalFunction<T>: Fn(&[T]) -> T + Send + Sync + DynClone {}
pub type EvalFn<T> = Box<dyn ExternalFunction<T>>;

pub struct EvaluationInfo {
eval_fns: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
}

当用户编写：

use symbolica::prelude::*;

let eval = EvaluationInfo::new()
.register(|args: &[f64]| 2.0 * args[0])
.register(|args: &[Complex<f64>]| args[0] + args[0]);

每个回调都存储在 TypeId::of::<T>() 下，其中 T 是由回调参数切片携带的数值域。实际回调被装箱为 Any，从而从外部容器中移除具体类型。

pub fn register<T: 'static>(mut self, f: impl ExternalFunction<T> + 'static) -> Self {
let f: EvalFn<T> = Box::new(f);
self.eval_fns.insert(
TypeId::of::<T>(),
Box::new(f) as Box<dyn Any + Send + Sync>,
);
self
}

稍后，当求值器专门用于某个域时，Symbolica 请求该确切类型的实现：

impl EvaluationInfo {
pub fn get_evaluator<T: 'static>(&self) -> Option<EvalFn<T>> {
let e = self.eval_fns.get(&TypeId::of::<T>())?;
let f = e.downcast_ref::<EvalFn<T>>()?;
Some(f.clone())
}
}

重要设计点是：类型擦除不在求值器的热循环中。它位于注册和专门化路径上。一旦为 f64、Complex<f64>、Float 或其他域构建了求值器，它就在指令流中存储具体回调，或将具体调用交给 JIT 后端。

选择评估域

求值器故事的另一个方面是 Symbolica 一开始如何选择数值类型。在内部，这通过 EvaluationDomain trait 表达。简化形式下，该 trait 如下所示：

pub trait EvaluationDomain: Sized + 'static {
fn get_evaluator(info: &EvaluationInfo) -> Option<Box<dyn ExternalFunction<Self>>> {
info.get_evaluator()
}
}

因此，对于类型 T，默认实现会在 EvaluationInfo 中查找为 T 注册的回调函数（如果存在）。然而，有时某个领域可以提供有用的回退方案。例如，SIMD 领域可以在未注册原生 SIMD 回调时对标量 f64 回调进行向量化，而 double-float 计算则可以回退到任意精度实现，然后再将结果转换回 double-float。

impl EvaluationDomain for f64 {}

impl EvaluationDomain for wide::f64x4 {
fn get_evaluator(
info: &EvaluationInfo,
) -> Option<Box<dyn ExternalFunction<Self>>> {
if let Some(f) = info.get_evaluator::<wide::f64x4>() {
return Some(f);
}

// create a vectorized version of the scalar function if it exists
if let Some(f) = f64::get_evaluator(info) {
Some(Box::new(move |args| {
let mut buffer = vec![0.; args.len()];
let mut res = [0.; 4];

for i in 0..4 {
for (b, v) in buffer.iter_mut().zip(args) {
*b = v.as_array()[i];
}

res[i] = f(&buffer);
}

res.into()
}))
} else {
None
}
}
}

AI 的使用

AI 是软件开发领域中不言自明的大问题，因此有必要说明它是如何融入 Symbolica 2.0 的开发过程的。Symbolica 在超过两年的时间里基本没有借助 AI 辅助进行开发，这一过程涉及大量规划、优化工作以及设计权衡。

最初，我对 AI 在这样一个高性能项目中的实用性持怀疑态度。然而，如果一个工具能比我更快地完成我所能做的事情，那么忽视它就显得愚蠢了。因此，我进行了一些独立的实验：将小型项目从 Mathematica 移植到 Symbolica，进行网站和文档工作，并尝试了更严肃的实现和研究任务。

其中一个实验是让 Codex 5.5 根据一篇研究论文实现 GCD 算法，并以已有的代码片段作为上下文。结果很有趣：它识别出了论文中的关键点，并正确实现了大部分算法，尽管有时会使用无意义的辅助函数。然而，当遇到 bug 时，它有时会得出错误结论，并开始修补越来越具体的情况，而不是识别根本原因——即测试输入不满足某个假设，并触发了无限循环^2。最终，编写关键代码和调试困难案例仍然是一个需要人工完成的过程。

^2 当它无法解决问题时，会添加一个回退例程到另一个 GCD 算法，而不是解决实际问题！

使用 AI 带来的最大收益体现在大规模重构和外围任务上，例如网站重新设计以及检查网站上的示例是否仍然有效。这极大地节省了时间。

即将推出的功能

一项即将推出的新功能将使 GMP 后端在任意精度算术中变为可选（改用 malachite 和 astro-float），这将以牺牲性能为代价，实现支持 WASM 的 Symbolica 构建（并简化 Windows 上的编译）。更多详情敬请期待！

TipSupport Symbolica

Symbolica 对爱好者免费，单核实例对非商业用途也免费。如果你想要新功能、更完善的文档或针对你工作流的支持，你的组织可以购买许可证并直接支持该项目。

结语

Symbolica 2.0 使符号系统更具可编程性：用户可教会符号如何进行归一化、打印、求导、在奇点处展开以及数值求值。

欢迎在评论区分享你的看法！
