之前實作Computation book的範例程式碼,一直卡關的第2章原始碼解析的部分,最近突然有了大幅的進展 (因為在網路上找到一個別人寫好的相關原始碼),讓我突然頓悟rust 相關的設計,這裡解釋一些常用的技巧。

在建tree的部分,C++ 可能會定義介面用的base class ,再定義下面的derived class,這樣就能用base class 作為介面建tree, Rust 中我們可以用enum 做到這點,enum 除了像C like 的用法,也能指向物件,內含匿名或是有名的物件,我在這裡都是用匿名物件來實作:
https://doc.rust-lang.org/book/enums.html

#[derive(Clone)]
pub enum Node {
    Number(i64),
    Add(Box<node>, Box<node>),
    Multiply(Box<node>, Box<node>),
    Boolean(bool),
    LessThan(Box<node>, Box<node>),
    Variable(String),
    DoNothing,
    Assign(String, Box<node>),
    If(Box<node>, Box<node>, Box<node>),
    Sequence(Box<node>, Box<node>),
    While(Box<node>, Box<node>),
}

之所以要Box 而不是Node,在rustc –explain E0072 中有介紹,大體是recursive structure 裡,子物件若要包含父物件, 一定要是Box 或是Reference &,否則程式算不出Node 需要多大。

用了enum 之後,其他function 的實作也就是在enum 上實作,並用match 來處理所有enum 可能出現的結果,例如我的reducible() 實作:

fn reducible(&self) -> bool {
    match *self {
        Node::Number(_) | Node::Boolean(_) | Node::DoNothing => false,
        _ => true,
    }
}

reduce 的實作則是:

fn reduce(&self, environment: &mut Environment) -> Box<Node> {
    match *self {
        Node::Add(ref l, ref r) => {
            if l.reducible() {
                Node::add(l.reduce(environment), r.clone())
            } else if r.reducible() {
                Node::add(l.clone(), r.reduce(environment))
            } else {
                Node::number(l.value() + r.value())
            }
        }
    }
}

……reduce會將原本的node 替換掉,只能用self 當參數(好其實是我用&self就會出一些很詭異的錯誤,我還不知道怎麼解)

注意這邊的 match 裡要寫 ref l, ref r。 如果寫 Node::Add(l, r) ,這樣的意思是,如果我們 match Add,裡面的 l, r 的所有權有可能會被轉移掉,例如

return l

或是

return Box<l>

function 又是寫 reduce(&self) 的話,表示我self 是跟人用reference 借來的,我不能又把self的所有權又送出去,所以rustc 會警告match *self這行:

cannot move out of borrowed content

即便你的code 沒有這麼做,rust 還是不允許這麼寫。

ref l, ref r 表示我 l, r 仍然是借用,而借用的內容是傳不回去的,這樣一路從 self 下來都是借用,就沒有所有權轉移的問題; 要傳回一個跟 l 或 r 一樣的內容,就要在Node 的屬性加上#[derive(Clone)],用 l.clone()複製一個新的物件,試圖去 dereference l 或 r (*l, *r) 同樣都會被 rust 拒絕。

使用trait:

在本來的範例中他是將程式碼分到不同的資料夾,並用

require_relative '../syntax/add'

的方式來擴展原有的程式,Rust不允許使用在上層資料夾裡面的程式碼,我這裡是利用trait 來達成模組化的目的。

Syntax.rs 中只定義AST 裡所需要的物件。
其他的function 我們都用trait 來定義,如果我們要用small_step 的reduce

use reduce::{Reduce}

裡面就實作相關的function,好處是若main不需要reduce 的功能,不要use 這個trait 即可。

相關的原始碼可以看這裡