Skip to content

Commit

Permalink
零・一版分詞算法
Browse files Browse the repository at this point in the history
  • Loading branch information
MROS committed Sep 24, 2024
1 parent 2192190 commit 5b6f9db
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 4 deletions.
10 changes: 7 additions & 3 deletions book/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,16 @@ export default defineConfig({
items: [
{
text: "音界咒零.一版設計與定義",
link: "/音界咒零.一版設計與定義",
link: "/零.一版/音界咒零.一版設計與定義",
},
{ text: "全形字體選擇", link: "/全形字體選擇" },
{
text: "零.一版編譯目標:精五門(RISC-V)真言極簡子集",
link: "/零.一版編譯目標:精五門(RISC-V)真言極簡子集",
text: "編譯目標:精五門(RISC-V)真言極簡子集",
link: "/零.一版/編譯目標:精五門(RISC-V)真言極簡子集",
},
{
text: "分詞",
link: "/零.一版/分詞",
},
],
},
Expand Down
Binary file added book/image/零・一版分詞狀態機.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion book/序・去往新世界.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

將現代軟體當做一個世界,從學會寫程式的第一天開始,絕大多數的人就永遠也脫離不了它。本作企圖指引人們如何離開原世界(下稱塵界),再造一個新世界(洞天、仙界、天界)。

在這個新世界,不必為了理解一段彆扭的程式碼去考古三十年前的郵件往返,不必再與費解的歷史遺留問題做鬥爭,人不該活在前人定義好的世界裡反覆修補。來吧!道友,去創造一個新世界,即使要設計世界的法則會讓你絞盡腦汁;即使維持世界的運轉會令人精疲力竭...
在這個新世界,不必為了理解一段彆扭的程式碼去考古三十年前的郵件往返,不必再與費解的歷史遺留問題做鬥爭,人不該活在前人定義好的世界裡反覆修補。來吧!道友,去創造一個新世界,即使設計世界的法則讓你絞盡腦汁;即使維持世界的運轉令你精疲力竭...
172 changes: 172 additions & 0 deletions book/零.一版/分詞.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
是什麼構成一篇漢語文章的呢?從小到大列舉,最小單位是「字」(通常狀況不會再去拆部首),「字」組成「詞」,「詞」再組成「句」,「句」組成「段落」,「段落」組成「文章」。

計算機要處理漢語時,第一步通常就是「分詞」,例如說 `嫉妒使我面目全非` ,可以拆解成「嫉妒」、「使」、「我」、「面目」、「全」、「非」。也許有人會把「面目全非」直接分一個詞,也不能說錯,自然語言本就沒必要只能唯一拆解。

法咒(程式語言)跟自然語言仍是有些共通之處,同樣能拆解成不同層級。

想像編譯器讀取音界咒文件時,它看見的是一個又一個的字元,而分詞器做的事情就很接近上述的漢語分詞器。但法咒(程式語言)不能有岐義,因此需要藉助特殊符號,如空白鍵或音界號來確定詞與詞之間的邊界。

來看個範例:

```音界
元.人數=(11+3)*4
人數+1
```

第一行 `元.人數=(1+3)*4` 依序是分解為

1. ``,關鍵字「元」
2. ``,音界號
3. `人數`,這是個變數
4. ``,等號
5. ``,左括號
6. `11`,數字
7. ``,運算子「加」
8. ``,數字
9. ``,右括號
10. ``,運算子「乘」
11. ``,數字
12. `\n`,換行

而第二行 `人數+1` 依序是

1. `人數`,這是個變數
2. ``,運算子「加」
3. ``,數字
4. `\n`,換行

## 定義

下表羅列了零・一版音界咒的所有詞:
|| 種類 | 細分含義 |
| ----------- | --- | ---- |
|| 關鍵字 | |
|| 左括號 | |
|| 右括號 | |
|| 運算子 ||
|| 運算子 ||
|| 運算子 ||
|| 運算子 ||
|| 等號 | |
|| 音界號 | |
| \n | 換行 | |
| [0-9]+ | 數字 | |
| 除以上詞之外的所有字串 | 變數 | |

前幾項全是單字詞,要分出它們是再簡單不過,但最後兩種詞「數字」、「變數」就可能是多個字組成的了。

`[0-9]+` 是正規表達式,其意思是,字串由一到多個0123456789組成。

## 實作

上一節中變數的詞法定義並不清楚,例如,變數不可以是「元」,但能不能是「元氣」呢?變數能不能包含數字,像是「2號機」?若允許這樣的寬鬆定義,分詞會較為困難,當讀取到「元」時,並無法確定現在正在讀取「元」關鍵字,同時也可能只讀到「元氣」變數的開頭而已。同理,當讀到[0-9]時,無法判定正在讀取數字,還是某個變數的開頭。

但無妨,早已有成熟的演算法能應對這類複雜狀況,在零・一版的簡單狀況,倒也不必構思出通用算法才能分詞,只要仔細分析所有狀況就可以了。

為了方便後續表達,先令 x 代表除了零字詞以及[0-9]的所有字元集合。

當目前讀取到的字串是...

- 除了元之外的單字詞,亦即()+−*/=・
- 讀取到一個字即可確定為詞
-
- 可能是變數的前綴,當下一個字元屬於 x 或 [0-9] ,即確定該詞為變數
- 當下個字元不是上述狀況時,是元關鍵字
- [0-9]+
- 可能是變數的前綴,當下一個字元屬於 x ,即確定該詞為變數
- 當下個字元不是上述狀況時,是數字
- x
- 必是變數

上述分析以狀態機可表達為下圖

![零・一版分詞狀態機](../image/零・一版分詞狀態機.png)

其中每個狀態向外的虛線表示,若下個字元匹配不到向外的實線,就走回原點,並以當下狀態分詞。

該圖並沒有畫出所有單字詞,僅以加減為例,其餘單字詞請道友自行想像。

畫出狀態機之後,以法咒(程式語言)依樣畫葫蘆實作,就是件很容易的事了。

貧道採用 Rust 法咒來撰寫零版編譯器。

### 類型定義

首先,寫出合適的類型來表達詞的種類:

```rust
// Rust 慣以駝峰式命名類型
// 漢語無大小寫,本作慣例以全形英文字母O來當類型的開頭
// Rust 管制識別符的字元組成,不允許 ◉、⦿、☯︎ 等等萬國碼,故採用常見的全形O來代替。
#[derive(Debug)]
enum O運算子 {
加,
減,
乘,
除,
}

#[derive(Debug)]
pub enum O詞 {
元,
左括號,
右括號,
運算子(O運算子),
等,
音界,
數字(i64),
變數(String),
}

// TODO: 要實作的分詞術
pub fn 分詞(源碼: String) -> Vec<O詞> {
unimplemented!();
}
```

### 處理單字詞

見招拆招,很容易。

```rust
pub fn 分詞(源碼: String) -> Vec<O詞> {
let mut 詞列: Vec<O詞> = Vec::new();
forin 源碼.chars() {
match 字 {
'+' => {
詞列.push(O詞::運算子(O運算子::加));
}
'-' => {
詞列.push(O詞::運算子(O運算子::減));
}
'*' => {
詞列.push(O詞::運算子(O運算子::乘));
}
'/' => {
詞列.push(O詞::運算子(O運算子::除));
}
'=' => {
詞列.push(O詞::等);
}
'(' => {
詞列.push(O詞::左括號);
}
')' => {
詞列.push(O詞::右括號);
}
'元' => {
詞列.push(O詞::元);
}
'・' => {
詞列.push(O詞::音界);
}
_ => {
// TODO: 處理多字詞
}
}
}
詞列
}
```

### 處理多字詞

0 comments on commit 5b6f9db

Please sign in to comment.