Pages

Friday, April 22, 2011

VOCABULARY in Forth

最近找了一些 Forth 的舊文章來看,也看了一些舊程式碼。在這些資料中發現很多以前沒學會的東西,正好趁這機會補習一下。這次要介紹的是 Forth 裡的 VOCABULARY

如果對 Forth 有一點瞭解,或者看過我以前翻譯的 PForth 教學,應該會知道 Forth 中的每一個執行單位叫做 word。相對於其他程式語言來說,就像是函式那樣的東西。Vocabulary 的意思是「詞彙」,簡單的說就是 word 的集合。更進一步,大概就像是 Library 的設計。

以上是我一直以來對 Vocabulary 的理解。以前我還在念書時,想找個程式語言來學,卻糊裡糊塗入了 Forth 坑。但當時 Forth 並不是主流,不但買不到書,也找不到人教,很多東西就這樣斷章取義,隨意理解了。等到實際要用時才知道問題大條,趕緊花了點時間玩一下,原來誤會了那麼久的東西,其實是那麼簡單又強大的設計。

其實 Vocabulary 除了 Library 的功能之外,更重要的功能其實是 Namespace 的控制。

Forth 的核心設計有幾個重要的基礎元件,其中的 Dictionary 用來存放資料與新定義的 word。Vocabluary 可以將 Dictionary 中的 word 分組,並影響後續使用的搜尋順序。用比較現代一點的說法,其實就是 Namespace 與 Scope 機制。以下會簡單介紹一下 Forth 的 Vocabulary 設計與運作方式,參考資料來源是 GForth 附的程式碼與實驗結果。

在 Forth 系統誕生之時會提供一個最基礎的 Namespace,在這個 Namespace 裡就只有最基本的 Namespace 定義與管理工具,連基本的堆疊運算辦不到。以 GForth 為例,這個最基本的 Namespace 叫做 Root,只提供了 5 個 word:

order set-order forth-wordlist Forth words

其中比較重要的是 ORDERFORTH 這兩個 word。 ORDER 的作用是列出目前作用中的 Namespace,以及目前的定義空間; FORTH 是一個 Vocabulary,裡面定義了撰寫 Forth 程式會使用到的 word。先來看看 ORDER 的結果,在 GForth 中輸入 ORDER 會印出:

Forth Forth Root     Forth

這串訊息要分成三部份來看:

  1. 最左邊的 Forth:代表目前作用中,搜尋順序最高的 Vocabulary。依照以前的術語,這個位置代表 CONTEXT Vocabulary
  2. 最右邊的 Forth:代表目前的定義空間,所有新定義的 word 將會加入 Forth 這個 Vocabulary 中。依照以前的術語,叫做 CURRENT Vocabulary
  3. 中間的部份:一個 Vocabulary 清單,代表目前的搜尋順序。當系統在 Context Vocabulary 中找不到要執行的 word 時,就會依照這邊的順序一個一個找下去

以這個顯示結果來說,在 C++ 看到的可能是:

using namespace Root;
namespace Forth
{
  ...
}

這三個部份都是可以隨意改變的。在 Forth 中,每個 Vocabulary 其實也是一個可執行的 word,執行後的動作就是更新 Context Vocabulary。Forth 允許兩個同名的 word 定義在同一個 Vocabulary 或不同 Vocabulary。定義在同一個 Vocabulary 時,只會搜尋到最後定義的 word;而定義在不同 Vocabulary 時,就是靠 Context 決定會找到哪一個。

舉個簡單的例子來說明。假設我有 A 和 B 兩個 Vocabulary,其中 A 定義了兩個 foo:

: foo ( -- )
  ." in a" cr
  ;

: foo ( -- )
  ." in a (2)" cr
  ;

B 也定義了一個同名的 foo

: foo ( -- )
  ." in b" cr
  ;

假設我想執行 A 裡面的 foo,必須先切換 Context 到 A:

\ Set Context
A  ok

\ Exec foo
foo in a (2)
 ok

可以看到真正執行的是最後定義的 foo。若想執行 B 裡面的 foo,同樣也要先切換 B:

B  ok
foo in b
 ok

除了切換 Context 外,其他兩個部份也可以隨意設定。但在介紹方法之前得先引入幾個小工具。Forth 對於整個 Vocabulary 的管理提供了幾個小工具。比較重要的有 VOCABULARYONLYALSODEFINITIONS

VOCABULARY 可以建立新的 Vocabulary。以剛剛的例子來說,建立 A 與 B 的方法是:

vocabulary A  ok
vocabulary B  ok

ONLY 的意思是只搜尋 Root 這個 Vocabulary。在 GForth 中可以看到 ORDER 的結果是:

only  ok
order Root Root     Forth  ok

可以看到去除左右兩個 Vocabulary 後,中間的搜尋清單只剩下 Root。

ALSO 的意思是說,將目前的 Context Vocabulary 加入搜尋清單。加入以後,不管 Context 怎麼變動,都可以不受影響。以前面的例子繼續實驗。我把 A 加入搜尋清單後,再改變 Context,看看是否可以找到定義在 A 的 foo:

only forth also \ reset search list
A also          \ add A to search list
forth           \ set Context

\ current environment
order Forth a Forth Root     Forth

\ exec foo
foo in a (2)
 ok

看來的確可以自由配置搜尋清單。

最後一個是 DEFINITIONS,用來決定新定義的 word 該屬於哪一個 Vocabulary,執行後會把 Context Vocabulary 設定到 Current Vocabulary。以前面的例子來說,我想在 A 裡面定義一個 foo,可以這樣做

A definitions
: foo ( -- )
  ." in a" cr
  ;

Forth 的 namespace 設計很簡單,卻威力強大。只加了這幾個工具就可以產生一些好玩的應用。舉例來說,為了 debug 方便,我會在 word 定義中加上一些 log 訊息。有時候跑一次程式後再檢查 log 會比進入 debugger 有效率得多。例如:

: foo ( -- )
  \ ." >> enter foo" cr
  ." exec foo" cr
  \ ." << exit foo" cr
  ;

比較麻煩的是加了 log 就得移除,需要一直修改程式。利用 Forth 的特性,我可以在不同的 Vocabulary 設計一組外掛用的 wrapper word,專門用來顯示一些 log 訊息:

vocabulary tool
tool also definitions
: foo ( -- )
  ." exec foo" cr
  ;

vocabulary tool-debug
tool-debug definitions
: foo ( -- )
  ." >> enter foo" cr
  foo
  ." << exit foo" cr
  ;

可以看到我在 debug 用的也是同名的 foo,但因為定義在不同 Vocabulary,所以不會互相干擾。在一般情況下我可以正常地執行 foo:

only forth also definitions tool  ok
foo exec foo
 ok

需要追蹤執行流程時,就將新工具外掛進來

tool-debug  ok
foo >> enter foo
exec foo
<< exit foo
 ok

不需要 log 的話可隨時換回正常版的 foo。

雖然很容易就做到這樣有趣的效果,但其實這只是簡單的示範而已, 並不具太大的實用價值。要達到比較實用的程度,可能得搭配 Forth 的 DEFER word 機制,將 log 輸出功能設計成一個可抽換的 policy word,並在執行前決定要設置成哪一個 policy1。運作起來類似 late binding 的效果。

Forth 這麼古老2的東西 (其實不比 Lisp / Fortran 晚多少喔),竟然連 Namespace 管理機制都設計進去了,以現代程式語言的角度來看還真是一點都不退流行阿!而且 Forth 流行的時代,其實主要做一些硬體控制的工作,有複雜到需要加入 Namespace 機制嗎3?在那個高階語言都還不太流行的年代,天曉得這些前輩高人們怎麼想到的?


1. 應該很多人猜得到這是從哪本書借來的點子

2. 想瞭解 Forth 的誕生,可以參考 Forth - The Early Years (中譯) 這篇文章

3. 事實上就是有這個需要,希望以後有機會能介紹到這部份