考虑以下非类型化lambda演算的实现:
pub enum Term {
Var(usize), // a variable with a De Bruijn index
Abs(Box<Term>), // an abstraction
App(Box<Term>, Box<Term>) // an application
}
impl Term {
...
}
我觉得这个简单而简洁的设计可以从转化为特质中受益。不同的术语应该具有不同的方法集,例如,只有抽象应该是“不可抽象的”,只有应用程序才应该是可评估的。
我知道枚举与特质的通常论点;即使enums是更好的选择,我仍然想知道它是否可能。
到目前为止,我的基本构建块或多或少如下:
#[derive(Clone, PartialEq, Eq)]
pub struct Var(usize);
#[derive(Clone, PartialEq, Eq)]
pub struct Abs<T: Term>(T);
#[derive(Clone, PartialEq, Eq)]
pub struct App<T: Term, U: Term>(T, U);
pub trait Term: Clone {
fn abs(self) -> Abs<Self> { Abs(self) }
fn app<T: Term>(self, other: T) -> App<Self, T> { App(self, other) }
fn update_free_variables(&mut self, added_depth: usize, own_depth: usize);
fn _apply<T: Term>(&mut self, other: &mut T, depth: usize); // this is a helper function that enables term tree traversal for Abs<T>::apply
fn is_reducible(&self, limit: usize, count: &usize) -> bool;
fn beta_reduce(&mut self, order: Order, limit: usize, verbose: bool) -> usize;
}
impl Var {
pub fn new(index: usize) -> Self {
assert!(index > 0);
Var(index)
}
}
impl<T: Term> Abs<T> {
fn unabs(self) -> T {
self.0
}
fn apply<U: Term>(mut self, other: &mut U) -> T {
self._apply(other, 0);
self.unabs()
}
}
impl<T: Term, U: Term> App<T, U> {
fn unapp(self) -> (T, U) {
(self.0, self.1)
}
}
// and some impl Term for X
虽然实现基本功能非常简单,但有一些地方我很难找到合适的解决方案。我需要能够做到以下几点:
我宁愿尝试自己实施,我只需要一些关于方向的建议。没有枚举包装它甚至可能吗?如果是这样,我应采取什么方法(在对象安全方面,unsafe
欺骗等)?
答案 0 :(得分:2)
不同的术语应该有不同的方法集,例如:只有抽象应该是“不可抽象的”,只有应用程序才应该是可评估的。
我不认为这是基于特质的设计的一个很好的论据。通过模式匹配,枚举在运行时公开了术语类型之间的差异,但是特征隐藏了这些差异,迫使您以相同的方式处理所有术语。您可能在编译时不知道术语的类型,因此给不同类型的术语提供不同的方法没有多大意义。如果您想使用特定于每种术语类型的功能而不是仅通过统一接口以多态方式与术语交互,那么您应该使用基于枚举的设计。
如果您决定坚持使用基于特征的实现,则需要删除所有通用方法并改为使用特征对象。如果它具有泛型方法,您将无法使用Term
动态分派,因为它不是对象安全的。
基于特质的设计的一个潜在优势是可扩展性,但在这种情况下,它不是一个问题,因为无类型lambda演算的定义是固定的。
使用单个函数
创建一个能够解释任何术语的解析器,从简单变量到复杂术语
如果要求所有应用程序都被括号括起来,解析表达式应该相当简单。我没有太多解析经验,但我可能会尝试这样的递归方法:
To read one term, given a mutable reference to a variable stack (which begins empty):
If the next character is an opening parenthesis:
Consume it.
Read a term.
Read a term.
Make sure the next character is a closing parenthesis, and consume it.
Return an application of the two terms.
If the next character is a lambda:
Consume it.
Make sure the next character is a variable, then consume it.
Make sure the next character is a dot, and consume it.
Push the variable to the variable stack.
Read a term.
Pop the variable off of the stack.
Return an abstraction of the term.
If the next character is a variable:
Consume it.
Search the variable stack find the first index of the variable from the top.
Return a variable term with this index.
您可以修改此选项以接受lambda演算表示法中的常用快捷方式,例如(a b c)
((a b) c)
。目前,它会接受λx.λy.λz.((x z) (y z))
但不接受λx.λy.λz.x z (y z)
。
用另一个变量或不同的术语替换任何变量(无论嵌套多深)
我假设变量项存储的数字是引入变量的点与使用它的点之间的抽象层数。如果是这种情况,那么您可以递归地遍历结构,同时记住当前的抽象深度。当遇到与数字匹配的变量时,它将替换为给定的术语,除了术语中的所有自由变量,可以通过在给定的术语中查找数字大于其抽象深度的变量来找到,应该将当前的抽象深度添加到其编号中以解释级别的差异。遇到应用程序时,将在其两个子代中递归执行替换过程。当遇到新的抽象时,替换在其体内递归执行,抽象深度增加1以占新层。
如果你真的想使用特征,那么Term
会有这样的方法:
fn substitute(&mut self, variable_number: usize, other: &Term);
只是为了澄清编号系统,以下是正确的吗?
λn.λf.λx.(f ((n f) x))
→Abs(Abs(Abs(App(Var(1),App(App(Var(2),Var(1)),Var(0))))))
递归地减少术语(我不确定是否可以使用可能必须是特征对象的解析术语)
虽然很麻烦,但对于特征对象来说这是可能的。你可以在Term
中定义两个方法,第一个方法除了Abs
实现之外什么也不做,它需要一个术语并返回抽象主体,用变量索引0代替给定的术语第二种方法除了App
实现之外什么都不做,它会在应用程序的左边项上调用第一个方法,传递正确的术语。使用类似的策略,您可以定义查找beta可简化术语的方法,通过组合这些方法,您可以对枚举模式匹配进行混乱仿真,这对于此任务来说是一个更好的工具。
在实施beta-reduction时,您可能会发现this paper很有帮助。我没有多读它,但它似乎提供了有效的β减少算法的策略。
虽然使用枚举和特征可以实现相同的行为,但是当不需要可扩展性时,枚举是更好的选择,并且有必要以复杂的方式分析和重新排列数据的结构。对于这个问题,基于枚举的解决方案可能更好。