Parsec 的迁移(三)—— Ruskell 的改进

638 阅读4分钟
原文链接: zhuanlan.zhihu.com
Rust 是 Mozilla 基于其未来的开发需求设计的编程语言。这是一门非常有趣的编程语言,某种意义上说,它是一个重新设计的 C++ ——当然,业界不止一次试图用这种方式构造一个更好的 C++ ,关于它是否真的能成为 Next C++ 我们不多讨论了。

Ruskell ( Dwarfartisan/ruskell · GitHub ) 是我在学习 rust 的过程中做的一些积累。首先是 Parsec 的移植,其次是一些函数式编程概念的实现尝试。其详细的文档在 《 Ruskell——Haskell Parsec 的 Rust 移植 - 前路迢迢 - 知乎专栏 》。近期我又对这个库做了一次升级,简化了算子的结构和构造方式。

在过去的设计中,Ruskell Parsec 的结构总是一个实现了Parsec(Parsec 实现了 Monad)的 struct,为了使用简单,内置算子我都实现了 Fn/FnMut/FnOnce 。这样的代价是每个算子的实现代价非常高。尽管一部分算子我基于一个通用的 Parser struct 实现,但是仍然是一个比较重的代价。每一个算子对象,都是一个携带着 abc 形式的 struct ,而 abc 宏是 Arc 包装中的一个闭包的 Box。这样至少需要三层封装。

在重构 goparsec2 的时候(Parsec 的迁移(一) - 前路迢迢 - 知乎专栏) ,我意识到其实 rust 版本也可以处理为一个类型别名和一个有默认实现的 traits 的组合。于是我做了一些尝试。

因为 rust 的语法比 go 苛刻的多,这个过程并非一帆风顺。开始我试图直接将在闭包Fn(State)->Status的类型别名上绑定已实现的 Parsec 。但是同样遇到 size 问题。最终我还是将类型别名定义到了 abc (Arc Box Closure)结构上。


pub type Parser = Arc)->Status>>;

impl Parsec for Parser where T:Clone, R:Clone {
    fn parse(&self, state: &mut State) -> Status {
        self(state)
    }
}

impl Monad for Parser where T:Clone, R:Clone {}

是的,新的解析子类型只需要这么几行,因为不用再写 new 和 Fn/FnMut/FnOnce 实现,比原有的代码量小了太多。而且再定义新的算子的时候也简单的多,只要定义一个fn,返回 Parser 类型就可以了,例如:


pub fn try(p:X)->Parser
where T:Clone, R:Clone, X:Parsec+Clone {
    abc!(move |state: &mut State|->Status{
        let pos = state.pos();
        let res = p.parse(state);
        if res.is_err() {
            state.seek_to(pos);
        }
        res
    })
}

目前很多算子还没有仔细的审读其代码是否可以进一步优化,但是基本上都简化为一个parser的构造逻辑了。

而其它逻辑,例如 Monad、Parsec、State的定义,几乎都没有受到影响,调用方式也几乎不需要改变,这说明 rust 的语言抽象能力还是非常好的。下例是 ruskell 代码库中的一段测试函数,在新的设计下也是有效的:


#[test]
fn many1_test_1() {
    let mut state = VecState::from_iter("abc".chars().into_iter());
    let a = eq('b');
    let b = eq('b');
    let c = eq('c');

    let re = many1(either(try(a), try(b)).or(try(c)))(&mut state);
    assert!(re.is_err());
}

唯一的问题出在上例中这个“either”算子的定义,它是choice的一个简化版本,只支持两路分支,如果需要多路选择,就要支持链式的 or 操作。但是在 type aliase 上直接定义方法会报错,于是我又定义了一个 Or trait :


pub trait Or {
    fn or(&self, Parser)->Parser;
}

pub type Either = Arc)->Status>>;
pub fn either(x:X, y:Y)->Either
where T:Clone, R:Clone, X:Parsec+Clone, Y:Parsec+Clone{
    let x = x.clone();
    let y = y.clone();
    abc!(move |state:&mut State|->Status{
        let pos = state.pos();
        let val = x.parse(state);
        if val.is_ok() {
            val
        } else {
            if pos == state.pos() {
                y.parse(state)
            } else {
                val
            }
        }
    })
}
impl Or for Either {
    fn or(&self, p:Parser)->Parser{
        let s:Parser = self.clone();
        either(s, p)
    }
}

另外就是发现 rust 有 try! 宏以后,可以愉悦的模仿 do 环境了,比如现在 between 可以这样写:


pub fn between
        (open:Open, close:Close, parsec:X)
        ->Parser
where T:Clone, P:Clone, B:Clone, E:Clone, Open:Monad+Clone, X:Parsec+Clone,
        Close:Parsec+Clone {
    let open = open.clone();
    let parsec = parsec.clone();
    let close = close.clone();
    abc!(move |state: &mut State|->Status

{ try!(open.parse(state)); let re = parsec.parse(state); try!(close.parse(state)); re }) }

忽视那几个clone以后,其实关键代码就是:


    abc!(move |state: &mut State|->Status

{ try!(open.parse(state)); let re = parsec.parse(state); try!(close.parse(state)); re })

这么几行,而且清晰的给出了between的逻辑,非常舒服。

还有这段测试代码:


#[test]
fn m_test_2() {
    let mut state = VecState::from_iter("abc".chars().into_iter());
    let exp:Parser = abc!(|state: &mut State|->Status{
        try!(eq('a')(state));
        let re = try!(eq('b')(state));
        try!(eq('c')(state));
        try!(eof()(state));
        Ok(re)
    });
    let re = exp.parse(&mut state);
    assert!(re.is_ok());
    let data = re.unwrap();
    assert_eq!(data, 'b');
}

在目前,经过对几门语言的比较和学习,对各个移植版本的升级也可以告一段落了。