1 个不稳定版本
0.1.0 | 2021年12月21日 |
---|
#9 在 #fold
27KB
245 行
xpr
免责声明:这是一个主要用于拖延时间的玩具项目。
xpr 是一个Rust库,允许您使用自定义类型与表达式模板。它受到 boost::yap 的启发。表达式可以通过 Fold 特性进行惰性评估和操作。
示例用法
use xpr::*;
// An expression representing an operation
let y = -Xpr::new(2)*Xpr::new(-21);
// lazy evaluation
assert_eq!(y.eval(), 42);
许可协议
双许可以与Rust项目兼容。
根据您的选择,在 Apache License, Version 2.0 或 MIT许可 下许可。此文件可能无法根据这些条款进行复制、修改或分发。
lib.rs
:
此crate允许您使用自定义类型的表达式模板。它受到C++表达式模板库 boost::yap 的启发。
要使用 xpr
,您只需将所有输入变量包装在 Xpr::new
的调用中,并对它们应用任何支持的操作。结果将是一个代表您执行的操作的对象。任何可能的操作链都将由它们自己的特定类型表示。
使用方法
通过调用 Xpr::eval
惰性执行表达式的评估
use xpr::Xpr;
// create an exression. x will be a type representing the
// addition of 7 and 5, not the result of the calculation
let x = Xpr::new(7) + Xpr::new(5);
// lazily evaluate the expression
assert_eq!(x.eval(), 12);
在上面的示例中,x
的类型是 Xpr<Add<(Xpr<Term<{integer}>>, Xpr<Term<{integer}>>)>>
[^note]。它是一个表示两个整数相加的类型。要实际评估表达式,我们调用 x.eval()
。
[^note]:为了更好的可读性,省略了所有crate和嵌套模块名称。
除了延迟求值外,您还可以使用 Fold
特性来操作表达式。实现 Fold
的结构体可以替换终端(叶表达式)并在过程中执行操作。它可以是具有状态的或者零大小的类型,具体取决于您的需求。
use xpr::{Xpr, Fold, ops::Term};
struct Fortytwoify;
impl Fold<Term<f64>> for Fortytwoify {
// We will replace these terminals by terminal expressions wrapping f64 values
type Output = Xpr<Term<f64>>;
// replaces terminals with terminals wrapping the value 42.
fn fold(&mut self, _: &Term<f64>) -> Self::Output {
Xpr::new(42.)
}
}
// create an expression
let x = -Xpr::new(2.)/Xpr::new(5.);
// fortitwoify the expression
let y = Fortytwoify.fold(&x);
// lazily evaluate the expression
assert_eq!(y.eval(), -1.);
请参阅 Fold
的文档,以获取更多有用的示例。
限制
当前库的限制如下
因此,我们无法在表达式中混合例如标量、向量和矩阵,这是一个主要的限制。
一旦 Rust 支持 特化,这些限制可以很容易地解除,但这可能不会很快发生。
性能
xpr 允许我们像预期的那样编写算术运算,并隐藏丑陋的实现细节,如优化,用户既不必担心,也不必干涉。
这种开销是多少?剧透一下:**xpr 提供了零成本抽象**!
类似于 boost::yap 文档中的分析,我们可以将 xpr 实现的汇编代码与原生实现进行比较。以下代码引入了两个函数,eval_native
和 eval_as_xpr
,前者直接执行一些算术计算,而后者创建一个 xpr 表达式并对其进行评估。
use xpr::Xpr;
#[derive(Debug, Copy, Clone)]
struct Num(f64);
impl std::ops::Add for Num {
type Output = Num;
#[inline]
fn add(self, other: Num) -> Num {
Num(self.0 + other.0)
}
}
impl std::ops::Mul for Num {
type Output = Num;
#[inline]
fn mul(self, other: Num) -> Num {
Num(self.0 * other.0)
}
}
fn eval_native(a: Num, x: Num, y: Num) -> Num {
(a * x + y) * (a * x + y) + (a * x + y) +
(a * x + y) * (a * x + y) + (a * x + y) +
(a * x + y) * (a * x + y) + (a * x + y) +
(a * x + y) * (a * x + y) + (a * x + y)
}
fn eval_as_xpr(a: Num, x: Num, y: Num) -> Num {
let a = Xpr::new(a);
let x = Xpr::new(x);
let y = Xpr::new(y);
(
(a * x + y) * (a * x + y) + (a * x + y) +
(a * x + y) * (a * x + y) + (a * x + y) +
(a * x + y) * (a * x + y) + (a * x + y) +
(a * x + y) * (a * x + y) + (a * x + y)
).eval()
}
fn main() {
let a = Num(1.);
let x = Num(2.);
let y = Num(3.);
// println!("{:?}", eval_as_xpr(a, x, y));
println!("{:?}", eval_native(a, x, y));
}
我们可以通过在发布模式下构建并使用 rustc
的 --emit=asm
选项来查看优化后的汇编代码。如果我们取消注释主函数中的最后一行并取消注释倒数第二行,我们将得到相同的汇编代码。这不会因为向表达式中添加更多操作而改变(您可能需要将 #![recursion_limit = "256"]
添加到文件顶部)。
尽管如此,这项分析仅检查了使用简单数据类型和仅评估的特殊情况。对于更复杂的数据类型、表达式和折叠操作,仍需要测试性能。