在boost::yap中实现Rust-like中的表达式模板



我正在尝试自学Rust,作为一个具有挑战性的学习项目,我想复制C++表达式模板库boost::yap的设计模式。我不想要一个成熟的实现,我只想要一个小的演示器来了解Rust的泛型是否足够强大,从而实现它,并在这一过程中学到一些东西。

我已经想出了一个主意,但目前被卡住了。我的问题有两个:

  • 目前是否存在一个主要障碍,使具有转换功能的表达式模板(请参阅boost::yap或我下面的代码(在Rust中不可能实现
  • 如果没有,我该如何让它工作

以下是我到目前为止的想法。

我有一个枚举E,它表示所有支持的操作。在实践中,它将采用两个通用参数来表示任何二元运算的左侧和右侧表达式,并将具有称为AddMulSub等的变体。我将实现E<U>的特征std::ops::{Add, Mul, Sub}等。

然而,出于演示目的,让我们假设我们只有两个变体,Terminal表示一个包装值的表达式,而Neg是目前唯一支持的一元运算。

use std::ops::Neg;
enum E<U> {
Terminal(U),
Neg(U)
}
impl<U> Neg for E<U> {
type Output = E<E<U>>;
fn neg(self) -> Self::Output {
E::Neg(self)
}
}

接下来,我实现了一个特性Transform,它允许我通过带有闭包的子表达式遍历表达式。一旦返回Some(_),闭包将停止递归。这就是我提出的(代码不编译(

trait Transform<Arg = Self> {
fn transform<R,F>(&self, _f: F) -> Option<R>
where F: FnMut(&Arg) -> Option<R> 
{
None
}
}
impl<U> Transform for E<U> 
where U : Transform<U> + Neg
{
fn transform<R,F>(&self, mut f: F) -> Option<R>
where F: FnMut(&Self) -> Option<R>
{
// CASE 1/3: Match! return f(self)
if let Some(v) = f(self) { return Some(v); };
match self {
E::Terminal(_) => None, // CASE 2/3: We have reached a leaf-expression, no match!
E::Neg(x) => {      // CASE 3/3: Recurse and apply operation to result
x.transform(f).map(|y| -y) // <- error[E0277]: expected a `FnMut<(&U,)>` closure, found `F`
}
}
}
}

这是编译器错误:

error[E0277]: expected a `FnMut<(&U,)>` closure, found `F`
--> src/main.rs:36:29
|
36 |                 x.transform(f).map(|y| -y) // <- error[E0277]: expected a `Fn<(&U,)>` closure, found `F`
|                             ^ expected an `FnMut<(&U,)>` closure, found `F`
|
help: consider further restricting this bound
|
28 |     where F: FnMut(&Self) -> Option<R> + for<'r> std::ops::FnMut<(&'r U,)>
|                                        +++++++++++++++++++++++++++++++++++

这是我的问题1/2:我想传递一个闭包,该闭包可以在SelfU上为E<U>工作(因此也接受E<E<U>>E<E<E<U>>>…(。Rust中的泛型类型可以这样做吗?或者,如果我的方法是错误的,那么正确的方法是什么?在C++中,我将使用SFINAE或CCD_ 16。

下面是对表达式模板库的一个小测试,看看如何使用它:

fn main() {
//This is needed, because of the trait bound `U: Transform` for `Transform`
//Seems like an unnecessary burden on the user...
impl Transform for i32{}
// An expression template 
let y = E::Neg(E::Neg(E::Neg(E::Terminal(42))));
// A transform that counts the number of nestings
let mut count = 0;
y.transform(|x| {
match x {
E::Neg(_) => {
count+=1;
None
}
_ => Some(()) // must return something. It doesn't matter what here.
}
});
assert_eq!(count, 3);

// a transform that replaces the terminal in y with E::Terminal(5)
let expr = y.transform(|x| {
match x {
E::Terminal(_) => Some(E::Terminal(5)),
_ => None
} 
}).unwrap();

// a transform that evaluates the expression
// (note: should be provided as method for E<U>)
let result = expr.transform(|x| {
match *x {
E::Terminal(v) => Some(v),
_ => None
}
}).unwrap();
assert_eq!(result, -5);
}

我的问题2/2不是一个交易破坏者,但我想知道是否有办法让代码在没有这行的情况下工作:

impl Transform for u32{}

我认为必须这样做对这样一个图书馆的用户来说是一种麻烦。问题是,我在E<U>Transform的实现上具有特性绑定的U: Transform。我觉得不稳定的专业化功能可能会对这里有所帮助,但如果能用稳定的Rust来实现这一点,那就太棒了。

这是铁锈操场链接。

编辑:

如果有其他人对此感到困惑,这里有一个生锈的操场链接,可以实现公认答案的解决方案。它还清理了上面代码中的一些小错误。

这对我来说就像一个访问者模式。

这里的解决方案看起来与您尝试做的类似。

与您的尝试不同的是,U类型在Box<U>中被放置在堆上(想想C++中的unique_ptr<U>(,这使得整个E大小是固定的(独立于具体的U(。

除了让编译器乐于统一F和Transform的类型,使其可以同时与E和U一起工作外,F的特征参数可能还需要与Box<dyn Transform>一起装箱(这大致类似于在C++中将transform标记为虚拟方法,以便能够通过"智能"指针调用它,该指针知道如何在运行时找到特定的impl(。使用match对x: U进行解包后,应该可以从U : Transform开始从中生成Box::<dyn Transform>::new(x)。有了这个F就会接受它。

如果没有Box,我认为唯一的其他解决方案是使用宏,并显式地为每种类型生成方法。

最新更新