1 个不稳定版本

0.1.0 2021年12月21日

#9#fold

MIT/Apache

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.0MIT许可 下许可。此文件可能无法根据这些条款进行复制、修改或分发。


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 的文档,以获取更多有用的示例。

限制

当前库的限制如下

  • 只有持有相同类型终端的表达式才能使用 Fold 进行转换,
  • 终端是唯一可以转换成 Fold 的表达式。

因此,我们无法在表达式中混合例如标量、向量和矩阵,这是一个主要的限制。

一旦 Rust 支持 特化,这些限制可以很容易地解除,但这可能不会很快发生。

性能

xpr 允许我们像预期的那样编写算术运算,并隐藏丑陋的实现细节,如优化,用户既不必担心,也不必干涉。

这种开销是多少?剧透一下:**xpr 提供了零成本抽象**!

类似于 boost::yap 文档中的分析,我们可以将 xpr 实现的汇编代码与原生实现进行比较。以下代码引入了两个函数,eval_nativeeval_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"] 添加到文件顶部)。

尽管如此,这项分析仅检查了使用简单数据类型和仅评估的特殊情况。对于更复杂的数据类型、表达式和折叠操作,仍需要测试性能。

无运行时依赖