FORTH精義
從電腦架構的觀點來看, FORTH是一個有兩個堆疊的CPU,一個返回堆疊堆放程式返回的地址,和一個参數堆疊堆放子程式間傳遞的参數。因為参數的傳遞是經由一個獨立的堆疊,所以子程式可以很方便地串聯起來,成為新的子程式。這樣建造的子程式,可以看作是一個虛擬機的指令。這個虛擬機指令集的數目和功能是沒有限制的,因而能夠讓使用者自行設計解決他手頭上應用問題最有效的指令集。
從電腦程式的觀點來看, FORTH是一個處理指令序列的語言。它有一套基本的指令集,而且允許使用者用以下的方法,定義新的指令: : <新指令> <現有指令序列> ;
以後執行新指令時,即執行<現有指令序列>中所有的指令。新指令是編成機器碼存在記憶裡,所以佔的記憶空間小,而執行速度快。新指令馬上又可以和現有指令一起編造更新,更高階的指令。用這種漸進的方法建造新指令,是解決程式問題最簡單,最直接的途徑。
FORTH的指令,不論是系統原有的基本指令或是使用者自己定義的新指令,都是結構化程式理論中揭櫫的結構。在新指令定義中的<現有指令序列>,除了有直線串聯的指令外,也允許各種分支結構和迴路結構。因此FORTH程式是不折不扣的結構化程式。
傳統的電腦程式語言中,子程式間参數的傳遞是靠著子程式裡的參數序列。用参數序列來傳遞参數,是非常複雜的工作,需要很複雜的編碼器來處理。子程式因而必須在主程式設定的環境中才能工作,也不是真正的結構化模組。
FORTH用参數堆疊在子程式間傳遞參數,使得子程式在操作系統中都成為可以獨立工作的結構單元,也可以組合成為新的指令或結構單元。使得程式簡化,程式發展的工作效率提高,軟體的品質也容易確保。
為什麼要採用FORTH?
在說明了FORTH語言的特性,和電腦的基本原理(四大定律)後,我認為我們可以從理論上証明FORTH是最優越的電腦語言。
首先,我們要將電腦分類。電腦基本上是可以分為兩類:專用型電腦和通用型電腦。專用型電腦是為特殊應用問題而設計的電腦,它有一套為這特殊應用問題而設計的指令集,因而可以用最有效的方法解決這個特殊應用問題。通用型電腦則是有一套什麼工作都能做,但都不能做得很好的指令集。
解決一個問題最有效的辦法當然是造一部專用型電腦,它的指令集,就是專門用來解決這個問題的。麻煩的事是為一個問題而設計一部電腦,所費不貲,談何容易。以前只有美國的國防部才有錢,有資源做這個傻事。蘇聯解體之後,美國的國防部也幹不來這種事了。
成功的電腦一定是有很多買主的商品,要賣得多就不能太貴。不能貴就不能做得太考究。所以成功的電腦都是通用型的。它的指令集只要夠用而且比隔壁公司的電腦好一點,它的速度只要比隔壁公司的電腦快一點,它的價錢只要比隔壁公司的電腦便宜一點就可以了。
使用者因為出不起開發專用型電腦的費用,只好買一部通用型的電腦湊合著用用。指令集不好用,只好多請幾位軟體工程師多寫點程式彌補指令集的不足。只要程式不是太大,只要電腦夠快,還是可以解決目前的問題。
用一個通用型電腦來解決該用專用型電腦來解決的問題,最好的辦法是把這個通用型電腦改變成為一個虛擬機,模擬我們真正需要的專用型電腦。在這個虛擬機上,發展專用型電腦的指令集來解決真正的問題。如果這個虛擬機做得好,就可以在這通用型電腦上找到問題最好的答案,即是最理想的指令集。用這虛擬機解決問題,和用專用型電腦一樣得到正確的結果,唯一的差別是速度比較慢。如果速度不是問題,那麼我們就找到了最理想的電腦了,雖然它只是一個虛擬機。
FORTH就是一個最有效的虛擬機。它可以架在任何電腦上,將這部電腦變成一個雙堆疊的虛擬機,執行一套FORTH的指令集。這套指令集是可以無限延伸的,因此可以很方便地模擬任何專用型電腦。發展解決問題的指令集,是解決問題最簡單,最直接的方法。從這樣的論點來看,FORTH是最優越的電腦語言。
在現在,IC工業的技術已經可以將千萬閘的邏輯線路整合到單晶片上,系統晶片的時代已經來臨了。在單晶片上整合龐大的系統,必須要能夠掌握CPU及上面的軟體。FORTH的虛擬機顯示一個高效率的FORTH CPU可以作為系統晶片的核心,許多FORTH的指令都可以包括在CPU裡面,使用者可以很方便地在晶片上發展應用程式。當應用程式確定後,我們還可以把最關鍵的應用指令,整合到CPU裡面去。這種技術,突破了電腦硬體和軟體的分界線,它的發展前景和經濟效益,是不能估計的。
這本易符真經就是在傳遞這個願景,我們能夠自己設計CPU,操作系統,和所有發展硬體及軟體的工具。我們可以造自己的電腦,解決我們工業及社會所需要的電腦及微控器應用的問題。
weForth系統簡介
weForth是一個為Windows環境設計的FORTH程式語言系統。它讓你在Windows的一個視窗裡發展高效率的FORTH程式。在本書後面討論的P24組合器,P24的操作系統等程式,都是用weForth寫成的。要完全掌握這些技術,你必須很熟悉weForth系統。所以在這一章裡,我們要詳細說明如何使用weForth及如何用weForth來寫程式。
weForth系統包裝在一個壓縮的檔案weForth.ZIP。用UNZIP展開全部檔案,並將全部檔案存入一個你選定的封套中。假設個這封套名字是'weForth',你可以看見這些檔案是在這封套中,顯示於這以下的視窗圖像內。
(weForth的視窗圖像)
你有兩個方法開動weForth系統。一個是用滑鼠點選weForth.EXE檔案。按滑鼠兩下載入weForth。weForth首先顯示一個檔案選擇交談區,如以下螢幕的選擇視窗圖像,請你選擇一個.FEX延伸檔。這些延伸檔案包含下列示範程式:
CONSOLE.FEX 很好用的控制台及很多工具 EXTEND.FEX 為FORTH語言程式延伸的系統 HELLO.FEX 顯一個訊息方框的示範程式 SERIAL.FEX COM1串列輸出入埠的示範程式
(FEX 檔案的選擇視窗圖像)
另一個開動weForth系統的方法是用滑鼠直接點選FEX檔案。如果你用滑鼠直接點選CONSOLE.FEX如此開動weForth系統及CONSOLE.FEX中的示範程式。這控制台視窗看起來像這以下的螢幕:
(CONSOLE.FEX檔案的視窗圖像)
假如你是第一次使用weForth,你將看見一個對話方框,,如於這以下的螢幕所示,請求你選擇一個.EXE檔來執行。
(weForth系統選擇對話方框的視窗圖像)
請點選weForth.EXE,weForth系統即被載入。它的控制台視窗開動,你現在可以鍵入指令使用weForth。
這本手冊中有17個練習幫助你認識weForth系統,和讓你看如何寫weForth程式。我希望你會輸入這些練習並使它們正確地工作。在輸入這些練習時,你將遭遇很多問題,像打錯字,忘了在指令間留空格,輸入錯的數值,和堆疊上的參數用完等等。不過這是學習一個新的電腦語言必然經過的困難。在weForth系統中有近300個指令,這些練習和練習中用了54個指令。熟悉了這54指令如何工作,你將會用weForth寫程式解決大部份的程式問題。
weForth指令都用參數堆疊來傳送它們需要的參數,並將它們產生的結果推上堆疊。要能純熟使用weForth,我們必須對每個指令從參數堆疊上取得的參數和推上堆疊的參數有完全的了解才行。因此我們除了要解釋每個指令的功能,還要詳加說明它們對參數堆疊的使用方式。每個指令的名稱後面都有一對括弧,裡面是參數堆疊的注釋。在括弧內,橫線左邊是輸入的參數,橫線右邊是輸出的參數。參數在堆疊上的排列是最靠右邊的在最頂端。例如(輸入1 輸入2 ... -- 輸出1 輸出2 ...)的形式。
所有的參數都是32位元的數值,但是依它們的來源或用途,各個參數可以有個別的特性。為了標示參數的特性,我們用以下的一套符號作參數特性的注解:
參數代碼 參數的特性 f 布林邏輯旗號,其值是0或者是-1 ? 真值,非0即是真,0即是非真 c 8位元ASCII字碼 b 任何8位元數值 w 任何32位元數值 n 32位元整數 u 32位元正整數 a 32位元地址 d 64位元雙整數以下是54個最常使用的weForth指令的一覽表。他們是按英文字母的順序排列出的,以方便你搜尋。我們在17個練習中會用到這些指令。在附錄中我們詳細列出所有的weForth指令。
指令 參數 功能 ! w a -- 將w儲存在地址a中 ( <comment> ) -- 注解,略過字串<comment> * n1 n2 -- n3 n1乘n2 */ n1 n2 n3 -- n4 比例(n1*n2)/n3 */MOD n1 n2 n3 - 商 餘 比例 (n1*n2)/n3的商和餘數 + n1 n2 -- n3 n1加n2 +! n a -- 將n加進在地址a中的數值 - n1 n2 -- n3 n1減n2 . n -- 顯示數字n ." <text>" -- 顯示文句<text> .R n u -- 在u欄位中顯示數字n,向右對齊 .S -- 顯示參數堆疊的內容 / n1 n2 - 商 n1除n2 2DROP w w -- 彈出並丟棄參數堆疊頂端二數值 2DUP w1 w2 - w1 w2 w1 w2 複製參數堆疊頂端二數值 : <name> -- 開始一個新指令,名稱是<name> ; -- 結束一個新指令 < n1 n2 -- ? 假如n1<n2,留下真值 = n1 n2 -- ? @ a -- w 由地址a讀出數值w AND w1 w2 -- w3 w1 AND w2 BASE -- a 輸入輸出轉換基數的地址 BEGIN -- 開始一個迴路 BL -- 32 將32推上參數堆疊 CELLS n - 4n n乘以4 CR -- 換行 DECIMAL -- 設定十進位 DROP w -- 彈出並丟棄參數堆疊頂端一數值 DUP w1 -- w2 複製參數堆疊頂端一數值 DUMP a u -- 顯示從地址a開始u個字碼的資料 ELSE -- 分開IF-ELSE-THEN分支結構中兩段字串 EMIT c -- 顯示字碼c EXIT -- 結束冒號指令 FOR u -- 開始FOR-NEXT迴路結構 HEX -- 設定十六進位 IF ? -- 開始IF-ELSE-THEN分支結構 KEY -- c 從鍵盤讀入一個字碼 MOD n1 n2 - 餘 n1/n2除得的餘數 NEGATE n1 -- n2 負數 NEXT -- 結束FOR-NEXT迴路結構 NUMBER? a - n -1 | a 0 將在地址a的數字字串轉換成數字n及真值-1。 假如轉換不成,將非真值0推上參數堆疊 OR w1 w2 -- w3 w1 OR w2 OVER w1 w2 -- w1 w2 w1 複製參數堆疊頂端第二個數值 QUERY -- 等使用者從鍵盤上輸入一串指令 R@ -- w 在FOR-NEXT迴路中讀出迴路的指數 REPEAT -- 結束BEGIN-WHILE-REPEAT迴路結構 ROT w1 w2 w3 -- w2 w3 w1 旋轉參數堆疊頂端三個數值 SPACE -- 顯示一個空格 SPACES u -- 顯示u個空格 SWAP w1 w2 -- w2 w1 交換參數堆疊頂端兩個數值 THEN -- 結束IF-ELSE-THEN分支結構 U.R u1 u2 -- 在u2欄位中顯示正整數u1,向右對齊 U< u1 u2 - ? 比較正整數,假如u1<u2,留下真值 UM* u1 u2 - d 正整數u1*u2,留下雙整數乘積d UNTIL ? -- 結束BEGIN-UNTIL迴路結構 VARIABLE <name> -- 定義一個變數,名稱是<name> WHILE -- 分開BEGIN-WHILE-REPEAT迴路結構中兩段字串 WORD <text> c -- a 在輸入文字串中分出以字碼c結束的字串並存入a WORDS -- 顯示系統中所有的指令
認識 FORTH
我們先來看看FORTH如何處理數字。FORTH一開動,就等著你從鍵盤上輸入指令或數字。如果你輸入數字,FORTH就將數字轉轉變為32位元的數值,放在參數堆疊上。要檢查参數堆疊上的數值及用這些數值做算術計算,我們需要以下這些指令:
. ( n -- ) 顯示數字n .S ( -- ) 顯示參數堆疊的內容 + ( n1 n2 -- n3) n1加n2 - ( n1 n2 -- n3) n1減n2 * ( n1 n2 -- n3) n1乘n2 / ( n1 n2 - 商) n1除n2
請嘗試輸入這以下的數字:
1 2 3 4 5
數字間必須要用空格分開。你將看見這些數字顯示於堆疊的視窗中。
輸入以下命令 .S
你將看見堆疊上有1 2 3 4 5五個數值。
輸入以下命令
.
你將看見5是從堆疊上移去,並顯示在視窗中。 你可以見到.S不改變參數堆疊的內容,但是”.”會棄除參數堆疊最頂端的數值。
使用加+,減-,乘*,除/ 等四個指令,你就有一個很有用的4功能計算機。你能做這以下的實驗:
12345 67890 +
13 *
77 /
.
這部計算機的限制是所有的數字和資料都是32-位元整數,其範圍是從2,147,483,648到2,147,483,647。大部份的應用這個範圍是夠大的了。
練習1 顯示文字
這是所有電腦程式語言的第一個習題,在螢幕上顯示一段文字Hello, world!寫這段程式一共需要4個指令。它們的正式描述是:
: <name> ( -- ) 開始一個新指令,名稱是<name>
; ( -- ) 結束一個新指令
." <text>" ( -- ) 顯示文句<text>
CR ( -- ) 換行
用:和;建造的新指令我們特別稱為冒號指令。它像是普通程式語言中的巨集指令,將一個序列的現有指令串定一個新名稱而成為一個新指令。新指令和現有的指令一极樣可以執行,也可以編入更新的冒號指令。建造新的冒號指令就是weForth解決問的手段。冒號指令的正式描述是:
: <新指令名稱> <舊指令串> ;
以後用到新指令時,即執行<舊指令串>。冒號指令功能上和巨集指令相同,不同處是冒號指令是編輯成機器碼存在記憶中的。所以佔的記憶容量小,而執行的速度快。
.”是在螢幕上顯示文字的指令。顯示的文字和.”間一定要用一個空格分開,而以下一個”號結束。在weForth系統中,文字可以是英文字串,也可以是中文字串。英文和中文字也可以混用。指令名稱可以是英文字串,也可以是中文字串。英文和中文字也可以混用。所以我們可以用weForth來寫中文程式。但你必須記住,顯示的文字和.”間一定要用一個空格分開,而以下一個”號結束。文字字串中也不能有”號,因為”號是用來結束字串的。
我們這樣子建造一個新的指令HELLO:
: HELLO CR ." Hello, world!" ;
輸入HELLO再加上一個<enter>顯示這文字Hello,world!。
練習2 顯示F字
這指令."能用來建造很多其他有用的指令,可以顯示一些簡單的圖形。下面是要顯示一個5x6點陣內F字所須的兩個新指令:
: bar cr ." *****" ; : post cr ." * " ; : F bar post bar post post post ;
輸入F和一個<enter>,你將看見一個大F顯示在螢幕上。
練習3 顯示中文
中文是麻煩得多了,但是用5x7點的矩陣還是能顯示很多簡單的中文字。例如要顯示”中文”這兩個字,我們要再加幾個新指令:
在冒號指令裡執行EXIT指令時,這個冒號指令立刻結束,EXIT後續的指令串都不再執行。這個指令是打破了結構化程式中的一進一出的規則,使得程式在冒號指令內重重套裝的結構裡一步跳出來。這個指令有時會使程式大幅簡化,有它實用上的必要性。 下面的例子裡的旗號是用輸入的數字和1-6等數字比較而得到的。因為輸入的數字可能要比較五次,所以在執行=指令作比較之前,輸入的數字要複製一份,所以程式有這樣的形式:: center CR ." * " ;
: side CR ." * *" ;
: side1 CR ." * * *" ;
: mid CR ." * * " ;
: chung center bar side1 bar center center center ;
: wen center center bar mid center mid side ;
: 中文 chung wen ;
公子章台走馬 少婦閨閣刺綉 寒士茅舍讀書 屠夫市井揮刀 妓女花街賣俏 乞丐墳墓睡覺 隨便擲這三顆骰子可以有216種不同的組合,有許多組合是蠻有趣的。 DROP ( w -- ) 彈出並丟棄參數堆疊頂端一數值 EXIT ( -- ) 結束冒號指令輸入chung和一個<enter>,你將看見一個”中”顯示在螢幕上。輸入wen和一個<enter>,你將看見一個”文”顯示在螢幕上。
如果你的電腦接受中文輸入的話,輸入”中文”和一個<enter>,你將看見中文兩個字顯示在螢幕上。
練習4 重複的圖形
我們將使用."來顯示一大堆的星號*,用來畫許多圖形。下面我們用FOR-NEXT的迴路指令來建造迴路結構去畫這些圖形。我們需要下面這些指令:
FOR ( u -- ) 開始FOR-NEXT迴路結構
NEXT ( -- ) 結束FOR-NEXT迴路結構
VARIABLE <name> ( -- ) 定義一個變數,名稱是<name>
! ( w a -- ) 將w儲存在地址a中
@ ( a -- w) 由地址a讀出數值w
R@ ( -- w ) 在FOR-NEXT迴路中讀出迴路的指數
VARIABLE定義一個變數,新的變數指令執行時會將此變數的地址推上參數堆疊有了這個地址,我們就可以用!指令將一個數值存入這個地址,或是用@指令將這個數值讀到堆疊上使用。
FOR-NEXT在冒號指令中建造一個迴路結構,它們的正確描述是:
<迴路指數> FOR <迴路指令串> NEXT
FOR將迴路指數從參數堆疊彈出並推上返回堆疊,再設定好迴路的機構。然後我們執行FOR及NEXT間的迴路指令串。NEXT檢查在返回堆疊上的迴路指數,若迴路指數是0,就結束迴路。若迴路指數不是0,就將結束迴路減一,並再次執行<迴路指令串>。如此重複直到迴路指數是0時結束迴路。
<迴路指令串>重複的次數是<迴路指數>加1。所以如果<迴路指數>是0時,<迴路指令串>會執行一次。
在執行迴路昤,迴路指數是暫存在返回堆疊上的。R@指令可以將迴路指數的現值抄回參數堆疊,供迴路指令串中的指令使用。
VARIABLE WIDTH ( 要印出的*號的數目 ) : ASTERISKS ( -- , 從WIDTH讀出數值,並印出這麼多的*號 ) WIDTH @ ( 讀出WIDTH的數值 ) FOR ( 開始迴路結構 ) ." *" ( 印出一個*號 ) NEXT ( 重複 ) ; : RECTANGLE ( 長 寬 -- , 用星號印一個長方形 ) WIDTH ! ( 將寬度設為WIDTH ) FOR CR ( 以高度為迴路的指數 ) ASTERISKS ( 印一行星號 ) NEXT ( 重複 ) ; : PARALLELOGRAM ( 長 寬 -- , 用星號印一個平行四邊形 ) WIDTH ! FOR CR R@ SPACES ( 用空格將星號向右移 ) ASTERISKS ( 印一行星號 ) NEXT ( 重複 ) ; : TRIANGLE ( 寬 -- , 用星號印一個三角形 ) FOR CR R@ WIDTH ! ( 星號的數目隨迴路指數遞減 ) ASTERISKS ( 印一行星號 ) NEXT ( 重複 ) ; 輸入以下指令可以顯示不同的幾何圖形: 3 10 RECTANGLE 5 18 PARALLELOGRAM 12 TRIANGLE練習5 氣候報告
氣侯報告是一個簡單的程式從在參數堆疊上的溫度值印出不同的文字,說明人對溫度的感覺。這是一個好地方來介紹數字比較指令和分支結構。
這裡是一些我們需要的新指令: IF ( ? -- ) 開始IF-ELSE-THEN分支結構 ELSE ( -- ) 分開IF-ELSE-THEN分支結構中兩段字串 THEN ( -- ) 結束IF-ELSE-THEN分支結構 < ( n1 n2 -- ?) 假如n1<n2,留下真值IF-ELSE-THEN分支結構是這樣用的:IF彈出並測試參數堆疊頂端的旗號值,若是真值(不等於0)就繼續執行下面的指令,一直到ELSE,然後跳到THEN執行THEN以後的指令。如果旗號是非真值(等於0),就跳到ELSE執行ELSE以後的指令。這個結構的正確描述是:<旗號> IF <真值指令串> ELSE <非真值指令串> THEN
ELSE和其後的<非真值指令串>可以沒有,而成為只處理<真值指令串>的簡單分支結構:
<旗號> IF <真值指令串> THEN
<真值指令串>和<非真值指令串>中還可以重重套裝IF-ELSE-THEN的分支結構。
<指令比較在參數堆疊頂端二數字,而代之以一個旗號,這個旗號決定在IF-ELSE-THEN的分支結構中該走那一條路。
: WEATHER ( 攝氐溫度 -- ) DUP 15 < IF ." 太冷了!" DROP ELSE 30 < IF ." 好天氣." ELSE ."熱死了!" THEN THEN ; 輸入下列指令,看看有什麼結果。你也可以試其他的溫度。 32 WEATHER 熱死了! 20 WEATHER 好天氣. 10 WEATHER 太冷了!練習6 文字遊戲
這是一個用骰子玩的文字遊戲。用三顆骰子,每顆有六面各別寫上兩個字。一顆寫的是人物名稱, 一顆寫的是地點,一顆寫的是動作。正常的六句詩是:
DUP X = IF <真值指令串> ELSE <非真值指令串> THEN : 人物 ( n -- , 選一個人 ) DUP 1 = IF .” 公子“ ELSE DUP 2 = IF .” 少婦“ ELSE DUP 3 = IF .” 寒士“ ELSE DUP 4 = IF .” 屠夫“ ELSE DUP 5 = IF .” 妓女“ ELSE .” 乞丐“ THEN THEN THEN THEN THEN DROP ; : 地點 ( n -- , 選一個地點 ) DUP 1 = IF .” 章台“ DROP EXIT THEN DUP 2 = IF .” 閨閣“ DROP EXIT THEN DUP 3 = IF .” 茅舍“ DROP EXIT THEN DUP 4 = IF .” 市井“ DROP EXIT THEN DUP 5 = IF .” 花街“ DROP EXIT THEN .” 墳墓“ ; : 動作 ( n -- , 選一個動作 ) DUP 1 = IF .” 走馬“ DROP EXIT THEN DUP 2 = IF .” 刺綉“ DROP EXIT THEN DUP 3 = IF .” 讀書“ DROP EXIT THEN DUP 4 = IF .” 揮刀“ DROP EXIT THEN 5 = IF .” 賣俏“ ELSE .” 睡覺“ THEN ; : 骰子 ( n1 n2 n3 -- , 印一句詩 ) 人物 地點 動作 ; 輸入下列指令,看看有什麼結果。你也可以試其他的組合。 1 2 3 骰子 2 4 6 骰子 5 3 1 骰子 2 5 2 骰子人物,地點和動作三個指令的原理是一樣的,都是從六種物件中選一個。我們顯示有兩種不同的寫法:”人物”是用了五個套裝的IF-ELSE-THEN分支結構;而”地點”和”動作”則是用IF-EXIT-THEN的結構。EXIT結束了現在執行的指令,這樣的程式比較簡單明瞭,避免了重重套裝的IF-ELSE-THEN結構。
練習7 心理醫師
這練習顯示如何使用weForth來和使用者對話。它裝假是一個心理醫師對著病人提供各種症狀的解決方案。你能隨你的喜好加入其他的討論題材。
我們需要的新指令是: KEY ( -- c ) 從鍵盤讀入一個字碼 OR ( w1 w2 -- w3) w1 OR w2 : question CR CR ." 你的問題是?" CR ." 關於(性),(工作),(錢),(健康)? " CR ." 請輸入括弧中的字." CR ; : help CR CR ." 我是心理科吳醫師." CR ." 你好嗎?" CR ." 你今天覺得怎麼樣?" CR ." 好-請按(Y),不好--請按(N)" KEY 32 OR 89 = CR IF CR ." 我很高興." ELSE CR ." 我很抱歉." CR ." 也許今天我可以幫你一點忙." THEN CR ." 我可以幫你解決很多問題." question ; : 性 CR CR ." 你的問題是(太多)還是(太少)?" CR ; : 太多 CR CR ." 你覺得這真是問題嗎?!! 我倒是希望有這個問題." CR ." 如果你真是覺得這是個問題,就去沖涼,洗個冷水澡吧." question ; : 太少 CR CR ." 那你來這兒幹什麼!" CR ." 你該去台北玩玩." CR ." 或者找個朋友." question ; : 健康 CR CR ." 我的建議是:" CR ." 1. 吃兩片阿斯匹靈." CR ." 2. 喝許多果子汁." CR ." 3. 早點上床." question ; : 工作 CR CR ." 我很同情你.現在工作壓力很大." CR ." 而且適合的工作不好找." CR ." 我覺得你可以去開個電腦店." question ; : 錢 CR CR ." 可惜.我現在手頭也十分緊." CR ." 你能不能去推銷金鋼鑚戒?" CR ." 或者娶個有錢的老婆? " CR ." 這樣你就不需要那麼多錢了." question ; : HELP help ; : H help ; : h help ;
輸入HELP開始對話。
我們利用weForth本身的對談性,將病人的問題定成冒號指令,而請病人自己輸入這些指令。這些指令或者提供答案,或者指示病人輸入下一層的指令而繼續對話。
這個模式可以發展成很龐大的人工智慧及專家系統。我們可以將有用的資訊建造在指令中。使用者憑著一張指令表,就可以很方便地檢索他要的資訊。
練習8 匯率換算機
這是第一個做數值計算工作的練習。這個匯率換算機可以換算各國的錢幣。讓我們用這以下的匯率表:
32.49 NT(新台幣) 1 Dollar(美元) 7.50 HK(港幣) 1 Dollar 7.33 RMB(人民幣) 1 Dollar 1兩黃金 Gold 296 Dollars 1兩白銀 Silver 4.09 Dollars
我們可以直接將數字從鍵盤輸入而推上參數堆疊。各個指令從這參數堆疊得到他們的輸入參數,和將它們的結果推上參數堆疊。你在使用weForth和寫程式時必須很清楚使用參數堆疊上的參數數值和順序,才能得到正確的結果。使用weForth成功的關鍵即是正確地控制參數堆疊。
weForth有一個特別的功能,你可以打開參數堆疊的視窗。在參數堆疊的視窗裡,你可以隨時檢查參數堆疊的狀況,確定你的工作有正確的結果。
+ ( n1 n2 -- n3) n1加n2 - ( n1 n2 -- n3) n1減n2 * ( n1 n2 -- n3) n1乘n2 / ( n1 n2 - 商) n1除n2 */ ( n1 n2 n3 -- n4) 比例(n1*n2)/n3. . ( n -- ) 顯示數字n .S ( -- ) 顯示參數堆疊的內容
*/是比例指令,這是匯率轉換的關鍵指令。它將幣值n1從一個單位n3轉換到另一個單位n2。選擇適當的比例值n2及n3,我們可以用整數算術做很精確的換算工作,而不必使用浮點算術。
“.”是印出指令,將參數堆疊頂端的數值彈出並顯示在螢幕上.”.S”指令則將參數堆疊上所有的數值都顯示在螢幕上,但不改變堆疊的內容。所以當你要顯示最後的結果時用”。”指令,當要檢查計算過程中的中間數值時,請使用”.S”指令。
因為各國幣值最容易與美元兌換,換算表都是以美元為基準的。因此我們用美元做這個匯率換算機的基本單位,所有錢幣都先換成美元後進行計算工作,最後的結果再轉換成我們要的幣值。以下我們就以各國錢幣的名稱定為此種錢幣轉換成為美元的指令。從美元轉換成他國幣值的指令則用$符號開頭。下面是這些轉換指令:
: NT ( nNT -- $ ) 100 3249 */ ; : $NT ( $ -- nNT ) 3249 100 */ ; : RMB ( nRMB -- $ ) 100 733 */ ; : $RMB ( $ -- nJmp ) 733 100 */ ; : HK ( nHK -- $ ) 100 750 */ ; : $HK ( $ -- $ ) 750 100 */ ; : GOLD ( nOunce -- $ ) 296 * ; : $GOLD ( $ -- nOunce ) 296 / ; : SILVER ( nOunce -- $ ) 409 100 */ ; : $SILVER ( $ -- nOunce ) 100 409 */ ; : OUNCE ( n -- n, a word to improve syntax ) ; : DOLLARS ( n -- ) . ; 我們能用下列指令來試驗這個匯率換算機: 5 ounce gold . 10 ounce silver . 100 $NT . 20 $RMB . 假如你的錢包裡有很多不同的鈔票,你能全部加起來再以台幣顯示出來: 1000 NT 500 HK + .S 320 RMB + .S $NT .
練習9 溫度換算
換算攝氏和華氏溫度也是一個有趣的問題。溫度轉換與匯率轉換的不同處是這二溫度數值間除了有一個比例值外,還有一個位移差值。以下是華氏到攝氏和攝氏到華氏的轉換指令:
: F>C ( 華氏溫度 -- 攝氏溫度 ) 32 - 10 18 */ ; : C>F ( 攝氏溫度 -- 華氏溫度 ) 18 10 */ 32 + ; 請用以下指令來試驗: 90 F>C . 夏天的溫度 0 C>F . 寒冬的溫度
練習10 九九乘法表
印一個漂亮的九九乘法表需要一些特別的列印指令,它們能把數字在預定的欄位中整齊地排列起來:
.R ( n u -- ) 在u欄位中顯示數字n,向右對齊 SPACE ( -- ) 顯示一個空格 SPACES ( u -- ) 顯示u個空格
“.”指令是所謂的自由格式列印指令,它將參數堆疊頂端的數值轉換成數字字串,顯示在螢幕上,然後再加一個空格.”.R”指令則參數堆疊頂端的數值規定第二個數值列印的欄位數,而且這第二個數值列印時是在這規定欄位靠右面對齊的。如果我們列印許多行的資料昤都用相同的欄位列印相關的資料,就可以印出棑列整齊的表格。不用”.R”指令列印的文字或數字資料則必須用SPACE及SPACES等指令加入空格把這些資料對齊。
: ONEROW ( nRow -- ) CR DUP 3 .R 3 SPACES 1 8 FOR DUP DUP * 4 .R 1 + NEXT DROP ; : MULTIPLY ( -- ) CR CR 6 SPACES 1 8 FOR DUP 4 .R 1 + NEXT DROP 1 8 FOR DUP ONEROW 1 + NEXT DROP ;
輸入MULTIPLY即可印出一個九九乘法表。
注意以上程式中的FOR-NEXT迴路結構,它們有這樣的形式:
1 8 FOR DUP <指令串> 1 + NEXT DROP
8是給FOR開始迴路用的迴路指數,它使迴路重複10次,而它的數值是由8遞減到0的。但在迴路中我們需要的指數是要由1遞増到9的。因此我們不能直接用迴路指數來工作。一個好的解決方法是在參數堆疊上先推上一個1,在迴路中將它複製一份使用,使用完後再留著的一份加1,所以在迴路中每次重複時就有一個遞増的指數可以用了。在結束迴路之後,留在參數堆疊上的指數則必須用DROP棄去。
這是使用迴路的一個技巧。
練習11 基數和數位轉換
電腦內部都是儲存及處理二進位的數字。這些數字一定要轉換成十進位或十六進位的數字字串才能讓人類閱讀,我們輸入進電腦的數字字串也必須轉換成為二進位數值,才能給電腦處理。數字的輸入和輸出是電腦程式一定要面對的困難問題。weForth提出一個根本的解決方案,即是把數字轉換的基數存在一個變數BASE中。這部電腦就可以簡單地接受數字字串,而用這個基數轉成電腦內部使用的形式。電腦內部使用的數值也是用這倨基數轉換成人能讀的數字字串。BASE中存的基數可以隨時改變以適合使用者的需要。控制這輸入和輸出數字轉換的指令是:
HEX ( -- ) 設定十六進位 DECIMAL ( -- ) 設定十進位 BASE ( -- a ) 輸入輸出轉換基數的地址 DECIMAL和HEX只是將10和16存入BASE: : DECIMAL 10 BASE ! ; : HEX 16 BASE ! ; 我們能再定義新指令指令將BASE改成8或2,就可以用八進位或二進位來輸入數字或列印數字: : OCTAL 8 BASE ! ; : BINARY 2 BASE ! ; 請試用以下的指令來轉換在不同基數時的數字: DECIMAL 12345 HEX U. HEX ABCD DECIMAL U. DECIMAL 100 BINARY U. BINARY 101010101010 DECIMAL U.
程式設計師必須經常處理不同數位的數字。HP公司做過一種掌上計算器可以將十進位數字轉換成十六進位,非常受到程式設計師的歡迎。它一度成為程式設計師炫人的寵物。用weForth你也可以隨意地轉換數字,做秀給你的朋友和同事看。
我最喜歡做的秀是做十九進位的算術。十九是處理圍棋程式最適當的基數,因為圍棋棋盤是19x19格的。用十九進位的數字,我們只要兩位數就可以標定一顆棋子的位置。這是記錄圍棋譜最精簡的方式。
36進位是電腦界有時會使用的進位法。用10個數字碼和26個英文字母可以代表全部36個36進位的數目字。這樣我們可以在一個32位元的數值中塞進6個英數字瑪。這是壓縮英數字碼的最簡單但很有效的方法。
這些有趣的玩意,請你自己去試驗。
練習12 ASCII字碼表
ASCII字碼表和九九乘法表相似,只是我們希望能將ASCII字碼的數值用八進位,十進位,和十六進位並排的列印出來。weForth提供DECIMAL作十進位,和HEX作十六進位的轉換。它們的關鍵是基數BASE。weForth將輸入的數字字串轉換成數值推上參數堆疊時,是用在BASE裡的基數值做轉揍計算的。
”.”,”.R’及”.S”等指令顯示數值時,都是用在BASE裡的基數值做轉換計算的。當BASE中的數值是10時,數字輸入輸出就是用十進位。 BASE中的數值是16時,數字輸入輸出就是用十六進位。在上一個練習中我們又加了八進位,所以做ASCII字碼表的指令已經夠了。程式如下:
: CHARACTER ( n -- ) DUP EMIT HEX DUP 3 .R OCTAL DUP 4 .R DECIMAL 3 .R 2 SPACES ; : LINE ( n -- ) CR 5 FOR DUP CHARACTER 16 + NEXT DROP ; : TABLE ( -- ) 32 15 FOR DUP LINE 1 + NEXT DROP ; 輸入TABLE指令即可印出ASCII字碼表。
練習13 正弦和餘弦
正弦和餘弦是在三角計算中最常遭遇的超越函數,在通常製圖和很多其他不同的應用中都經常出現。他們都需要使用浮點數字來計算,才能有足夠的精密度。但是在我們的32位元電腦中,整數的範圍是從-2147483648到2147483647,這麼大的整數範圍是足夠製圖及大多數的應用來使用的。我們將研究如何用整數做正弦和餘弦的計算。
我們用整數0到360表示一個圓周涵蓋的角度。一個角度的正弦或者餘弦值是在-1.0和+1.0間的。我們用整數10000來代表1.0,因此正弦和餘弦有4位數值,其精確度可以滿足大部份的應用問題。圓周率pi是31416。 360度角度的弧度是62832,180度角度的弧度是31416,90度角度的弧度是15708。我們計算正弦和餘弦時,角度是先縮減-90到+90度,然後轉換到從-15708到+15708的弧度。從這些弧度我們再來計算正弦和餘弦的值。
正弦和餘弦的演算法是保留正弦和餘弦泰勒展開式的前面四項,計算是準確的到10000分之1。
Sin(x) = x + (x**3)/6 + (x**5)/120 + (x**7)/5040 = (1 +(1 + (1 + (1 + (x**2)/42)(x**2)/20)(x**2)/6)x Cos(x) = 1 + (x**2)/2 + (x**4)/24 + (x**6)/720 = 1 + (1 + (1 + (1 + (x**2)/30)(x**2)/12)(x**2)/2 VARIABLE XS ( 暫存x*x/10000 ) : KN ( n1 n2 -- n3, n3=10000-n1*x*x/n2 x是弧度 ) XS @ SWAP / ( x*x/n2 ) NEGATE 10000 */ ( -n1*x*x/n2 ) 10000 + ( 10000-n1*x*x/n2 ) ; : (SIN) ( x -- sine*10000, x是弧度*10000 ) DUP DUP 10000 */ ( x*x/10000 ) S ! ( 存入XS ) 10000 72 KN ( 泰勒系列最後一項 ) 42 KN 20 KN 6 KN ( 泰勒3,2,1項 ) 10000 */ ( 乘x ) ; : (COS) ( x -- cosine*10000, x是弧度*10000 ) DUP 10000 */ XS ! ( 計算 x*x/10000 ) 10000 56 KN 30 KN 12 KN 2 KN ( 泰勒展開 ) ; : SIN ( 度 -- sine*10000 ) 31415 180 */ ( 換算成弧度 ) (SIN) ( 計算正弦 ) ; : COS ( 度-- cosine*10000 ) 31415 180 */ (COS) ; 測試以上的程式,請輸入: 90 SIN . 9999 45 SIN . 7070 30 SIN . 5000 0 SIN . 0 90 COS . 0 45 COS . 7071 0 COS . 10000
練習14 開平方
開平方有很多算法。這兒我們討論對一個32位元的正整數開平方的演算法。
我們把一個32位元的正整數存在變數SQUARE中,它的平方根在變數ROOT中。開完平方後的餘數則在變數REMAINDER中。開始時ROOT及REMAINDER都是0。
每一步的計算中,SQUARE和REMAINDER都向左移動二位元,SQUARE中最左邊的二位元則移進REMAINDER最右邊的二位元。SQUARE則向左移動一位元。現在我們該從REMAINDER中減去SQUARE乘2加1,如果夠減(減的結果是正數),就把減的結果存入REMAINDER,並將ROOT加1。如果不夠減(減的結果是負數), REMAINDER和ROOT就都不變。
這個步驟重複16次,ROOT中就是平方根,而REMAINDER中則是開完平方後剩下的餘數。SQUARE裡則是0。 這個演算法的程式如下:
VARIABLE ROOT VARIABLE REMAINDER VARIABLE SQUARE : SHIFT ( --, 將SQUARE最高的2位元移入REMAINDER ) REMAINDER @ 2* ( REMAINDER左移1位元 ) SQUARE @ DUP >R ( 測試SQUARE最高位元 ) 0< IF 1+ THEN 2* ( SHIFT REMAINDER AGAIN ) R> 2* DUP 2* SQUARE ! ( 測試SQUARE最高第2位元 ) 0< IF 1+ THEN REMAINDER ! ; : SQRT ( SQUARE -- ROOT ) SQUARE ! 0 REMAINDER ! 0 ROOT ! 15 FOR SHIFT REMAINDER @ ROOT @ 2* 2* 1+ - DUP 0< IF DROP ROOT @ 2* ROOT ! ELSE ROOT @ 2* 1+ ROOT ! REMAINDER ! THEN NEXT ROOT @ ; 試試下面的指令: 100 SQRT . 10000 SQRT . 1000000 SQRT . 100000000 SQRT . 123456789 SQRT . 10000000000 SQRT . 最後一個平方根好像是不大對勁,你是否能解釋它們的結果?
練習15 亂數值
亂數值經常用在電腦程式中,特別是電腦遊戲和模擬自然現象的程式。這個亂數值的程度是Leo Brodie的 'Starting Forth'一書1中記載的。
VARIABLE RND ( 亂數種子 ) HERE RND ! ( 亂數種子初值 ) : RANDOM ( -- n, 亂數值 0-65536 ) RND @ 31421 * ( RND*31421 ) 6927 + ( RND*31421+6926 ) DUP RND ! ( 重設亂數種子 ) ; : CHOOSE ( n1 -- n2, a 亂數值由0到n1 ) RANDOM UM* ( n1*random 雙整數乘積 ) SWAP DROP ( 棄除低位值, 即是乘65536 ) ; 測試程式請輸入: 100 CHOOSE . 100 CHOOSE . 100 CHOOSE . 看看所得的結果是否是在 0和99 之間。
練習17 猜數字
FORTH是一個操作的系統,此外是一個程式語言。這作業系統有它的語法,接受指令和執行指令。一個應用程式常需要它們自己的作業系統,以它們自己的語法和使用者溝通。這練習顯示我們如何建造如此的操作系統,雖然它是一個很簡單的系統。
電腦首先請這使用者輸入一個數字,以這數字來限制到猜數字的範圍。它然後請使用者輸入他猜的數字。它會告訴使用者假如這數字是太大或太小,直到使用者輸入正確的數值。然後再重複這整個過程。
一個遊戲操作的系統,需要更多FORTH資源從。這裡是我們需要的新指令:
BEGIN ( -- ) 開始一個迴路 BL ( -- 32) 將32推上參數堆疊 NUMBER? ( a - n -1 | a 0 ) 將在地址a的數字字串轉換成數字n及真值-1 QUERY ( -- ) 等使用者從鍵盤上輸入一串指令 WORD <text> ( c -- a ) 在輸入文字串中分出以字碼c結束的字串並存入a 2DROP ( w w -- ) 彈出並丟棄參數堆疊頂端二數值 2DUP ( w1 w2 - w1 w2 w1 w2) 複製參數堆疊頂端二數值最要緊的指令是請使用者輸入一個數字,並將這個數字放在參數堆疊上。這個新的指令GetNumber如下:
: GetNumber ( -- n ) BEGIN CR ." Enter a Number: " ( 顯示問題 ) QUERY BL WORD NUMBER? ( 輸入數目字 ) UNTIL ( 重複,直到得到對的數字 ) ; 有了這個指令,我們就能建造這'猜數字'遊戲。 : InitialNumber ( -- n , 先得到數值的範圍 ) CR CR CR ." What limit do you want?" GetNumber ( 請使用者輸入數值 ) CR ." I have a number between 0 and " DUP . CR ." Now you try to guess what it is." CR CHOOSE ( 得到一個亂數值 ) ; : Check ( n1 -- , 請使用者輸入他猜的數字,數字正確時結束 ) BEGIN CR ." Please enter your guess." GetNumber 2DUP = ( 相等? ) IF 2DROP ( 數值正確,結束程式 ) CR ." Correct!!!" EXIT THEN OVER < IF CR ." Too low." ELSE CR ." Too high!" THEN CR 0 UNTIL ( 數值不正確,重新來過 ) ; : Greet ( -- ) CR CR CR ." Guess A Number" CR ." This is a number guessing game. I'll think" CR ." of a number between 0 and any limit you want." CR ." (It should be smaller than 32000.)" CR ." Then you have to guess what it is." ; : GUESS ( -- , the game ) Greet BEGIN InitialNumber ( 取得範圍值 ) Check ( 請使用者猜數字 ) CR CR ." Do you want to play again? (Y/N) " KEY ( 檢查按鍵 ) 32 OR 110 = ( 若是N或 n 即結束 ) UNTIL CR CR ." Thank you. Have a good day." ( 再見 ) CR ; 輸入'GUESS'開始和這部電腦將玩這個遊戲。浬意程式中的這個迴路結構: BEGIN <repeat-clause> ( ? ) UNTIL EXIT能跳出這無限的迴路,它跳過全部指令直到';'結束這指令。
練習17 印月曆
這個練習允許你印刷任何一年中的月曆。它假設每4年有1461天,每年有365天加一天閏日。第0日是1950年1月1日。
這裡使用的新指令有: */MOD ( n1 n2 n3 - 商 餘) 比例 (n1*n2)/n3的商和餘數 +! ( n a -- ) 將n加進在地址a中的數值 EMIT ( c -- ) 顯示字碼c VARIABLE JULIAN ( 凱撒日曆, 1950年1月1日開始 ) VARIABLE LEAP ( 閏年是1,非閏年是0 ) : YEAR ( YEAR --, compute Julian date and leap year ) DUP 1949 - 1461 4 */MOD ( 1/1/1950後的日數 ) 365 - JULIAN ! ( 1/1/1950是第0日 ) 3 = ( 餘數是3就是閏年 ) IF 1 ELSE 0 THEN ( 設定 LEAP ) LEAP ! DUP 2000 = ( 2000年不閏 ) IF 0 LEAP ! THEN 2001 < IF ELSE -1 JULIAN +! THEN ; : FIRST ( MONTH -- 1ST, 1st of a month from Jan. 1 ) DUP 1 = IF DROP 0 EXIT THEN ( 1月1日是0 ) DUP 2 = IF DROP 31 EXIT THEN ( 2月1日是31 ) DUP 3 = IF DROP 59 LEAP @ + EXIT THEN ( 3月1日是59或60 ) 4 - 30624 1000 */ 90 + LEAP @ + ( 4月1日到12月1日 ) ; : STARS 60 FOR 42 EMIT NEXT ; ( 印邊界 ) : HEADER ( -- ) ( 印月曆邊框 ) CR STARS CR ." SUN MON TUE WED THU FRI SAT" CR STARS CR ( 印週日 ) ; : BLANKS ( MONTH -- ) ( 第一日前面留空 ) FIRST JULIAN @ + ( 每月第一日的凱撒日 ) 7 MOD 8 * SPACES ; ( 第一日前面留空 ) : DAYS ( MONTH -- ) ( 印一月中的日子 ) DUP FIRST ( 每月第一日的凱撒日 ) SWAP 1 + FIRST ( 下月第一日的凱撒日) OVER - 1 - ( 印日子的迴路) 1 SWAP ( day count -- ) FOR 2DUP + 1 - JULIAN @ + 7 MOD ( 週日數值 ) IF ELSE CR THEN ( 星期日要另印一行 ) DUP 8 U.R ( 印日數 ) 1 + NEXT 2DROP ; ( 棄除日數 ) : MONTH ( N -- ) ( 印月曆 ) HEADER DUP BLANKS ( 印月曆邊框 ) DAYS CR STARS CR ; ( 印日數 ) : JANUARY YEAR 1 MONTH ; : FEBRUARY YEAR 2 MONTH ; : MARCH YEAR 3 MONTH ; : APRIL YEAR 4 MONTH ; : MAY YEAR 5 MONTH ; : JUNE YEAR 6 MONTH ; : JULY YEAR 7 MONTH ; : AUGUST YEAR 8 MONTH ; : SEPTEMBER YEAR 9 MONTH ; : OCTOBER YEAR 10 MONTH ; : NOVEMBER YEAR 11 MONTH ; : DECEMBER YEAR 12 MONTH ; 印刷2003年四月的日曆,輸入: 2003 APRIL
道和名
老子在道德經上,開宗明義第一章就說:
道可道也,非恆道也。名可名也,非恆名也。
無名萬物之始也,有名萬物之母也。
故恆無欲也,以觀其眇。恆有欲也,以觀其所噭。
兩者同出,異名同胃。玄之又玄,眾眇之門。
(道德經,馬王堆帛書古本原文)。
即是這一段道德經最中心的文字,歷代學者都有不同的解釋。我們從FORTH電腦語言的角度來看,這段經文有特別的含義。
就我看來,道,無名,無欲和眇,都是同一回事。而名,有名,有欲,和噭(蹤跡) ,也都是一回事。在電腦裡,道是零和一組合成的機器碼,有它特定的功能,但是不容易瞭解,很難解開其中的祕密。
要明白電腦程式,最重要的資料是原始的程式。如果沒有原始程式,最關鍵的資料就是名稱表Symbol Table了。名稱表中例出了所有程式片斷和參數的名稱和地址,由這些名稱,有經驗的程式師可以很快地破解複雜的程式。
一般的電腦系統在將原程式編譯完成之後,即將名稱表棄去,因為在執行機器碼時,名稱已經沒有什麼用處了。在符式電腦系統裡,指令和參數的名稱則是保留在程式的機器碼中的。名稱保留在機器碼中,使得系統可以支持功能強大的除錯器Debugger,解碼器Interpreter,編碼器Compiler和組合器Assembler。這些工具使得FORTH系統形成完整的程式開發環境,對軟體的寫作和驗証,有出人意表的便利。
這是為什麼老子把名和道一同放在最高的層次,讓兩者等量齊觀的原故。道是實質,名是表象。沒有道的實質,就無所謂名的表象。但是沒有名的表象,我們就無從瞭解道的實質。
恆道無名,是天地始創時的實質。等到人類出現,要研究探索世界實質的奧祕時,就必須先將事物定名。定名的一個先決條件是有了語言和文字。事物有了名稱,才能構築抽象的觀念,才能作高一層次的思考和推理,才能發展出文化,藝術,工程和科學。所以老子才大膽的宣告:有名萬物之母!
在FORTH系統裡,所有的功能都是表現在它的指令集上。指令集中每個指令都有它獨特的功能和獨有的名稱。使用者輸入一個指令的名稱,電腦就執行這個指令規定的工作。使用者還能將一系列現有的指令,定義成為一個新指令,有一個新的名稱。所以FORTH的指令集是可以延伸擴展的,可以很方便地解決任何應用的問題。道和名的整合,有無窮的變化,也有數不盡的應用。
難怪老子喜之不盡地讚嘆道:
兩者同出,異名同謂。玄之又玄,眾眇之門!