最近用 GPUI 框架實作了一個圖形化計算機,過程中學到不少 Rust 的實用技巧。這篇文章整理了從這個專案中可以學到的 Rust 程式設計概念。

專案概述

這個計算機支援基本四則運算、指數運算 (^)、取餘數 (%)、變數賦值,以及內建常數 pie。UI 使用 Zed 編輯器團隊開發的 GPUI 框架,這是一個 GPU 加速的 Rust UI 框架。

gpui-calculator.png

1. 使用 Enum 定義 Token

Rust 的 enum 非常適合用來定義詞法分析器 (lexer) 的 token 類型:

#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    Name(String),    // 變數名稱
    Number(f64),     // 數字
    Plus,            // +
    Minus,           // -
    Mul,             // *
    Div,             // /
    Mod,             // %
    Pow,             // ^
    Print,           // ;
    Assign,          // =
    LP,              // (
    RP,              // )
}

這種設計的好處:

  • 類型安全:編譯器確保你處理所有可能的 token 類型
  • 資料攜帶Name(String)Number(f64) 可以攜帶額外資料
  • 模式匹配:配合 match 表達式,程式碼清晰易讀

2. 實作 Iterator Trait

將 lexer 實作為 Iterator,讓程式碼更符合 Rust 慣例:

pub struct TokenStream {
    input: Vec<char>,
    offset: usize,
}

impl Iterator for TokenStream {
    type Item = Token;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            if self.offset >= self.input.len() {
                return None;
            }

            let ch = self.input[self.offset];
            self.offset += 1;

            match ch {
                '+' => return Some(Token::Plus),
                '-' => return Some(Token::Minus),
                '*' => return Some(Token::Mul),
                // ... 其他 token
                x if x.is_whitespace() => continue,
                _ => return None,
            }
        }
    }
}

實作 Iterator trait 的好處:

  • 可以使用 .collect() 收集所有 token
  • 可以搭配其他 iterator 方法如 .map().filter()
  • 延遲求值 (lazy evaluation),不會一次解析完整個輸入

3. 遞迴下降解析器

計算機的核心是一個遞迴下降解析器 (recursive descent parser),這是編譯器課程中的經典技術:

impl Calculator {
    // 加減法 (最低優先級)
    fn expr(&mut self, token: Option<Token>) -> Result<f64, String> {
        let mut left = self.term(token)?;
        loop {
            match self.current_token {
                Some(Token::Plus) => left += self.term(None)?,
                Some(Token::Minus) => left -= self.term(None)?,
                _ => return Ok(left),
            }
        }
    }

    // 乘除法、取餘數 (中等優先級)
    fn term(&mut self, token: Option<Token>) -> Result<f64, String> {
        let mut left = self.power(token)?;
        loop {
            match self.current_token {
                Some(Token::Mul) => left *= self.power(None)?,
                Some(Token::Div) => left /= self.power(None)?,
                Some(Token::Mod) => left %= self.power(None)?,
                _ => return Ok(left),
            }
        }
    }

    // 指數運算 (高優先級,右結合)
    fn power(&mut self, token: Option<Token>) -> Result<f64, String> {
        let base = self.prim(token)?;
        if let Some(Token::Pow) = self.current_token {
            let exp = self.power(None)?;  // 遞迴實現右結合
            Ok(base.powf(exp))
        } else {
            Ok(base)
        }
    }

    // 基本元素 (最高優先級)
    fn prim(&mut self, token: Option<Token>) -> Result<f64, String> {
        // 處理數字、變數、括號、一元負號
    }
}

這個設計的關鍵:

  • 運算子優先級:透過函數呼叫順序自然實現(exprtermpowerprim
  • 結合性:左結合用迴圈,右結合用遞迴
  • 錯誤處理:使用 Result<f64, String>? 運算子傳遞錯誤

4. GPUI 的 Render Trait

GPUI 使用 trait 來定義 UI 元件的渲染邏輯:

impl Render for CalculatorApp {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        div()
            .flex()
            .flex_col()
            .size_full()
            .bg(rgb(0x1e1e1e))
            .p_4()
            .gap_3()
            .child(self.render_display())
            .child(self.render_keypad(cx))
    }
}

GPUI 的特色:

  • 宣告式 UI:類似 SwiftUI 或 Flutter 的風格
  • Tailwind 風格的樣式.flex().p_4().gap_3() 等方法
  • GPU 加速:比 Electron 少 60-80% 記憶體使用量

5. 事件處理與閉包

按鈕點擊事件使用閉包 (closure) 和 cx.listener() 來處理:

div()
    .id(SharedString::from(format!("btn_{}", label)))
    .on_click(cx.listener(move |this, _event, _window, cx| {
        match label_owned.as_str() {
            "C" => this.clear(),
            "⌫" => this.backspace(),
            "=" => this.evaluate(),
            "±" => this.toggle_sign(),
            "π" => this.append("pi"),
            other => this.append(other),
        }
        cx.notify();  // 通知 UI 更新
    }))

這裡展示了 Rust 閉包的幾個重點:

  • move 關鍵字將變數所有權移入閉包
  • 閉包可以捕獲外部變數(label_owned
  • cx.notify() 觸發 UI 重新渲染

6. 模組化設計

專案採用清晰的模組結構:

src/
├── main.rs           # 進入點 + UI 元件
└── calculator/
    ├── mod.rs        # 模組宣告
    ├── token.rs      # Token 定義
    ├── lexer.rs      # 詞法分析器
    └── parser.rs     # 語法分析器 + 測試

這種結構的好處:

  • 關注點分離:UI 和計算邏輯分開
  • 可測試性:parser 可以獨立測試
  • 可重用性:calculator 模組可以用於其他專案

7. 單元測試

Rust 內建強大的測試框架,測試直接寫在同一個檔案:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_exponentiation() {
        let mut calc = Calculator::new();
        assert_eq!(calc.evaluate("2^3").unwrap(), 8.0);
        assert_eq!(calc.evaluate("2^3^2").unwrap(), 512.0);  // 右結合
        assert_eq!(calc.evaluate("2 + 3^2").unwrap(), 11.0);  // 優先級
    }

    #[test]
    fn test_modulo() {
        let mut calc = Calculator::new();
        assert_eq!(calc.evaluate("10 % 3").unwrap(), 1.0);
        assert_eq!(calc.evaluate("17 % 5").unwrap(), 2.0);
    }
}

Rust 測試的特點:

  • #[cfg(test)] 確保測試碼不會編譯進正式版本
  • #[test] 標記測試函數
  • 執行 cargo test 即可運行所有測試

8. 錯誤處理

使用 Result 類型和 ? 運算子進行錯誤處理:

pub fn evaluate(&mut self, input: &str) -> Result<f64, String> {
    self.tokens = TokenStream::new(input).collect();

    if self.tokens.is_empty() {
        return Err("empty expression".to_owned());
    }

    let first_token = self.next_token();
    self.expr(first_token)
}

// 使用 ? 運算子傳遞錯誤
fn expr(&mut self, token: Option<Token>) -> Result<f64, String> {
    let mut left = self.term(token)?;  // 如果 term 失敗,錯誤會被傳遞
    // ...
}

這種模式的好處:

  • 明確標示可能失敗的操作
  • 錯誤類型清楚(這裡用 String,正式專案建議用自定義錯誤類型)
  • ? 運算子讓錯誤處理程式碼簡潔

總結

這個計算機專案雖然簡單,但涵蓋了多個重要的 Rust 概念:

概念 應用場景
Enum + Pattern Matching Token 定義與解析
Iterator Trait 詞法分析器
Recursive Descent 語法分析器
Trait (Render) UI 元件定義
Closure 事件處理
Module System 程式碼組織
Unit Testing 驗證解析邏輯
Result + ? 錯誤處理

如果你想深入學習 Rust,動手實作一個計算機是很好的練習。從簡單的四則運算開始,逐步加入變數、函數、甚至自定義語法,你會學到很多編譯器和語言設計的知識。

參考資源