第壹篇 入門篇

第零課,FORTH 導論

F-PC 是 FORTH 系統開放給 MS-DOS 的領域,相容於個人電腦,是由 Tom Zimmer 和 Dr. Robert L.Smith 和許多 Forth 程式設計師設計的,它是配合 MS-DOS 操作系統的程式環境,利用操作系統的助益和基本的 80X86 機器,它有強力的編輯器和 8086 編譯器,也有一個便利的超大文字系統來幫助使用者駕馭及探險 Forth 系統。

此指導是針對第一次使用 Forth 者學習其基本指令集,可以在短時間內加以運用,此指導只討論一百五十組指令。然而,這個小指令集,讓使用者對解決許多實際問題所需複雜演算法得以表達,因此也可為更進一步學習 Forth 語言和完整的 F-PC 系統,紮下穩固的根基。

此指導包含六課有許多的例子及練習,可以被 F-PC 編譯,使用者可以立即做例子以下是如何開始 F-PC 及編譯課文,閱讀說明,測試例題及做練習 Forth 指令。

一、安裝 F-PC

本文

如果有全部的 F-PC 磁片,你必須執行系統磁片裡的 INSTALL 程式來將它安裝入你的 PC 。將 Disk 放入 A 槽鍵入:

A>INSTALL

安裝程式會指引你,並且建立一個 F.EXE 執行檔,你需要大約 4MB 空間的硬碟來安裝全部的 F-PC 系統,在 C 碟會產生一個 F-PC 子目錄,你可以執行它,鍵入:

C:\FPC>F

幾秒後螢幕會出現 F-PC 的畫面,則 FORTH 可以使用了

如果你沒有全部的 F-PC ,你可以使用指導裡的 F.EXE 檔來研究課文,事實上你不需用硬碟,裝配於 360K 磁片裡的課文和 F.EXE 檔,只要一個槽就能使用,把指導磁片放入 A 槽打入:

A>F

二、學習課文

把課文磁片放入 A 槽且 Forth 已載入,鍵入:

A:

確定 A 槽是預設的磁碟機,你可以打開第一課讀說明,鍵入:

open lesson1 b

"b"指令是尋求 F-PC 編輯器的幫助,並且在編輯視窗讀 lesson1.seq 檔,可以利用游標和 PqUp、PqDn 來讀,按 F10 跳出,回到 Forth。

編譯和測試第一課的例子,鍵入:ok

"OK"是編譯目前開啟的檔,你可以鍵入第一課所定義的指令,來證明和課文中討論的是做相同的工作。

第一課之後,以相同的動作讀說明

open lesson2 b

你可以編譯第二課,鍵入:

open lesson2 ok

全部六課皆以相同之方式

三、練習

如果問題簡單,你可以鍵入新指令的定義,馬上測試,但是最好將新指令建檔,你可以修改及重覆測試。

要做練習,你必需開啟新檔,鍵入你的新指令,鍵入:

newfile exercise

"newfile"建立一個名叫 exercise.seq 的新檔,並尋求編輯器的幫助,可以開始輸入新指令的定義,使用例子做為新指令的模擬,當你完成指令碼輸入,按 F10 跳出編輯,回到 Forth 鍵入:

ok

編譯你的新指令,然後加以測試。

如果 Forth 在編譯你的指令碼有問題時,它會尋求編輯器的幫助,且游標會停在無法辨認之指令處,你可以做正確的修改,離開編輯,鍵入:

ok

再編譯指令碼

如果要直接編輯 exercise.seq 檔,鍵入:

open exercise e

" e " 和 " b "類似,但 " e " 可以在檔案裡編輯文章 " b " 只能瀏覽,不能修改你可以不須經過 " openOK " 之過程來編譯任何檔,只要鍵入以下之指令

include lesson1 include lesson2 include exercise

以此類推,如果在 exercise.seq 檔語法錯誤 " include exercise "將尋求編輯器的幫助,且游標會指出該指令讓你編輯。

編輯器是非常直覺的,它接受 Werdstar 的控制指令,如果你按 ESC鍵,可以在螢幕頂端看到主功能表,可以用游標來選出一個求助項,並且拉下一個詳細編輯器功能之求助功能表。

此編輯器允許你在新指令裡改正語法錯誤,當你成功的編譯新指令後,仍需不斷測試,確定它們正確的做該做的,你可能做錯當機,但使用課文中的指令,對電腦或磁碟中的資料不會有嚴重的傷害,最糟的只是重新開機再次執行 F.EXE

幫助你將定義的指令除錯,F-PC 提供一個有力的除錯器,有單步除錯的能力,要測試和除錯你定義的指令,如要對 mycommand 除錯則鍵入:

dbg mycommand

會出現兩層螢幕,上半部指出在 mycommand 定義下的指令,下面視窗展示出資料堆疊的內容和下一個執行的指令,按指令會一個接一個測試,藉著檢視資料堆疊的改變,你可以隨著執行的順序來確認錯誤的原因,用編輯器改正它的編譯指令碼並再試一次。

程式是編輯、編譯及測試的不斷循環直到每一樣都做對,它經常使我們情緒很壞,但如果做對了,會很滿足。

四、程式格式和程式助手

課文是以一種你要跟隨寫自己的指令碼的形式寫的,大量有關指令碼的說明文字是用來解釋和註解此指令碼,如果你花時間去把指令碼加以解釋或做成文件,那麼等你寫好指令碼即使是十年之後來閱讀也毫無困難,而且別人來維護也很容易,以下是幾個註解指令碼的方式

(comments)            \ ( 表示開使一個註解直到右括號為止 \comments             \ 表示開使一個註解直到最後一列為止  comment:              \ 多列註解直到 comment; 為止    comment 1          \ 註解 1    comment 2          \ 註解 2      .....            \ ...    comment n          \ 註解 n comment;              \ 結束註解 EXIT                  \ 停止編譯,以後所有行列皆視為註解

選擇好的指令名稱,使程式易讀,以下是好的 Forth 程式命名的原則

第一課,印出字串

此課用最簡單的例子來說明 Forth 的原則;從現有的指令集裡建立新指令我們使用簡單的 Forth 指令" XXXX "來顯示螢幕上的字,並使用它來建立一指令集允許我們在螢幕上建立任何一組字,為了說明 " ." " 指令的用法,讓我們來寫第一個 Forth 程式。

金城 註疏:

." 是一個 FORTH 的詞(word)也就是其他語言所謂的指令,所以其前後皆要有空白,才能被分辨(解譯)執行。

例題一、普通的問候

: hello ." hello, world!" ;

現在,如果你打入"hello"然後按," hello, world! "會出現在" hello " 之後

:                     \ 開始新指令 hello                 \ 新指令之名稱 ."                    \ 印出字串,不包括下一個 " ;                     \ 結束新指令

"hello"是新指令其功能是在螢幕上印出"hello, world!"

例題二、大F

現在我們要做的是使用簡單技巧來顯示大型的英文字元在螢幕上,以 F 為例

: bar   cr  ." ***** "  ; : post  cr  ." *     "  ; : F     bar post bar post post post  ;

打入" F "按 Enter,你可以看見螢幕上之 F 被顯示了

***** * ***** * * *

F 有兩個成分,一個由 5 個 * 組成的 bar 及一個可由一個或多個 * 代替的 Post,我們定義兩個新指令" bar" 及" Post "分別顯示 5 個 * 和一個 * 最後的指令 F 定義為顯示一個 bar、一個 Post、一個 bar,三個 Post。

指令" cr "開始新的一行,使得後來的字由螢幕最左邊顯示

金城 註疏:

此例題給了一個最重要的 FORTH 概念,將一個複雜的問題先"分析簡化",再將其製造成零件(一個 FORTH 的詞),最後再予組合。

練習一

使用新指令 bar 及 Post 定義新指令" C "、" E "、" L "在螢幕上顯示對應的大型字元。

練習二

分析你的姓,一組像 bar 及 post 的指令,使用它們來組合你有姓裡所有的字母,我以" TING "為例

金城 註疏:

FORTH 的精神,就是簡化問題。因此,牢記 FORTH 的第一條無上心法 "如果你不能將問題分析簡化,那代表你並未真正透徹的了解問題,所以請回頭去探索、分析問題的本身。

例題三、我的名字

 : center cr  ."   *  "  ;   : sides  cr  ." *   *"  ;   : triad1 cr  ." * * *"  ;   : triad2 cr  ." **  *"  ;   : triad3 cr  ." *  **"  ;   : triad4 cr  ."  *** "  ;   : quart  cr  ." ** **"  ;   : right  cr  ." * ***"  ;   : bigT  bar center center center center center center ;   : bigI  center center center center center center center ;   : bigN  sides triad2 triad2 triad1 triad3 triad2 sides ;   : bigG  triad4 sides post right triad1 sides triad4 ;   : TING  bigT bigI bigN bigG ;

練習三

以此方式來組合英文字母是容易的,問題是在 5*7 的格式裡組合 26 個字母需多少基本指令? 關於標點符號呢?

練習四

原則上,我們可以類似的技術來組合中文字,但需 16*16 之格式編譯更多中文則需 24*24 格式,試著用 5*7 格式組合簡單的中文,例如日、月、土

金城 註疏:

此題是說明了中文系統中組字法的觀念,仍是分析簡化。

例題四、在螢幕上顯示大的字元

試著在螢幕的任何地方寫出組合字,螢幕可以 25*80 格式顯示字元,即25 列、每列 80 字,下列指令允許我們在寫字元前移動游標的位置。

 dark               \  清除螢幕游標放在左上角   at                 \  移動標至螢幕指定位置                      \  40 12 at 把游標放在螢幕中央

金城 註疏:

x y at 指令的前者"x"是橫座標,後者"y"是縱座標。FORTH 將螢幕的左上方視為(0,0)

以下指令把一大 F 放到螢幕中央

: newBar        ." *****" ; : newPost       ." *    " ; : new-F   dark   38 10 at newBar   38 11 at newPost   38 12 at newBar   38 13 at newPost   38 14 at newPost   38 15 at newPost   cr ;

顯然地,這是放置文字的笨拙方法,如果必須在指令裡指定文字明確的位置,一個放置文字較普遍的方法是使用變數(Variables)來貯存位置,以致於一個字母可以放在螢幕上的任何地點。

金城 註疏:

variable x variable y : newLine   x @ y @ at    \  移動游標到(X,Y) 位置   1 y +!        \   增加 Y 至下一列 ; : F        newLine newBar   newLine newPost   newLine newBar   newLine newPost   newLine newPost   newLine nwePost ;

把 F 放在螢幕上必須先指定其位置:

30 x ! 10 y ! F

這裡所學的是幾個 FORTH 指令

 variable             \  定義一個變數(Variable),數字可以                              \  貯存及取出   @      ( var -- data )     \  取出存在變數(Variable)中的數字   !      ( data var -- )     \  存一個數字到變數(Variable)中   +!     ( data var -- )     \  把存在變數(Variable)中之數字增加一

金城 註疏:

: F-demo   dark   0 x ! 0 y ! F   70 x ! 10 y ! F   10 x ! 18 y ! F   40 x ! 15 y ! F ;

練習五

定義一個新指令來清除螢幕,並把 Forth 此訊息以組合字母放到螢幕中央。

: bar           newLine ." *****" ; : post          newline ." *    " ; : triad1        newline ."  *** " ; : sides         newline ." *   *" ; : tetra         newline ." **** " ; : duol          newline ." **   " ; : duo2          newline ." * *  " ; : duo3          newline ." *  * " ; : center        newline ."   *  " ; : F     bar post bar post post post ; : O     triad1 sides sides sides sides trial1 ; : R     tetra sides tetra duo2 duo3 sides ; : T     bar center center center center center ; : H     sides sides bar sides sides sides ;   : FORTH dark   25 x ! 10 y ! F   32 x ! 10 y ! O   39 x ! 10 y ! R   46 x ! 10 y ! T   53 x ! 10 y ! H ;

練習六

自己設計一個訊息,將其顯示於螢幕中央。

金城 註疏:

例題五、重覆的圖樣

電腦非常擅於重覆處理事件,很多 FORTH 指令被設計使程式容易處理重覆的工作

DO   ( limit index -- )   \ 設定 LOOP 給極限和指數 LOOP ( -- )                \ 從 1 增加指數並比較極限 如果 Index=limit 跳出 LOOP,否則重覆 DO 之後的指令 I    ( --index )          \ 傳回目前 LOOP 指數

要定義 loop 必須在冒號裡定義, I 只能用於 DO 和 LOOP 之間,當DO....LOOP 語法被執行,Loop 增加指數並與 Limit 比較,如果 index等於或大於 limit ,結束 Loop ,如果增加的指數仍小於 limit,在 Loop裡之指令會重覆執行

以一些例子來測試 DO....LOOP 指令

VARIABLE width      \ 印出星號的字 : Asterisks         \  在螢幕上印出 n 個星 n = width   width @ 0         \  開始指數=0 ,limit=寬   DO ." *"          \ 一次印一個星號   LOOP             \ 重覆 n 次 ;  : Rectangle ( height width -- , 印出星號的長方形 )   width !           \ 印出最初之寬   0                 \ limit=高,index=0   DO  CR     Asterisks       \  印出一列星號   LOOP ;   : Parallelogram ( height width -- )   width !   0   DO  CR I SPACES   \ 移動行列至右邊       Asterisks     \ 印出一列星號   LOOP ;   : Triangle ( width -- ,  印出一個星字三角形 )   1                 \ limit=width, initial index=1   DO  CR I width !       \ 增加每一列的寬     Asterisks       \ 印出一列星號   LOOP ;

測試上述之指令

3 10 Rectangle 5 18 Parallelogram 12 Triangle

練習六

想出一些有趣的圖形,可在螢幕上顯示的,並寫程式來產生此圖形例如。

   *    * *   * * *  * * * *   * * *    * *     *

例題六、畫框框

IBM-PC 有一組給螢幕顯示的圖形字元,它們可以在螢幕上畫出簡單圖形,來加強你的文字訊息,當在 F-PC 編輯器,按 ESC A 和 Enter ,就可以看見這些字元,在此例中,我們要畫出一個環繞一篇文章的框框,所需的指令如下:

218 EMIT       \ 印出左上角 ┌ 191 EMIT       \ 印出右上角   ┐ 192 EMIT       \ 印出左下角 └ 217 EMIT       \ 印出右下角  ┘ 176 EMIT       \ 印出垂直線  │ 196 EMIT       \  印出水平線  ─

讓我們看看如何在螢幕上畫出一個框框,我們假定框框左上角是(X,Y),右下角是(X1,Y1)

VARIABLE x1 VARIABLE y1   : TopSide ( -- )   x @ y @ AT            \ 游標移至左上方   x1 @ x @              \ 水平極限(終點及起點)   DO      196 EMIT      \ 印出上方之邊線   LOOP ;  : BottomSide ( -- )   x @ y1 @ AT           \ 游標移至左下方   x1 @ x @              \ 水平極限   DO      196 EMIT      \ 印出下方之邊線   Loop ;  : LeftSide ( -- )   y1 @ y @              \ 垂直極限   DO      x @ I AT      \ 游標位置           179 EMIT      \ 畫出左邊線   LOOP ;  : RightSide ( -- )   y1 @ y @              \  垂直極限   DO       x1 @ I AT    \  游標位置            179 EMIT     \  畫出右邊線   LOOP ;  : Corners ( -- )   x @ y @ AT   218 EMIT               \ 左上角 ┌   x @ y1 @ AT   192 EMIT               \ 左下角 └   x1 @ y @ AT   191 EMIT               \ 右上角  ┐   x1 @ y1 @ AT   217 EMIT               \ 右下角  ┘ ;  : Box ( x y x1 y1 -- )   y1 ! x1 !              \  貯存右下角   y ! x !                \  貯存左上角   LeftSide RightSide     \  畫兩邊   TopSide BottomSide     \  畫頂和底   Corners ;  : demo FORTH             \  印出大 Forth   20 8 62 17 box         \  在它四周畫出框框   0 21 at               \  移動游標出來 ;

打入 DEMO 你可以看到 Forth 被圍在框裡

金城 註疏:

FORTH 的程式設計方式,一如其它語言的結構化模組,但 FORTH 的架構鼓勵使用者,寫許多個短式子,來組合成應用的全部程式,短的式子有許多好處,第一個是能專心思考。第二個是較容易除錯。第三個是可重覆使用。第四個是為逐步改善。最後一個是立即測試。

第二課,二個文字遊戲

Forth 程式的本質是從現存的指令和模組,建立一個更大更有力的模組,從小組指令開始,可以建立內容充實的程式處理有趣的事。

我們學過了 Forth 的小指令,包括 " ." " 在螢幕上顯示字串,我們將使用此指令來設計兩個遊戲之程式,來玩弄文字串

例題一

" The Theory that Jack built " 這篇文章是先定義一些片語,然後用它來建立句子和章節,此例子教我們如何在 Forth 裡以小的模組建立大的程式

首先定義一些簡單片語

: the               ." the " ; : that              cr ." That " ; : this              cr ." This is " the ; : jack              ." Jack Builds" ; : summary           ." Summary" ; : flaw              ." Flaw" ; : mummery           ." Mummery" ; : k                 ." Constant K" ; : haze              ." Krudite Verbal Haze" ; : phrase            ." Turn of a Plausible Phrase" ; : bluff             ." Chaotic Confusion and Bluff" ; : stuff             ." Cybernatics and Stuff" ; : theory            ." Theory " jack ; : button            ." Button to Start the Machine" ; : child             ." Space Child With Brow Serene" ; : cybernatics       ." Cybernatics and Stuff" ;

現在建立複雜的合成的片語

: hiding          cr ." Hiding " the flaw ; : lay             that ." Lay in " the theory ; : based           cr ." Based on " the mummery ; : saved           that ." Saved " the summary ; : cloak           cr ." Cloaking " k ; : thick           IF that ELSE cr ." And " THEN                   ." Thickened " the haze; : hung            that ." Hung on " the phrase ; : cover           IF that ." Covered "                   ELSE cr ." To Cover "                   THEN bluff ; : make            cr ." To Make with " the cybernatics ; : pushed          cr ." Who Pushed " button ; : without         cr ." Without Confusion, Wxposing the Bluff" ;  : rest                          \ 在使行動中止   ." . "                        \ 印出一節   10 SPACES                     \ 空10個間隔   KEY                           \ 等使用者按一鍵   DROP CR CR CR  ;                     : recite                        \ 朗讀詩篇   dark cr this theory rest   this flaw lay rest   this mummery hiding lay rest   this summary based hiding lay rest   this k saved based hiding lay rest   this haze cloak saved based hiding lay rest   this bluff hung 1 thick cloak           saved based hiding lay rest   this stuff 1 cover hung 0 thick cloak           saved based hiding lay rest   this button make 0 cover hung 0 thick cloak           saved based hiding lay rest   this child pushed           cr ." That Made with " cybernatics without hung           cr ." And, Shredding " the haze cloak           cr ." Wrecked " the summary based hiding           cr ." And Demolished " theory rest ;

金城 註疏:

FORTH 程式的整體架構類似樹狀結構,由葉子與樹枝構成樹的支幹,再由支幹構成主幹。而執行時也類似樹木的養份輸送,由主幹將水份送往支幹,支幹再分送樹枝和葉子,到達小葉子上行光合作用(真正的有效運作)。以資訊專業的眼光來看就是其他語言的函數呼叫與參數傳遞。

KEY        ( --char )       \  等候按鍵,並傳回該按鍵的 ASCII 碼 DROP       ( n-- )          \ 丟棄在堆疊(stack)頂端的數字 SPACE      ( -- )           \ 顯示一個空白 SPACES     ( n-- )          \ 顯示 n 個空白 IF         ( f-- )          \ 如果旗標(flag) 是0,跳至 ELSE or                             \  THEN 如果旗標(flag) 不是0,執行                             \ 底下的指令,                             \  至 ELSE 然後跳至 THEN ELSE       ( -- )           \  跳過以下的指令到 THEN THEN       ( -- )           \  結束 IF-ELSE-THEN                             \  或 IF-THEN

金城 註疏:

KEY、DROP 稍後討論,目前其作用是讓電腦暫停,給你時間去讀詩章,當按任意鍵,電腦會繼續執行下一個指令" rest "會在每一章結束時執行。

IF---ELSE---THEN 允許電腦依其執行狀況選擇交替的指令,這些指令以群體方式使用。

(flag) IF  ELSE  THEN (flag) IF  THEN

False Clause 是任意的,舉例:" 1cover " 是相等於 that ." Covered " bluff

而" 0cover "相等於cr ." To Cover " bluff

這個條件的分支結構,對電腦選擇一或二個不同路徑來執行是很方便的。現在我們可以建一個大而更複雜的指令,一章一章的印出章節執行。

RECITE

以文章順序印出全部東西

練習一

以上述技巧輸入你喜歡的詩篇,給每一篇獨特的名字,作為新的 Forth 指令,當你打入一首詩的名字,所有的詩會以動人的形式在螢幕上被顯示。

例題二

這是一個對話遊戲,電腦將對不同問題給予忠告,比如性、健康、錢、工作,是從 " Basic Computer Games "改編而來。

: Advisor   CR ." Hello!  My name is Creating Computer."   CR ." Hi there!"   CR ." Are you enjoying yourself here?"   KEY 32 OR 89 =   IF      CR ." I am glad to hear that."   ELSE    CR ." I am sorry about that."           CR ." maybe we can brighten your visit a bit."   THEN   CR ." Say!"   CR ." I can solved all kinds of problems except those dealing"   CR ." with Greece.  What kind of problems do you have"   CR ." ( sex, health, money or job )?"   CR ;   : question   CR ." Any more problems you want to solve?"   CR ." What kind ( sex, job, money, health ) ?"   CR ;   : sex   CR ." Is your problem TOO MUCH or TOO LITTLE?"   CR ;   : much  CR   ." You call that a problem?!!  I SHOULD have that problem."      CR   ." If it really bothers you, take a cold shower."    question ;   : little   CR ." Why are you here!"   CR ." You should be in Tokyo or New York of Amsterdam or"   CR ." some place with some action."   question ;   : health   CR ." My advise to you is:"   CR ."      1. Take two tablets of aspirin."   CR ."      2. Drink plenty of fluids."   CR ."      3. Go to bed (along) ."   question ;   : job   CR ." I can sympathize with you."   CR ." I have to work very long every day with no pay."   CR ." My advise to you, is to open a rental computer store."    question ;   : money   CR ." Sorry!  I am broke too."   CR ." Why don't you sell encyclopedias of marry"   CR ." someone rich or stop eating, so you won't "   CR ." need so much money?"   question ;

開始對話,鍵入:ADVISOR

此對話利用 Forth 整合的交談環境的天性,使用者的回答被定義為 Forth 的指令,當打入指令時,似乎在回答電腦的問題,這些指令印出給使用者的正確忠告。

金城 註疏:

此範例希望能讓使用者體會到,原來其他語言要寫一長串的指令來判斷使用者輸入的字串,再層層過濾找出適當的動作,而 FORTH 卻只用了幾段話就定義了整個問答的過程,和答覆的內容,非常的自然、流暢。

練習二

使用例子二的技巧定義更多忠告(婚姻、孩子、法律關係......)。

練習三

建立一個中國幸運餅工廠,定義許多的幸運餅當 Forth 指令,把名單給使用者,他可以鍵入名字,獲得幸運消息,稍後我們學亂數字的產生器,使用亂數來選擇幸運餅。

第三課、數字

這一課要討論 Forth 處理整數的方法,16位元整數是從 -32768 到 32767 的數,在 Forth 裡很方便貯存和執行, Forth 也可以處理很大的雙倍精密度數,甚至浮點數,但已超出於本課之範圍。

金城 註疏:

FORTH的數字使用方式,喜歡整數的運算,因為 FORTH 的傳統觀念中,在沒有硬體浮點運算能力的 CPU 上,使用整數來運算,以求得速度與精密度的最佳效果。

例題一

數字在 Forth 裡如何使用的第一個例子是換錢程式,不同貨幣之兌換:

24.55 NT                    1 Dollar 7.73 HK                     1 Dollar 5.47 RMB                    1 Dollar 1 Ounce Gold                356 Dollars 1 Ounce Silver              4.01 Dollars

以美元為標準貨幣,把所有貨幣兌換成美元,所有的數學操作,會在美元裡執行,錢也可以從美元兌換成其他貨幣

我們以貨幣名稱為定義字將其換為美元,而兌換美元為一種貨幣,執行字是" $ "記號

: NT    ( nNT -- $ )    10 245 */  ; : $NT   ( $ -- nNT )    245 10 */  ; : RMB   ( nRMB -- $ )   100 547 */  ; : $jmp  ( $ -- nJmp )   547 100 */  ; : HK    ( nHK -- $ )    100 773 */  ; : $HK   ( $ -- $ )      773 100 */  ; : gold  ( nounce -- $ ) 356 *  ; : $gold ( $ -- nounce ) 356 /  ; : silver ( nounce -- $ ) 401 100 */  ; : $silver ( $ -- nounce ) 100 401 */  ; : ounce ( n -- n, a word to improve syntax )    ; : dollars ( n -- )    . ;

以此組換錢的字,可以做一些測試

5 ounce gold . 10 ounce silver . 100 $NT . 20 $RMB .

金城 註疏:

一個寫 FORTH 程式的重要概念為先想最後要怎麼用,再由使用的方式中分析此動詞(指令)所需完成的工作內容,然後經過整理和簡化,才動手寫作。切記:" 先想清楚要怎麼用,才會知道要怎麼寫 "。

如果你皮夾裡有不同的紙鈔,你可以把它們都加起來換成美元

1000 NT 500 HK + .s 320 RMB + .s dollars                        \  以美元印出稅值

" dollars " 與 " ." 相同,僅顯示一個數字,使用" dollars "來顯示錢的總數,在英文語法上是很自然的

此時如果我在香港總數也可快速的換成港幣

1000 NT 500 HK + 320 RMB + .s $HK dollars                   \ 全部換成港幣,印出

金城 註疏:

寫一個 FORTH 的程式一如寫一首詩,要信、達、雅兼顧,所以dollars 的用途就是在提昇「雅」的境界。

練習一、商務旅行

帶 1000 美元離開 S.F 到 H.K 用去 1200 HK 因 S.H 得到 2000 RMB 在H.K用去 900 HK 在臺北用去 30000 NT 剩多少美元:

答案是 :

1000 1200 HK - 2000 RMB + 900 HK - 30000 NT - dollars

例題二、溫度換算

攝氏和華氏之換算與換錢不同,乃在此兩種溫度,除刻度因素外有一個差距值

: F>C ( nFarenheit -- nCelcius )   32 -   10 18 */ ;   : C>F ( nCelcius -- nFarenheit )   18 10 */   32 + ;   90 F>C .                  \  顯示夏天白天的溫度 0 C>F .                  \  顯示冬天晚上溫度

金城 註疏:

在 FORTH 中, */ 的運算,主要是應用在分數的乘法上。例如: 10 18 */ 就是乘以 9 分之 5。

在以上的例子,我們使用下面之 Forth 數學運算子

+   ( n1 n2 -- n1+n2 )        \ 加 N1 和 N2,把和留在堆疊(Stack) -   ( n1 n2 -- n1-n2 )        \ 從 N1 減 N2,把差留在堆疊(Stack) *   ( n1 n2 -- n1*n2 )        \ N1 乘 N2,把積留在堆疊(Stack) /   ( n1 n2 -- n1/n2 )        \ 以N2除以 N1,把商留在堆疊(Stack) */  ( n1 n2 n3 -- n1*n2/n3 )  \ N1 乘 N2 之積除以 N3,把商留在堆疊(Stack) .s  ( ...--... )              \ 把堆疊(Stack)最上方的 4個數字顯       \ 示出來,此字不會改變堆疊(Stack)                               \ 的深度及內容。

我們必須介紹堆疊(Stack)之概念、堆疊(Stack)是電腦之記憶區來儲存及取回數字,與第一課之變數(Variables)不同,變數(Variable)是在記憶體中命名的位置,要以指定之名稱抓取,堆疊(Stack)是一張先進後出(First-In-Last-Out)的表,當給 Forth 一個數字時會放在堆疊(Stack)上,任何運算子所需之數字,必須由堆疊(Stack)上彈出所需數字,最容易取得的字是堆疊(Stack)最頂端的字,很多的 Forth 指令,可以製造一個或很多個數字而這些數字產生時會放在堆疊(Stack)上。

" + "使堆疊(Stack)最頂端之兩個數字彈出,相加後,將和放回堆疊(Stack),-、*、/ 是處理簡單數學常使用的運算子,必須注意給" + "和" * "兩個數字的先後順序是無關緊要的,而給" - "和" / "之先後順序是重要的,交換這兩個數字,分別會產生不同差或商。

在 Forth " */ "是一種比例運算子,尤其在整數的比例運算中,以 N2/N3之比例乘 N1 ,由例一、例二顯示,此種比例運算子是非常有力的,分數乘法的運用可以免去使用浮點點之必要。

.S 是有力的除錯工具顯示出堆疊(Stack)最上面四個項目之內容,它常在除錯時使用,來確定在計算時堆疊(Stack)之數字是正確的,它通常不用在最終程式以免印出太多中間值

一些不常用,但重要之數學運算子:

MOD     ( n1 n2 -- rem )        \ N1除以N2,餘數留在堆疊(Stack)上 /MOD    ( n1 n2 -- rem quot )   \ N1除以N2,把餘數和商留在堆疊(Stac k) ABS     ( n -- │n│ )            \  換算堆疊(Stack)頂部之數字為絕對值 NEGATE  ( n -- -n  )            \  更改堆疊(Stack)頂端 N 之正負號

堆疊(Stack)操作指令

堆疊(Stack)是個最重要的地方,先前執行的結果,由前一個指令留到現在執行的指令,指令從堆疊(Stack)取走參數,將結果留在堆疊(Stack)給後來之指令,堆疊(Stack)可以很容易地被成串的副程式共用,此副程式可以呼叫別副程式,以此類推,此副程式是 Forth 的詞,它們可毫無限制的組合,這是為什麼 Fortrh 結構和的語法構造上如此簡單的重要原因。

然而,在堆疊(Stack)裡把不正確的數字順序給指令是常發生的像 - 及 /,有一組堆疊(Stack) 操作指令可重新排堆疊(Stack)上的數字這5個最重要的高階堆疊(Stack) 操作指令是:

UP   ( n -- n n )             \ 複製堆疊(Stack)之頂端之數字 WAP  ( n1 n2  -- n2 n1 )      \ 把堆疊(Stack)頂端的兩個數字互換 VER  ( n1 n2 -- n1 n2 n1 )    \ 複製堆疊(Stack)的第二個數字 OT   ( n1 n2 n3 -- n2 n3 n1 ) \ 把第三個數字換至堆疊(Stack)頂端 ROP  ( n -- )                 \ 拋棄堆疊(Stack)的頂端的數值

金城 註疏:

剛開始學 FORTH 的時候,會很不習慣堆疊(stack)的運作方式,尤其是使用過其他高階語言的人們,對使用大量變數的程式寫作方式已然在心中定型,所以學 FORTH 便倍感吃力。但堆疊(stack)是 FORTH 最重要的觀念,所有的參數傳遞與運算皆在堆疊(stack)中完成。可謂之學 FORTH 的任督二脈。如果學者真的無法了解堆疊(stack)的操作觀念,可以想像堆疊(stack)就是區域變數與參數的共用區。其實如果真的深入了解各種高階語言的編譯程式,便會知曉堆疊(stack)的實用價值了。

例題三、長方形

長方形是由(X,Y)座標來指定其左上及右下角,以堆疊(Stack)裡的這4個整數,我們可以計算長方形之面積、中心及周長。

: area ( x1 y1 x2 y2 -- area )   ROT -   ( x1 x2 y2-y1 )           \  求高   SWAP ROT - ( y2-y1 x2-x1 )        \  求寬   * ( area )                        \  寬乘高成面積留在堆疊(stack)頂端 ;                 : center ( x1 y1 x2 y2 -- x3 y3 , center coordinates )   ROT - 2/ ( x1 x2 y3 )             \  求高的中心點   SWAP ROT - 2/ ( y3 x3 )           \  求寬的中心點   SWAP ( x3 y3 ) ;  : sides ( x1 y1 x2 y2 -- sides )   ROT - ABS ( x1 x2 y2-y1 )          \  求高   SWAP ROT - ABS ( y2-y1 x2-x1)      \  求寬   + ( sides ) 2* ;

邏輯運算子

電腦使用邏輯運算來決定及遵循不同之執行通道也就是所謂的分歧,邏輯運算本身非常簡單且容易瞭解,然而多種邏輯運算的組合及在一個大程式裡對眾多不同的通路上做分歧的抉擇會使得電腦的運作顯得很複雜,甚至顯示出一些智慧的特質。

這裡我們介紹一些與數字合併使用的邏輯運算而且分歧之跳躍運算會使用他們。以邏輯運算的結果來選擇不同之運作。

Forth 使用整數來表示邏輯值,只有 2 種值真與偽,凡不是 0 的任何數代表真,以 0 表示偽,一個 Forth 邏輯運算所傳回的真值通常傳回 -1 ,此數字代表邏輯的值也稱作旗標(Flag),此點與 C 語言的觀點是一致的。

>      ( n1 n2 -- f )       \  如果N1 大於 N2傳回對,否則傳回偽 <      ( n1 n2 -- f )       \  如果N1 小於 N2傳回旗標為真. =      ( n1 n2 -- f )       \  如果N1 等於 N2傳回旗標為真. 0=     ( n -- f )           \  如果N  等於 0傳回旗標為真. 0<     ( n -- f )           \  如果N  小於 0傳回旗標為真,用來檢查是否為負數                                NOT    ( f1 -- f2 )         \  如果f1是錯傳回真,如果f1是真傳回錯                                AND    ( n1 n2 -- n3 )      \  n3 是 n1 和 n2 每一位元作 AND 的結果 OR     ( n1 n2 -- n3 )      \  n3 是 n1 和 n2 每一位元作 OR  的結果

在為真和為偽之旗標(Flags)裡,AND 和 OR 會正確運作

-1  -1  AND   -1    \  true true AND    --   true -1   0  AND    0    \  true false AND   --   false  0   0  AND    0    \  false false AND  --   false -1  -1  OR    -1    \  true true OR     --   true -1   0  OR    -1    \  true false OR    --   true  0   0  OR     0    \  false false OR   --   flase

金城 註疏:

要小心 7 8 AND 的結果為 0 ,以二進位來看就明白了。

在冒號定義下,旗標(Flag) 可以用來選擇二個執行通道的一個,經由下面之結構。

( f ) IF [true clause] ELSE [false clause] THEN ( f ) IF [true clause] THEN

它們在第二課簡短討論過,現在我們知道如何產生旗標(Flags)了。也知道如何結合旗標與分歧敘述的運用格式了。

金城 註疏:

人類因為有抉擇判斷的能力,所以稱為萬物之靈。而電腦也因為提供了循序、迴路與分歧的控制流程指令而使得軟體程式透出一種幾乎近似人類的智慧能力。但千萬別倒因為果,是有智慧的人才能寫出有智慧能力的程式,不是電腦本身的「天生偉大」。所以如果寫程式的人是白痴、蠢材,麼那寫出的程式必然也又呆又笨了。

例題四、天氣報告

下面之冒號定義說明邏輯和分歧的用法

: weather ( nFarenheit -- )   DUP     85 >   IF      ." Too hot!" DROP   ELSE    55 <           IF      ." Too cold."           ELSE    ." About right."           THEN   THEN ;

你可以鍵入下列指令,並從電腦獲得一些回答

90 weather  Too hot! 70 weatehr  About right. 32 weather  Too cold.

金城 註疏:

判斷與分岐的關念,構成了條件子句,通常可用於做例外的處理或二選一的執行流程。在 FORTH 的諸多工具中,可以用結構化的 D-型圖(Dijestra-Chart)來將分岐的架構平面化。一來容易了解自己的語意(Semantic)是否正確,二來容易轉換成FORTH的條件子句。另外,我 要提醒的是,對 FORTH 的程式設計而言,應該僅量的少用IF---ELSE---THEN。因為分岐的不當使用,會使程式的複雜度提高,可讀性降低與速度變慢。

回顧 LOOP 指令

在冒號定義中以下列之格式使用固定次數迴路指令

[nLimit nIndex]        DO   [repeat clause] LOOP [迴路終值  迴路起值]   DO   [反覆的敘述]     LOOP

DO 從堆疊(Stack)取出兩個參數,頂端數字是迴圈的開始指數,第二個字是迴圈指數的上限進入 LOOP 之後[repeat clause]被重覆執行,LOOP 增加指數從 nIndex 至 nLimit ,當指數相等於 nLimit 時,LOOP 結束,在[repeat clause] 裡,一個特殊運算子 I 傳回目前的迴路索引,並將之放在堆疊(Stack)的頂端。

以下是 LOOP 結構的簡單例子

例題五、印出乘法表

: oneRow ( nRw -- )   CR   DUP 3 .R 3 SPACES   13 1   DO      I OVER *          \  留底數並計算乘積           4 .R              \  將堆疊(stack)頂端的乘積,以格式化的方式輸出   LOOP                            DROP  ;   : Table ( -- )   CR CR 6 SPACES   13 1   DO      I 4 .R  LOOP        \  印出頂端的表頭   13 1                        \  呼叫 oneRow (一行) 12 次   DO      I oneRow   LOOP ;

鍵入Table使乘法表,以整齊的格式顯示

練習一

美國乘法表在 12*12 結束,但在中國和公制國家,乘法表在 9*9結束,修改TABLE 定義,使其表格限定在 1*1 至 9*9

金城 註疏:

我一向反對濫用迴路來解答問題,例如:要你求 1 到 100 的總合,你會用迴路加個 100 次,那要你求 1 到 1億的總合,你又想用迴路加個1億次。好似迴路是不用付出代價的。其實如果你上過小學就會了解「首項加末項乘以項數之半」為等差級數的總合。這是否足以說明某些問題如果你能找到運算公式的通則,大量濫用的迴路是可以避免的。另外,由於迴路代價甚高,所以如果非用不可,應僅量的使迴路內的工作單純化,尤其如果可能要避免在迴路使用過多的分岐條件子句以免使執行效率大為降低。

第四課,數的兩個範例

在這一課,使用二個相當複雜的例子來,說明 Forth 指令的組合過程來解決真正的問題,使用在前三課我們學過的 Forth 指令

例題一、日曆

我們要做的是印出任一年任一月的月曆,事實上可從1950年到2050年任意選出年、月來印,日期每行以星期日,星期一以此類推來印,此問題,有許多事情要考慮:閏年、一個月的天數、那星期的那一天是某個一月的第一天,計算年、月、日,以(YYYY)表年、(MM)表月、(DD)表日,我們所求之年月日以(DD MM YYYY)表示出,用 7除以年、月、日之餘數就是星期幾了。

以1950,1,1為日曆的 0 日,剛好是 Sunday ,4年有 1461天,一個閏年有366天,任一年的年月日,可以由 YEAR 計算出,並存於變數(Variable)JULIAN,如果這一年是閏年,變數(Variable) LEAP 的值為 1,否則為 0 。

在一年裡每個月的第一天為此月的第幾天很難計算,在指令 FIRST 裡處理;FIRST 列出月份之數字(1 代表 1月,2 代表 2月等)並傳回 1年之中從1月1日至此月的第一天之天數,2月1日是一年的第31天,3月1日不是第59天,就是第60天,依據閏年是否而定,4月1日是第90或91天,以此類推,一個適切的公式,在FIRST 裡被用來計算 5月 1日和其後的全部月份。

指令 DAY 從堆疊(Stack)中取出 DD、MM 及 YYYY,並將該天自 1950年起的總天數放回堆疊(Stack),總天數被使用來除以 7 的餘數決定此日為星期幾,星期天是用 0 代表,星期六是 6 。

.DAY 印出一個月的月曆,由堆疊(Stack)上的月份所指定,並以星期排列之表格形式印出,此格式加上邊框來顯示,以指令 MONTH 指出月份名稱、日期名稱,例如鍵入:

1992 YEAR 7 MONTH

會顯示 1992年的 7月份的月曆,指令 CALENDER 取代任一年的 12個月份的完整月曆,由堆疊(Stack)上年的數字指定,你無法在螢幕上看到全部,但你可以用印表機印出來。

1992 YEAR CALENDAR

VARIABLE JULIAN  \  用來存放自 1950年 1月 1日起到今年第一天所經過的天數 VARIABLE LEAP    \  用來存放是否為閏年 1461 CONSTANT 4YEARS   \  包含閏年、四年的總天數   : YEAR ( YEAR --, 計算 Julian 與閏年是否為 1 )   1949 - 4YEARS 4 */MOD      \  自 1949年 1月 1日起經過了幾天   365 - JULIAN !             \  0 為 1/1/1950 的第一天   3 =                        \  若餘數為 3 則為閏年   IF     1 LEAP !            \  閏年   ELSE   0 LEAP !            \  普通年   THEN  ;

金城 註疏:

上列  IF     1 LEAP ! ELSE   0 LEAP ! THEN ;           部份可以改用 ABS LEAP! 而去掉判斷式。

: FIRST ( MONTH -- 計算每月的第一天為本年的第幾天 )   DUP 1 =   IF DROP 0                      \  0 為 1月 1日   ELSE    DUP 2 =       IF      DROP 31            \  31 為 2月 1日       ELSE    DUP 3 =             IF    59 LEAP @ 2    \  3月 1日可  為 59天或 60天             ELSE  4 - 30624 1000 */                   90 + LEAP @ +   \ 4月以後每個月乘以 30.624天             THEN       THEN   THEN ;

金城 註疏:

乘以 30624 的整數積剛好能使七、八兩月均為31天,此為本例題最精彩之處。0.624 為5天除以八個月減十一月為小月而得來。

: DAY ( DD MM YYYY -- JULIAN-DAY )   YEAR                 \  計算 JULIAN 與閏年   FIRST + 1-           \  計算今天是今年的第幾天   JULIAN @ +           \  計算由 1950年 1月 1日起到今天一共過了幾天 ;                          : STARS 0 DO 42 EMIT LOOP ;     \ " * " 的 ASCII 碼為 42   : header ( n -- )               \  印出表頭   cr cr 26 stars space   case     1 of ."  January " endof            2 of ." February " endof            3 of ."   March  " endof            4 of ."   April  " endof            5 of ."    May   " endof            6 of ."   June   " endof            7 of ."   July   " endof            8 of ."  August  " endof            9 of ." September" endof           10 of ."  October " endof           11 of ." November " endof           12 of ." December " endof           DROP   endcase   space 27 stars cr cr   ."      SUN     MON     TUE     WED     THU     FRI     SAT"   cr cr                         \  以下印出正確週、日期內容 ;   : BLANKS ( MONTH -- )      \ 將不是此月份的日子跳過   FIRST JULIAN @ +         \ 計算由 1950年 1月 1日到這個月過了幾天   7 MOD 8 * SPACES         \  如果不是星期天跳過前一個月在本週的餘尾天數 ;                                        : .DAYS ( MONTH -- )        \  印出本月的天數,以週的格式印出   DUP FIRST                 \  此月份之第一天的年天數   SWAP 1 + FIRST            \  下月份之第一天的年天數   OVER - 0                  \  以迴路印出數於本月的天數   DO I OVER +     JULIAN @ + 7 MOD        \  計算是星期幾     0= IF CR THEN           \  如果是星期天跳至下一行的起頭列印     I 1 + 8 U.R             \ 每一天佔八個格子靠右印出   LOOP   DROP ;                    \ 將多出的月份的第一天從堆疊頂端刪除 ;                                       : MONTH ( N -- )            \ 印出一個月之月曆   DUP   HEADER DUP BLANKS         \ 印表頭   .DAYS                     \ 印本月的天數 ;    : CALENDAR ( YEAR --- )     \ 印該年的全年月曆   YEAR                      \ 計算 JULIAN 和閏年   13 1 DO I MONTH LOOP     \  印12個月之月曆   CR CR 64 STARS            \  印出最後之邊框 ;

金城 註疏:

一個好的FORTH程式,讀起來應該似散文的流暢,詩般的美。

如果要列印出日曆打入:

PRINT 1992 YEAR CALENDAR

PRINT 是一個特別的F-PC指令,會把螢幕輸出轉換到印表表機輸出

金城 註疏:

一個好的程式設計家,會很清楚自己所面對的題目中有那幾個要突破的困難,並分析各種可能解決此問題的方案,其利弊得失詳加考慮後,才動手開始撰寫程式。並非是一腦糊塗,兩眼發呆,就能夠擺平問題的。在此題中可詳加品味其思索問題的方式與簡潔明快的程式寫作風格。如果一下子看不懂那就多咀嚼幾次,以不怕挫折的心接受考驗與磨鍊,體會受一下 FORTH 愚公移山的精神。

練習一、真正之 Julian 日期

在上面的例子,我們以1950,1,1為 0天來計算年,月,日這是因為我們使用一個整數來代表日期,而其範圍受限在-32768到32767,大約是以包含89年,真正之 Julian 年月日,開始於西元前 4713年 1月 1日是世界的開始(西方的觀念),要表示如此大範圍的年月日,我們必須使用雙倍精密度整數,-2,294,967,295至 2,294,967,294之範圍,Julian 日期才夠用。

: JULIAN-DATE ( DD MM YYYY -- d, 以32位元整數來計算 Julian 的值 )   >R                      \ 存YYYY於回返堆疊(return stack)   DUP 9 + 12 /            \  1月、2月為0,其餘為1   R@ + 7 * 4 / NEGATE     \ 每一年取出1.75天                           \   365.25 = 367-1.75   OVER 9 + 12 / NEGATE   R@ +   100 / 1 + 3 * 4 / -     \  各世紀產生之閏年   SWAP 275 9 */           \  這個月之前,本年的天數   + +                     \ 加DD,為本日之年天數   S>D 1.721029 D+         \  加西元,元年一月一的 Julian 日數   367 R> UM* D+           \ 加上到今年的日數 ;

有幾個處理雙倍精密度數的指令

S>D      ( n -- d )              \  將一個整數擴大為雙倍精密度整數 D+       ( d1 d2 -- dSum )       \  將 2個雙倍精密度數相加在堆疊(stack)的頂端 UM*      ( n1 n2 -- dProduct )   \  2個整數相乘並傳回一個雙倍精密度的整數乘積

Forth 有另一個堆疊(Stack),做為呼叫副程式及返回位置之保存,稱為回返堆疊(return stack),其功能對初學的使用者是隱藏的而使用者也不須費神去使用它。然而回返堆疊(return stack)被用在暫存參數堆疊(Parameter Stack)上之數是種常見的技巧,並且暫時將其在兩個堆疊(stack)間移動,將數目在參數堆疊(Parameter Stack)和回返堆疊(return stack) 之間移動的 Forth指令是:

>R         ( n -- )    \  取出參數堆疊(Parameter Stack)頂端之數,將它移到回返堆疊(return stack)的頂端 R>         ( -- n )    \  取出回返堆疊(return stack)頂端之數,將它放回參數堆疊(Parameter Stack) R@         ( -- n )    \  複製回返堆疊(return stack)頂端之數放到參數堆疊(Parameter Stack)

金城 註疏:

請小心的使用迴返堆疊

例題二、 生命的遊戲

生命遊戲是一個有趣的電腦程式可以在電腦的記憶體,模擬群體社會之成長和衰敗,假設電腦的記憶體是張平面圖,每一個記憶體不是空的就是有一個生命體,一個位置有無生命現象,都取決於鄰近位置之生命體,生活遊戲之規則:

生命體死亡,因為孤單(鄰居<2)或擁擠(鄰居>3),當有3個活鄰居,假定一個父親、一個母親、一個牧師、一個新生命會產生。

以電腦螢幕為圖,包含 25*80 之位置,一個 # 代表在一個位置上的生命體,一個空白指出在那個位置沒有生命,在記憶體裡一張圖配有2048 位元組.只有1920 個位置(24*80),可在螢幕上顯示,記憶區是以 80 個連續bytes 鄰接80 個連續位元組之形式排列,所以 N 位置的鄰居位置是是N+1、N-1、N+79、N-79、N+80、N-80、N+81、N-81 而所有數字都是 2048之餘數可用 2047 AND一個數來獲得除以 2048之餘數。

記憶區可從 PAD 之 2048bytes 指派,PAD 指出使用者能用之自由記憶體空間,事實上我們需要 2個 2048位元組區域,第一區貯存生命體之圖表,第二區貯存下一代生命體之分佈狀況,當計算一代之後,在第二區之圖表會複印至第一區。

金城 註疏:

如果要求除以 2 的幾次方的餘數,在 FORTH 的技巧中,我們會使用位元的罩斷(MASK)來求得,因為一個除法指令的速度代價犧牲太大了 ,以 8086 為例除法指令要 192 個 CPU 時脈而邏輯指令僅需 4個時脈。當然,這種類似組合語言的挑剔態度,也是 FORTH 在速度上直 逼組合語言的原因。

35 CONSTANT white             \  ASCII # 32 CONSTANT black             \  空白   : address1 ( n -- addr )      \ 生命體之第一區   2047 AND                    \ N 除以 2048 之餘數   PAD +                       \  加位移至PAD ;   : address2 ( n -- addr )      \  給下一代之第二區   address1                    \  加2048至addr1   2048 +                      \  由 addr1 處理餘數的問題 ;   : neighbors ( -- )   2048 0                        \  審視全部圖表   DO      I 1 + address1 c@     \  在8個鄰居內加生命體、右邊           I 1 - address1 c@ +   \  左邊           I 79 + address1 c@ +  \  左下角           I 79 - address1 c@ +  \  右上角           I 80 + address1 c@ +  \  下邊           I 80 - address1 c@ +  \  上邊           I 81 + address1 c@ +  \  右下角           I 81 - address1 c@ +  \  左上角           I address1 c@         \  生命體在這一位置嗎?           IF      DUP 2 =       \  是的,2或3個鄰居?                   SWAP 3 = OR   \  如果是 2至 3個鄰居則活下來                   IF 1 ELSE 0 THEN  \  太擁擠則死掉           ELSE    3 =               \  空的位置                   IF 1 ELSE 0 THEN                                 \  如果3個生命鄰居給予出生           THEN           I address2 c!         \  貯存下一代   LOOP ;   : refresh ( -- )                \  複印下一代到這一代   PAD 2048 + PAD 2048 CMOVE ;   : display ( -- )             \  在螢幕顯示這一代圖表   0 0 AT                     \  移動游標至左上角   PAD 1920 0                 \  審視1920位置   DO      DUP C@             \  生命體在這?           IF WHITE ELSE BLACK THEN    \  顯示它           EMIT           1 +                \  下一個位置   LOOP    DROP ;  : init-map  ( addr -- )      \  從某一記憶體產生一個圖表   2048 0                     \  注意2048bytes區   DO DUP C@ 1 AND            \  使用最小有效位元來檢查      IF 1 ELSE 0 THEN        \  指派生命體          I address1 C!       \  在我們的這一代圖表裡          1 +   LOOP   DROP ;  : generations ( n -- )         \  重覆N代   0 DO    neighbors      \  計算下一代           refresh        \  複印至這一代圖           display        \  並顯示它   LOOP ;

slow                      \  顯示每一個文字,使用 BIOS 的中斷來顯示 statoff                   \  隱藏表頭,將 F-PC 的狀態顯示暫時關掉 500 init-map              \  開始圖表,由記憶體中任選一塊記憶體,當第一代的生命來源 10 generations            \  做10代

在此例中介紹之Forth新指令

CONSTANT       ( n -- )        \  定義新指令,當執行時傳回到N C@             ( addr -- char) \  從一個記憶體位置取一個byte C!             ( char addr --) \  貯存一個byet至記憶體位置 CMOVE          ( a1 a2 n -- )  \  從記憶體位址a1複製,N個bytes至記憶體位址a2 PAD            ( -- addr )     \  送回到一個自由記憶體緩衝區的位址 AND            ( n1 n2 -- n3 ) \  逐位元AND 2個 16位元數字 OR             ( n1 n2 -- n3 ) \  逐位元OR 2個 16位元數字

練習二

想一個你喜喜愛的棋盤遊戲,並考慮電腦化的可能性,也許全盤電腦化非常困難,但是你應該找出一些方法使它部份電腦化,想些方法使電腦能幫助你棋藝進步。(丁陳博士本身就曾用 FORTH 寫了一個圍棋程式)

金城 註疏:

將 FORTH 看成一個雙堆疊結構的虛擬 CPU ,則 FORTH 的基本指令,便可以看成是此虛擬 CPU的機器指令(其實,在真正的 FORTH硬體 CPU 上,所有的 FORTH 指令均是以硬體階層直接執行,在那種 CPU 上沒有組合語,因為 FORTH 就是最低階的機器語言了),在 FORTH 的虛擬 CPU 上。

CONSTANT 可以看成 push Constant VARIABLE 可以看成 push Address of Variable

也就是載入變數的有效位址。在 FORTH 中所有對變數的操作均有統一性,給予一地址在堆疊的頂端,要加、減、乘、除,要存、要取,要八位元、要十六位元、要三十二位元,皆由使用者自己選配使用,剛開始會不習慣,但久了,反而會喜歡這種簡潔有力的表達方式。

第五課,數字的技巧

例題一、正弦和餘弦

三角函數之 SIN、COS 大都在繪圖函數功能裡遇到,在畫圓弧及其它用途上是常常用到的。通常使用浮點數字來做為準確性和活動性範圍之計算,然而對在數字系統裡的製圖過程中整數範圍 -32678至 32767 大致已夠用,我們應該學習使用十六位元的整數來計算 SIN 及 COS。

三角函數的 SIN 或 COS 的值域介於 -1.0及+1.0 間,我們用十進位整數10000代表 1.0 來計算,使 SIN和COS 能以最大的精密度表出。pi 是 3.1416 ,而90度角由15708 取代,角度先轉換成 -90度至+90 度間之範圍,然後再由-15708至+15708 之間的數換算成弳度,由弳度計算 SIN、CON之值。

Sin和Cos之計算精準到1/10000 ,此演算法首先由 John Bumgarner 發表於Forth Dimension 第四卷第一期第七頁。

31415 CONSTANT PI 10000 CONSTANT 10K                     \ 比例常數 VARIABLE XS                            \ 角度量的平方   : KN ( n1 n2 -- n3, n3=10000-n1*x*x/n2 在此處 x 表示角度 )   XS @ SWAP /                     \  x*x/n2   NEGATE 10K */                   \  -n1*x*x/n2   10K +                           \  10000-n1*x*x/n2 ;   : (SIN) ( x -- sine*10K, x 為弳度乘以 10k的值 )   DUP DUP 10K */                  \  以10K為X*X之比例   XS !                            \  將之存入 XS   10K 72 KN                       \  最後項   42 KN 20 KN 6 KN                \  項3,2和1   10 K */                         \  乘 X ;  : (COS) ( x -- cosine*10K, x 為弳度乘以 10k的值 )   DUP 10K */ XS !                 \  計算並存入X*X   10K 56 KN 30 KN 12 KN 2 KN      \  數個擴大形式 ;  : SIN ( degree -- sine*10K )   PI 180 */                       \  換算為弳度   (SIN)                           \  計算 Sin 的值 ;  : COS ( degree -- cosine*10K )   PI 180 */                       \  計算 cos 的值   (COS) ;

金城 註疏:

在此課中,我們會發現 FORTH 的「整數哲學」,非常突出的要求以整數來解決其他語言使用實數才能解決的問題。一來是 FORTH認為要發揮硬體 CPU 的真實功能。如果,沒有實數的浮點運算器,就不用實數,而高度技巧來發揮整數的功能。這一點也說明了為什麼人稱 FORTH 是屬於天才的語言,因為要「多思」乃才能解決問題啊!

來測試此程式,鍵入:

90 SIN .                    \  9999 用整數雖不完全精準,但也很夠用了                                        45 SIN .                    \  7070 30 SIN .                    \  5000  0 SIN .                    \     0 90 COS .                    \     0 45 COS .                    \  7071  0 COS .                    \  10000

例題二、亂數

亂數通常使用在電腦模擬和電腦遊戲。下列亂數產生器是刊載於 Leo Brodie 的 Starting Forth 一書中。

VARIABLE RND                            \ 種子 HERE RND !                              \ 初值化種子   : RANDOM ( -- n, a random number within 0 to 65536 )   RND @ 31421 *       \ RND*31421   6927 +              \ RND*31421+6926, 再求除以 65536 之餘數   DUP RND !           \ 更新種子,並在堆疊頂端留下新亂數 ;  : CHOOSE ( n1 -- n2, a random number within 0 to n1 )   RANDOM UM*          \ n1*random 得一 32位元的值   SWAP DROP           \ 拋棄低的 16位元值 ;                     \ 事實上等於是除以 65536 的商(向右移位 16位元)

鍵入以下數個測試程式,看看輸出的結果。

100 CHOOSE . 100 CHOOSE . 100 CHOOSE .

結果被隨機分配在 0 和 99 之間的整數被產生。

練習一

記得中國幸運餅嗎?寫一個資料庫程式選擇一個隨意的幸運詞給予幸運餅,當然你先要建立此資料庫。

金城 提示:

建立一個字串所組成的陣列,再以亂數當索引取出字串的內容。

金城 註疏:

在資訊科學的領域中,亂數的產生是一門與數學相關的大學問。如果產生的分佈不合預期的結果就不算是一個「合格」的亂數產生程式。一般可用蒙地卡羅法來觀察其輸出分佈的情形。如果要應用在電腦模擬的領域中,則更是講究亂數的產生方程式。當然,如果看官本身對機率統計的知識「不知所云」,那就套個別人的公式用好了。反正「有」得「抄」比「沒得用」要好多了。

例題三、平方根

N+1 的平方相等於 N 的平方與 2N+1的總和,從 0 加 1,3,5,7...等,直到總數大於你要求根的整數,你所停止時的數就是平方根

: SQRT ( n1 -- n2, n2**2<=n1 )   0                          \ 根之初值   SWAP 0                     \ 設 N1 為 limit   DO      1+  DUP            \ 更新根           2* 1+              \ ( 2n+1 )   +LOOP                      \ 把 2N+1 加到總和,如果小於N1則再做,否則結束 ;    

有限迴圈結構

limit  index   DO [repeat-clasue] ( inc ) +LOOP ( 極限   初值    DO   重覆的迴路體    累加數  +LOOP )

在重覆的迴路體部份與 DO...LOOP 很類似,其不同在於 +LOOP 是透過參數堆疊(Parameter Stack)頂端數字來增加指數,因此允許 LOOP 指數在執行計算時可使用非固定的任意數增加或減少迴返堆疊頂端的迴路索引值,+LOOP 結束迴路的方式為當迴路索引值相等或大於極限時。

Wil 以非傳統方式使用 +LOOP 在這裡迴路索引值累加器使用,將連續的2N+1(奇數)相加,直至總和超過N1,由此觀點,留在堆疊(Stack)的N2即是N1的平方根。

練習二

用牛頓法來找平方根是電腦中非常普遍的方法,如果r1是N平方根的近似值,那麼更好的近似值是:

r2=(r1 + n/r1)/2

你能否以此方法寫出找平方根的程式

金城 提示:

r1以1開始趨近,再將求出的r2當成r1傳入,反覆此程序,直到r2的平方等於N)。

練習三

整數的立方和 4 次方,會 16位元整數的範圍很快用盡,使用連續整數求其 3或 4 次方的數學方程式,可用連續的加法找出立方根,並與你所取得之根的整數比較,寫出新指令 Cubic Root 來找出任意正整數之立方根,4次方根。

金城 註疏:

求平方根的 SQRT 程式是全球聞名的FORTH高手,Wil Baclon 先生的「天才作品」,發表於 1984年在台灣交大舉行的 FORML年會上 。此程式短小辛辣為曠世不二之作,每一個指令都用得出神入化,百鍊而成。如果看官能一眼看穿其奧妙絕倫之處,則可喜可賀,必能青出於藍而勝於藍。當然,如果看官們對此佳作不能品味,則只好在紙上好好的畫個堆疊圖來慢慢追,慢慢K了。此題也是學FORTH 的最佳範例。提示:此題的演算法為一遞迴的定義,此線性的方式展開執行。

例題四、最大公因數(GCD)

古代數學大師歐其理德( Euclid )的方法

If m>n,find GCD(n,m) If m=0,GCD(m,n)=n  Otherwise,GCD(m,n)=GCD(m,MOD(m,n))

金城 註疏:

此演算法為最大公因數的遞迴(Recursive)定義。

將上列演算法改寫為 Forth 我們得到

: GCD ( m n -- gcd )   BEGIN   2DUP >               \ if m>n 交換 m 和 n           IF SWAP THEN           OVER                 \  if m=0 離開loop   WHILE   OVER MOD             \  否則以 MOD [m,n]替換n   REPEAT                       \ 重覆直至 m=0   SWAP DROP                    \  因 m=0 所以拋棄 m ,而留下來的 n 就是最大公因數了 ; 

這是一個極好的例子來說明條件迴圈的用法

BEGIN [repeat-clause] (f)   WHILE     [true-clause]    REPEAT BEGIN [迴路子句部份]  旗號  WHILE [為真的才執行的部份] REPEAT

如果旗號經 WHILE 測試是真的,重覆迴路子句部份和為真的才執行的部份,若旗號是 0(偽),結束此迴路。

測試 GCD ,鍵入

123 456 GCD .

練習四、最小公倍數

兩數的最小的公倍數的求法,可將兩數之積除以此二數之 GCD 寫一個新指令LCM 將 2個整數的 LCM 放回堆疊(Stack)

金城 註疏:

例題五、費氏數列

Fibonacc:是中世紀數學家 Leonardo 的筆名他提出此問題來計算一對兔子所能產生的後代數量。

把一對兔子放入由牆圍繞的地方,如果每個月,每對兔子會生出新的一對兔子,從第二個月之後所生的新兔子又能生出一對兔子,一年內會生出多少對兔子來?

順序會是:

1 1 2 3 5 8 13 21 34 55 89 144 233 337...

金城 提示:

費邊數列的遞迴定義為 Fib(N)=Fib(N-1)+Fib(N-2)

我提供兩個相等的解答

: Fib1 ( -- , 印出所有小於  5000 的費邊數列 )   1 1                        \ 兩個開頭的 Fib 數字   BEGIN   OVER U.            \ 印出較小的數字           SWAP OVER +        \ 計算下一個 Fib 數           DUP 50000 U>        \  如果數字太大離開   UNTIL                      \ 否則重覆   2DROP                      \ 拋棄此數字 ;   : Fib2 ( n -- , 印出所有小於正整數 n 的費邊數列 )   1                           \  最先的數字   SWAP 1                      \  設定範圍   DO      DUP U.              \  印出目前2的數           I                   \  下一個 Fib 數           SWAP                \  預備下一個到來   +LOOP                       \  把現有的加至迴返堆疊的頂端                               \  重覆至 Sum > n   U.                          \  印出最後 Fib 數 ;

測試程式,鍵入:

Fib1 10000 Fib2

Fib1 是很明確的指令,將先前的兩個 Fib 數字相加,來取得下一個數字。

BEGIN [repeat-clause]  ( f )  UNTIL BEGIN [ 迴路體的敘述 ]  旗號  UNTIL

當堆疊(Stack) 頂端項不是 0,重覆迴路體的敘述直至旗號是真的,則 UNTIL結束迴路的執行

Fib2 則使用 +LOOP 指令隱含的加法產物來計算下一個 Fib 數及與順便完成對極限是否到達的比較。此法使用了 Wil Baden 先生的程式技巧與風格來寫作程式,很辛辣刁鑽吧?

金城 註疏:

費邊數列又稱為「美」的數列,在自然界中,如年輪,海螺、耳蝸、合聲基至於蒙娜麗沙的五官比例,都是費邊數列。在資訊科學的領域中,也是許多應用,如記憶體分割區塊的管理,外部檔案的排序、與搜尋.......等。都是應用了費邊數列的自然分佈現象,如果有興趣在日本有專業的雜誌討論費邊數列。

練習五、二進位的等比級數

二進位等比級數類似費邊數列它的成員是:

1,2,4,8,16,32,64,128,256,512,1024,...

寫一個新指令 Binary Series 來顯示這一串數字

金城 提示:

可以用 2來乘(也就是用向左的移位),重覆的將結果轉出。

金城 感言:

多年以來,一直在 FORTH 的領域中掙扎,每次要寫一段小程式,就要「死」一次。因為Wil Badon 先生告訴我,寫 FORTH 的心境,就 是一種不停的自我挑戰、自我超越與自我革命。一定要不斷的創造新的技巧,不停地追尋美的境界。才能達到「字」「字」珠璣,增一「 字」則俗,減一「字」則怨的地步。(在 FORTH 的術語中,每個小程式都叫字(word))轉眼間,十年過去了,多少個嘔心瀝血的了夜為了 斟酌推敲「字」的得當與否而熬至天明。如今想起,依然觸目驚心。真的感受到了,FORTH 如詩、如詞、如歌、如畫,只有深深把 FORTH 推至了藝術的領域。也許,這一番話,你真的已經明白,也可能永遠都不會明白了。我承認,以資訊「工程」或「科學」的觀點來看這一 種心境,是太過不合理的「要求」因為大多數的人類是不可能做到大腦不必思考....」。難怪,大家喜歡BASIC、Clipper、Dbase了。但 我記得沙士比亞說過:「人們營營苟苟的為生活奔忙一世,但不曾真正的活過一天」。偉人與凡夫的差別應該在此吧﹖

第六課,終端機輸入和輸出

把第五課載入 (需要亂數產生器)

數字輸出的格式化指令群有

.          ( n -- )             \ 有正負號的印出指令 U.         ( u -- )              \ 無正負號的印出指令 D.         ( d -- )              \ 有正負號的雙倍精密度數印出 .R         ( n1 n2 -- )          \ 以 N2 個空間為格式並靠右印出 N1

這些指令通常足夠應付螢幕上顯示數字的用途,讓程式設計者知道電腦發生什麼事,但顯示數字給一般的使用者所習慣的特定方式,格式化輸出,仍無法以上列指令做到。

Forth 提供另一群更精準的輸出指令,使你可以將數字格式化成任何你想要的模樣,我們將討論這些指令並用一些例子說明如何來建立特定的數字格式。

格式過程由堆疊(Stack)項端之雙倍精密度數開始討論,輸出的是一個格式字串,暫時儲存在文字緩衝區之下的自由記憶體裡,由指令 PAD 指向暫時儲存區的位址格式化的輸出字串是逆向結構,一次處理一個阿拉伯數字。當建立此字串的過程中任何 ASCII 字元可以隨時插入此字串以改良其可讀性。建立數字格式化之結構如下:

<#         ( -- )           \ 開始數字格式化過程。 #          ( d1 -- d2 )     \ d1除以基底以商為 d2 留在                             \ 堆疊(Stack)上,餘數換成                             \ 阿拉伯數字,加入輸出字串的尾部。 #S         ( d -- 00 )      \ 換算所有有意義的阿拉伯字加                             \ 進輸出字串的尾部。 #>         ( d -- addr n )  \ 終結數的字串轉換,並留下此字串的地址和                             \ 長度在堆疊的頂端以供TYPE指令印出結果。 HOLD       ( char -- )      \ 加入 ASCII 字元到輸出字串。 SIGN       ( n -- )         \ 如果 N < 0 加入記號"-"到輸出字串。 BASE       ( -- addr )      \ 記憶地址儲存目前的基底。 DECIMAL    ( -- )           \ 設定基底為 10 (做為 10 進制換算用)

控制數字換算的基數,可以隨意志改變,這種完全自由的彈性允許 Forth 使用者做有趣及有力的數字格式化應用。

金城 註疏:

剛開始接觸 FORTH 的使用者,會不習慣沒有現成的格式輸出入函數可用。因為所有的高階語言都具備相當齊全的高階輸出入函數或指令,如 C語言有 printf() scanf()。Pascal有read() write()......等等。但是這些現成的函數除了易學、易用的好處之外,並無自由的彈性可供程式設計師耍玩精心巧思的設計能力。靈活的指令。不但能發揮強而有力的轉換功能,做出叫人讚賞不已的應用技巧。還能讓使用者真正的了解電腦的工作原理,達到一通百通的效應。

例題一、電話號碼

電話號碼包括區域碼、國際碼,可以用雙倍精密度數代替,以習慣的格式印出電話號碼,我們定義:

: Phone (d -- , 印出一個電話號碼,使用標準格式如 (415) 432-1230 )   <#                   \ 開始輸出字串   # # # #              \ 轉算最後四個阿拉伯字為字元的個格式   45 HOLD               \ 插入記號"-"   # # #                \ 轉算次三個阿拉伯字   32 HOLD              \ 加一個空白   41 HOLD              \ 加上右括弧   # # #                \ 加區域碼到格式化到字串中   40 HOLD              \ 加上左括弧   #>                   \ 結束格式化   TYPE SPACE           \ 印出輸出字串,並跳過一個空格 ;

雙倍精密度數可以由鍵盤輸入,輸入數字串的任何地方可含一個句點,FORTH的數字轉換系統便之此數字為雙倍精密度的整數。印電話號碼顯示如上所示,鍵入:

415432.1230 Phone

正確的格式字串會印在螢幕上

例題二、 時間輸出

假設一天的時間可以由雙倍精密度整數代替,由午夜開始計算,以 1/100秒為單位輸出,格式設計成 HH:MM:SS.XX ,XX 表示一秒中的百分之一秒。

: Sextal ( -- )   6 BASE !                     \ 設定基底為 6 ;  : :SS  ( d1 -- d2 , 將 d1 除以 60 並將其餘數(個位與十位兩字元)加入    輸出字串 )   #                      \ 以十進位換算其個位數   Sextal #               \ 以六進位為底換算下一個數(十位數)   DECIMAL               \ 恢復基底為十進位   58 HOLD               \ 加":"到輸出字串 ;  : Time ( d -- )   <#                      \ 開始輸出字串   # #                    \ 換算1/100秒   46 HOLD                \ 加" ."到輸出字串   :SS                    \ 換算秒   :SS                    \ 換算分   #S                     \ 換算小時   #>                     \ 結束換算   TYPE SPACE             \ 印出結果 ;

金城 註疏:

這些小程式都很實用,值得再三玩品。

從午夜到中午有 4320000 個百分之一秒,如果打入:

4320000. TIME

會看到 12:00:00.00 ,證明它會正確的轉換成格式化的字串輸出

例題三、角度

為了航海的目的,全世界的位置以經緯度來描繪,以 DDD:MM'SS''之形式,因為沒有合適的字元作度數,我們以":"作為度數,此格式只與時間稍為不同,定義:

: Angle ( d -- , print angle in the DDD:MM'SS'' format )   <#                          \ 開始輸出字串   34 HOLD                     \ 把秒加上''   #                           \ 秒以 十 進制數字轉換出個位數   Sextal # DECIMAL            \ 秒以六進位制數字轉換出十位數   39 HOLD                     \ 分加上 '   :SS                         \ 換算分   #S                          \ 換算度   #>                          \ 結束格式化作業   TYPE SPACE                  \ 印出結果 ;

改變貯存於 BASE 之基數,Forth 使用者可以自由地從一個基底換算至另一個基底依據當時之需要,程式設計者可從十進制換算數字至 16 進制、 8 進制、 2 進制,反之亦然。數字換算可用以下的指令改變基數來處理。

例題四、 數字轉換之基數

DECIMAL   : OCTAL         8 BASE !  ;   : HEX           16 BASE ! ;   : BINARY        2 BASE !  ;   : RADIX36       36 BASE ! ;   : RADIX19       19 BASE ! ;

特別的基數底對特殊情形有時是很有用的,例如基數 36 ,可用來壓縮字母與數字字串,來適合緊縮的記憶體空間是很方便的,因為它包含 10 個數字和26個字母。我喜歡的基數是 19 ,它在中國圍棋遊戲上非常好用。

在不同基數裡換算數字如以下的式子,你可以鍵入看看輸出的結果。

DECIMAL 12345 HEX . HEX ABCD DECIMAL U. DECIMAL 100 BINARY . BINARY 101010101010 DECIMAL .

金城 註疏:

以 36為基底來儲存飛機的班機號碼為例,可以達成兩個目的:

第一點、可以省空間,以 32位元(4個Byte)來儲存短字串。

第二點、可以省時間。電腦很容易對 32位元的整數來排序、搜尋。但對純粹的文字串則不然。這兩點使得程式好寫,速度又快,又省記憶體。

HP 計算機可以在十進制與 16 進制間換算,Forth 電腦有此功能,還有更多的字元與延伸。

例題五、電報碼

RADIX36 : Message THE. WOLVES. ARE. COMING. ; DECIMAL

如下可以知道訊息如何編碼和解碼

RADIX36 Message D. D. D. D. DECIMAL Message D. D. D. D.

注意在 Message 裡的字是附有句點的,強迫它們在基底 36 裡換算成雙倍精密度整數,在基數 36 可表最大數字是 1Z141Z3 ,如同基數 36 之雙倍精密度數,可以表示任何 6 字元字母數字之字串。

終端機輸入和輸出( FORTH 的使用者介面)

以鍵盤輸入數字和指令在螢幕上顯示結果和其他資訊,控制輸入輸出最基本的Forth 指令是:

KEY        ( -- char )       \ 接受按鍵傳回對應的 ASCII 碼 KEY?       ( -- f )          \ 按鍵後傳回真值旗標(按鍵了嗎?) EMIT       ( char -- )       \ 將堆疊(Stack) 頂端字元之 ASCII 碼                              \ 轉成文字,輸出到螢光幕 TYPE       ( addr n -- )     \ 從記憶體 addr 位置,                              \ 顯示 N 個字長度的字串螢光幕 EXPECT     ( addr n -- )     \ 鍵入 N 個字貯存在記憶體中 addr 之處                              \ 如按 Return 結束,輸入字元之                              \ 總數被存在變數 Span 中

金城 註疏:

在 FORTH 的輸入指令中,並無文字串與數字(整數、長整數)的型態差別。因為電腦的世界裡,輸出、輸入均以文字的單一型態來表示,而在計算與儲存時(指在記憶體與 CPU 中)的型態則純為二進制的表示方式 。兩者之間截然不同,所以在輸入與輸出時才需做型態轉換的工作。所以在簡潔有力的原則下,FORTH並不提供太多太複雜和太花俏的指令或函數。而僅提供有彈性的核心指令來組合調配出程式設計師所需的功能。這種「化繁為簡」的特質,在FORTH 中是統一不變的大原則,如果看官想清楚了這一點,自然就會神清氣爽,靜謐自得了。如果真的看不懂,或不會用,以我多年來教 FORTH 的經驗是您太嫩了。需要找一本好的組合語言的書來打個基礎再往下K,自然就了解了。

例題六、ASCII 字元表

大多數電腦、字元符號、數字符號、標點符號是以 8 位元的位元組表示,美國標準交換碼(ASCII)使用 32 到 127 的位元組值來代表可印出之符號,這些符號可列印在螢幕上或任何標準的印表機中,逾此範圍外,在不同的電腦裡會代表不同之符號,當超過上述之範圍 IBM-PC 會顯示繪圖符號到螢幕上,此練習是以格式化之表格來顯示正規的字元集和繪圖字元。

: Printable ( n -- n , 將不可見字元 ,轉換成空白的符號 )   DUP 14 <                        \ 7-13 是特殊格式(控制碼)用   IF      DUP 6 >                 \ 文字不能顯示出來所以           IF DROP 32 THEN         \ 用空白取代之   THEN ;   : HorizontalASCIItable( -- )   CR CR CR   5 SPACES   16 0 DO I 4 .R LOOP                \ 顯示行頭   CR   16 0 DO                            \ 做 16 列           CR I 16 * 5 .R             \ 列印左側的表值欄           16 0 DO                    \ 一列印出 16 個字元                   3 SPACES                   J 16 * I +         \ 現在文字的值                   Printable                   EMIT               \ 印出堆疊頂端的字元           LOOP                       \ LOOP 至下個字元   LOOP                               \ LOOP 至下一列   CR                                     ;                                                                                      : VerticalASCIItable( -- )   CR CR CR   5 SPACES   16 0 DO I 16 * 4 .R LOOP        \ 顯示行頭   CR   16 0 DO                         \ 做 16 列           CR I 5 .R               \ 印列頭           256 0 DO                \ 做 16 行                   3 SPACES                   J I +           \ 現在的字元的 ASCII 值                   Printable EMIT           16 +LOOP             \ 在行與行之間跳過 15 個字   LOOP                            \ 元直接印出 16個字元   CR ;

鍵入 Horizonta1ASCIItable 或 Vertica1ASCIItable 來看看結果。

會以兩種不同形式將完整的 IBM-PC 字元集顯示給你,許多繪圖字元是有趣的,因為它們允許在螢幕上繪出商業表格,這些指令是非常實用的,如果你想用特殊繪圖字元來組成匠心獨具的螢幕畫面。

金城 註疏:

這兩個小程式,仍可做進一步的最佳化,如乘以 16與向左移位的效果相同,但快得多。另外要測 n2

例題七、一封情書

這是一個很有趣的例子,錄自 Leo Brodie's 的 Starting Forth 一書。此程式允許你印一封邀請女友看電影的情書,輸入你的名字、她的名字、和她眼睛的顏色,LETTER 指令會印出這封信。

VARIABLE NAME 12 ALLOT      \ 金城 提示: VARIABLE EYES 10 ALLOT      \ 準備字串變數所需的空間,其大小為12 VARIABLE ME   12 ALLOT      \ 加上變數 Name 本身的 2Byte共 14Byte   : ENTER ( addr n -- , 接受一個長度為 n 的字串,並將之放到記憶體addr 的地方 )   2DUP BLANK                  \ 清除此記憶體為空白字串   EXPECT                      \ 等待輸入一行字串直到 N 個 ;                            \ 字元或 Enter 鍵被按下為止  : VITALS ( --, 讀取自己與對方的姓名及對方眼睛的顏色 )   CR ." Enter your name: "   ME 14 ENTER                 \ 輸入你的名字   CR ." Enter her name: "   NAME 14 TNTER               \ 輸入她的名字   CR ." Enter her eye color: "   EYES 12 ENTER               \ 輸入她眼睛的顏色 ;  : LETTER ( -- )   CR CR CR CR   ." Dear "   NAME 14 -TRALLING TYPE   ." ," CR   ." I go to heaven whenever I see your deep "   EYES 12 -TRAILING TYPE   ."  eyes. Can " CR   ." you go to the movies Friday?"   CR 30 SPACES   ." Love,"    CR 30 SPACES   ME 14 -TRAILING TYPE CR   ." P.S.  Wear something "   EYES 12 -TRAILING TYPE   ."  to show off those eyes!"   CR CR CR ;

先鍵入 VITALS 輸所有所需資料,然後打入 LETTER 你可以看見這封十分感人的信。

金城 註疏:

FORTH 的 VARIABLE(變數)不似其他語言的變數有左值(有效地址)與右值(內含值)的區別,僅單純的將一變數的有效地址放在堆疊上,供下一個指令來使用,因此反而類似其他語言的指標型態了。一個地址區域可以很自由的存放您所需要的物件,可以是整數、長整數、實數或字串。甚至可以是函數或陣列的起始位址。這種自由的調度能力,全憑使用者的規劃與組織能力來發揮,它融合了 C語言中 UNION(聯合)與無型態指標(VOID)的雙重特質,威力無窮。

在此例中,介紹幾個處理字串和記憶體的重要 Forth 指令

ALLOT       ( n -- )                \ 在變數之後的記憶體中,                                      \ 配置 N 個 bytes,建立一                                      \ 個陣列來貯存字串或數值 FILL        ( addr n char -- )      \ 從記憶體 addr 連續 Nbyte 長                                      \ ,用 Char 填入該記憶體陣列 -TRAILING   ( a n1 -- a n2 )        \ 修改字串長度 N1 ,消                                      \ 除尾部的空白字元

注意在使用 Expect 指令輸入新資料之前,字串陣列 ME.NAME,EYES 必須先清除成空白字串,因為我們不知道有多少個有效字元會輸入這些陣列。在輸入新資料後,可以使用 -TRAILING 來除去尾部的空白而取得較短的有效字串。

例題七、數字輸入

很多場合必需請使用者輸入數字符號所構成的字串,該字串被接受後,我們必須將它換算成數字,並放進資料堆疊給隨後的指令使用,轉換一個數字字串成為二進位數的基本指令是 CONVERT。

CONVERT    ( d1 addr1 -- d2 addr2 )  \ 由 addr1 處開始換算字串成雙倍                                      \ 精密度整數 然後加到 d1 ,而成                                      \ 為總和d2 留在堆疊(Stack)上而                                      \ addr2 指在字串裡第一個非數字                                      \ 處。

向使用者要求輸入一個數字的例子,如下:

: GetNumber ( -- n )   CR ." Enter a Number: "         \ 顯示訊息   PAD 20 EXPECT                   \ 取得字串   0 0 PAD 1 - CONVERT             \ 換算字串至雙倍精密度數   2DROP                           \ 留一個單整數在 ;                                 \ 堆疊(Stack)上

金城 提示:

如果將 2DROP 改成 DROP 可得到一個雙位精密度的整數。

金城 註疏:

在 FORTH 中有一特殊的執行機構稱為定義詞 CREATE DOES> ,通常 ALLOT 是使用在該處。定義詞是 FORTH 獨有的特質,是三個時 區(定義時間編譯時間執行時間)的程式設計,也是 FORTH的使用者自定的執行機構,奧妙無窮,為 FORTH 中最精彩,也最需要功力的一段。對使用者(程式設計師)而言,那才是最有挑戰性的一次考驗,是否能學會 FORTH,定義詞是任督二脈打通與否的重要關鍵。

用這個實用性指令可以寫個遊戲" 猜一個數字 "

: InitialNumber ( -- n , set up a number for the player to guess )   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                        \ 選擇一個亂數 ;                               \ 介於 0 和極限之間   : Check ( n1 --, 讓遊戲者去猜這個亂數,如果猜對了,就會結束遊戲的迴路 )   BEGIN   CR ." Please enter your guess."           GetNumber           2DUP =              \ 相等了嗎﹖           IF      2DROP       \ 清除堆疊上的兩個整數                   CR ." Correct!!!"                   EXIT           THEN           OVER >           IF      CR ." Too High!"           ELSE    CR ." Too low."           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] ( f ) UNTIL BEGIN [ 迴路中的重複部分 ] ( f ) AGAIN

以指令 Exit 跳出此無限迴圈,即跳過之後的剩餘指令直到 ";" 它的功能為結束此定義繼續下一個定義的執行。

金城 註疏:

其實 EXIT 是動了迴返堆疊的手腳,提前結束此高階定義的執行,要小心使用,否則當了機,那就弄巧成拙了。

金城 註疏:

不知諸位看官對 FORTH 的體驗為何﹖ 比 BASIC 簡單、比組合語言難、比C語言深奧、比 Pascal流暢。我認為這些答案都對,而體受自然不同。學 FORTH 一如學禪學佛,師父領進門,修行看個人,能否往昇西天極樂,就看慧根、悟性與造化了。

---------1992年5月19日子夜 ------