Author: Phil Burk (SoftSynth.com)
Translator letoh

pForth Home Page

Table of Contents

這份教學的目的在於提供一連串的簡單的實作練習讓你可以瞭解 Forth 最主要的觀念。這只是一個開始而已,當然你也可以不按照我所提供的流程。學習語言最好的方法也許就是隨著自己的好奇心任意地探索,而 Forth 尤其適合這種學習態度。

本教學是為了 PForth (基於 ANS Forth 標準的實作) 而寫的。我已經盡量使用 ANS 標準所規範的 word,但偶爾還是會出現 PForth 特有的 word。

在這份教學中,大寫字母並且縮排的文字代表的是你需要輸入的命令,但你可以以大寫或小寫的方式輸入。在每一行的最後,請按下 RETURN (或 ENTER) 鍵,這會讓 Forth 解譯剛剛輸入的命令。

Forth Syntax

Forth 的語法在程式語言中可以說是很簡單的。一言以蔽之,就是「Forth 程式是由一堆用空白分隔的 word 所組成的」。它甚至比英語還簡單!每個 word 的功能等同於其他語言中的函式,它們會依序被執行。Forth 的程式看起來就像下面這個句子:

WAKE.UP EAT.BREAKFAST WORK EAT.DINNER PLAY SLEEP

注意 WAKE.UP 是由一個點將 WAKE 和 UP 串接起來,這個點對於 Forth compiler 沒有特別意思,純粹把兩個 word 串接成一個 word 而已。 Forth 裡的 word 可以由任意字母、數字、或標點符號組合而成。因此我們可能會遇到像這樣的 word:

." #S SWAP ! @ ACCEPT . *

這些都叫做 word。就算是 $%%-GL7OP 也是一個合法的 Forth word,雖然看起來不太適當。要如何命名完全取決於程式設計師。

該是啟動 Forth 並開始練習的時候了。執行指令請參考你的 Forth 手冊。

Stack Manipulation

Forth 語言的基本概念是 stack (堆疊)。想像幾個數字堆起一個 stack,你可以在 stack 頂端增加或移除數字,也可以重新排列這些數字。在 Forth 使用了數個 stack。其中 Data Stack 是用來在 Forth word 之間傳遞資料用的,因此我們可以將注意力擺在這兒;Return Stack 則是系統內部使用的 stack。在本教程中,當我們提到 "stack" 時指的是 Data Stack。

一開始 stack 是空的。要放置一些數字,可以輸入:

23 7 9182

現在可以使用 Forth word ' . ' (念成 "dot") 來印出 stack 頂端的數字。因為是單一的點,所以很難在手冊中表示。輸入:

.

你應該可以看到最後輸入的數字 9182。Forth 有一個非常好用的 word 可以秀出目前的 stack 內容,這個指令是 .S,念成 "dot S"。這個 word 是由 . (輸出) 和 S (stack) 所組成。(在 PForth 中,如果將 TRACE-STACK 變數設為 TRUE,則會在每一行後自動印出 stack 內容) 如果你輸入;

.S

你會看到所有數字排成一列,而最右邊的數字會是 stack 頂端那一個。

注意到此時 9182 並不在 stack 中。' . ' 指令會在印出前將頂端的數字移除,而 ' .S ' 則不會更動 stack 內容。

通常我們有一個 stack diagram 的註解方法可以說明 word 對於 stack 的作用,這個 stack diagram 會用括號包起來。在 Forth 中,括號中的文字代表註解。在往後的例子中,你不需要輸入註解中的文字。當然,在寫程式的時候,我們鼓勵使用註解和 stack diagram,這可以讓你的程式碼更具可讀性。在本教程中,我們通常用粗體字表示 stack diagram。如下所示,注意別輸入底下的文字 (譯註:否則 stack 頂端的數字又會被吃掉),' . ' 的 stack diagram 會是這樣:

. ( N -- , print number on top of stack )

-- 左側的符號描述這個 word 要處理的參數,在這個例子中,N 代表任意整數;而 -- 右側一直到逗號之間則是描述當 word 結束後的 stack 參數。在本例中什麼也沒有,因為 'dot' 會吃掉傳入的整數 N。(stack 描述非必要,但對於學習別人的程式有很大的幫助。)

在逗號後面的是對這個 word 的功能描述。你會注意到在 -- 之後 N 就消失了,你可能會關心 stack 中其他數字的狀況,像是 23 與 7。不過 stack diagram 只要描述出這個 word 影響所及的部份就可以了。更詳細的內容可以在手冊中另闢章節來說明。

在不同範例之間,你可能會需要把 stack 清空。這時只要輸入 0SP (念成 zero S P),stack 就會被清空。

因為 stack 是 Forth 的主要部份,因此方便修改 stack 內容的方法是很重要的。讓我們看看其他操作 stack 的指令。輸入:

0SP .S \ That's a 'zero' 0, not an 'oh' O.
777 DUP .S

你會發現 stack 裡有兩份 777。DUP 這個 word 會把 stack 頂端的數字複製一份。當你想使用 stack 頂端的數字卻又想保留一份的時候非常有用。DUP 的 stack diagram 如下:

DUP ( n -- n n , DUPlicate top of stack )

另一個有用的指令是 SWAP。輸入:

0SP
23 7 .S
SWAP .S
SWAP .S

SWAP 的 stack diagram 如下:

SWAP ( a b -- b a , swap top two items on stack )

現在輸入:

OVER .S
OVER .S

OVER 指令會將 stack 中第二個內容複製一份,它的 stack diragram:

OVER ( a b -- a b a , copy second item on stack )

還有一個很常用的 Forth word:

DROP ( a -- , remove item from the stack )

猜猜輸入以下代碼後會看到什麼:

0SP 11 22 .S
DROP .S

另一個方便的指令是 ROT。輸入:

0SP
11 22 33 44 .S
ROT .S

所以 ROT 的 stack diagram 是

ROT ( a b c -- b c a , ROTate third item to top )

你已經學會很多重要的堆疊指令,幾乎在每個 Forth 程式都可以看到它們。不過如果你在程式中用了太多堆疊指令,我勸你最好重新檢視一下程式碼。後面會討論到藉由區域變數全域變數來避免堆疊指令被過度使用的方法。

如果你想取出 stack 中任意位置的資料,可以使用 PICK。試著輸入:

0SP
14 13 12 11 10
3 PICK . ( prints 13 )
0 PICK . ( prints 10 )
4 PICK .

PICK 會複製 stack 中的第 n 筆資料,而編號從 0 開始,因此:

0 PICK 等同於 DUP
1 PICK 等同於 OVER

PICK ( ... v3 v2 v1 v0 N -- ... v3 v2 v1 v0 vN )

(注意:Forth-79 和 FIG Forth 標準與 ANS 和 Forth-83 的標準是不同的,它們的 PICK 編號是從 1 開始。)

我已經為一些常用的堆疊指令加入 stack diagram 了,試著自己放幾個數字並呼叫它們,實驗一下它們的功能。再次提醒,括號之間的文字不需要輸入。

DROP ( n -- , remove top of stack )

?DUP ( n -- n n | 0 , duplicate only if non-zero, '|' means OR )

-ROT ( a b c -- c a b , rotate top to third position )

2SWAP ( a b c d -- c d a b , swap pairs )

2OVER ( a b c d -- a b c d a b , leapfrog pair )

2DUP ( a b -- a b a b , duplicate pair )

2DROP ( a b -- , remove pair )

NIP ( a b -- b , remove second item from stack )

TUCK ( a b -- b a b , copy top item to third position )

Problems:

Start each problem by entering:

0SP 11 22 33

Then use the stack manipulation words you have learned to end up with the following numbers on the stack:

1) 11 33 22 22
2) 22 33
3) 22 33 11 11 22
4) 11 33 22 33 11
5) 33 11 22 11 22

Answers to the problems can be found at the end of this tutorial.

Arithmetic

將 stack 中的數字搬來搬去是很有趣,但最終你還是會想做些有用的事。這一節會介紹怎麼在 Forth 中進行算術運算。

Forth 的算術運算子會作用在目前 stack 頂端的數字。如果你想對兩個數字求和,用 + (念成 plus)。輸入:

2 3 + .
2 3 + 10 + .

這種風格的算數表示法叫做 Reverse Polish Notation (RPN)。如果你有 HP 的計算機的話應該會很熟悉。在接下來的範例中,我會在註解裡加上對等的代數表示式。

其他的算術運算子是 - * /。輸入:

30 5 - . ( 25=30-5 )
30 5 / . ( 6=30/5 )
30 5 * . ( 150=30*5 )
30 5 + 7 / . \ 5=(30+5)/7

有一些運算的組合因為很常用,所以已經用組合語言實作來加速了,例如 2* 代表 2 *。當你想加快程式速度時應該使用這些它們。這些指令包括:

1+ 1- 2+ 2- 2* 2/

試著輸入:

10 1- .
7 2* 1+ . ( 15=7*2+1 )

有件事你得注意的是當你使用 / 進行整數除法運算時,餘數會直接捨去。試試:

15 5 / .
17 5 / .

對電腦上的程式語言大多是這樣。等一下會看看 /MODMOD,這兩個指令可以得到餘數。

Defining a New Word

該是用 Forth 寫個小程式的時候了。你可以用我們已經學會的 word 去定義出一個新的 word。我們來定義一個求兩數平均值的 word,這裡會用到兩個新的 word,分別是 : (冒號) 與 ; (分號)。這兩個 word 分別作為 Forth 定義的開頭和結尾。輸入:

: AVERAGE ( a b -- avg ) + 2/ ;

恭喜了,你剛寫好一個 Forth 程式。來仔細看看剛剛做了什麼事。冒號告訴 Forth 要加一個新的 word 到資料庫裡,這個資料庫通常稱為字典。緊接在冒號後的字串會當作這個 word 的名字。在這個名字後出現的任何 Forth word 都會變成新指令的一部份,一直到遇到分號為止。

測試一下這個 word:

10 20 AVERAGE . ( should print 15 )

當一個 word 被定義好以後,往後就可以用它來定義更多 word。再定義一個新的 word 來測試之前定義出來的 word:

: TEST ( --) 50 60 AVERAGE . ;
TEST

試著將你學過的 word 組合成新的 word 吧。如果你保證不會被嚇到,你也可以先看看有多少 word 可以用,只要輸入:

WORDS

不過別擔心,只有少部份會直接出現在你目前的程式中。

More Arithmetic

當你需要知道除法運算後的餘數時,/MOD 可以傳回餘數和商數,而 MOD 只會傳回餘數。

0SP
53 10 /MOD .S
0SP
7 5 MOD .S

還有另外兩個方便的指令是 MIN 與 MAX。它們接受兩個參數並分別傳回最小或最大的值。試試這段程式碼:

56 34 MAX .
56 34 MIN .
-17 0 MIN .

其他有用的 word 如下:

ABS ( n -- abs(n) , absolute value of n )

NEGATE ( n -- -n , negate value, faster then -1 * )

LSHIFT ( n c -- n<<c , left shift of n )

RSHIFT ( n c -- n>>c , logical right shift of n )

ARSHIFT ( n c -- n>>c ) , arithmetic right shift of n )

當你想快速乘上 2 的指數倍,可以使用 ARSHIFT 或 LSHIFT。右移一次的結果就相當於除以 2,這會比普通的乘除法還快。試著輸入:

: 256* 8 LSHIFT ;
3 256* .

Arithmetic Overflow

如果你使用 32 bit 的 stack 進行計算時有溢位的困擾,可以試試 */,它會產生一個 64 bit 長度的中間值。試著用下面三種方法進行相同的計算,你會發現只有使用 */ 時才會得到正確答案 5197799。

34867312 99154 * 665134 / .
34867312 665134 / 99154 * .
34867312 99154 665134 */ .

Convert Algebraic Expressions to Forth

我們在 Forth 中要如何表達一個複雜的數學運算式呢?例如 20 + (3 * 4)

要把這個式子轉換成 Forth 看得懂型式,必需重新排列為 Forth 的求值式型式 (譯註:後置運算式)。因此最後看起來會像這樣:

3 4 * 20 +

Forth 的求值是由左到右的,因此不會有模擬兩可的情形。比較下面的數學式和 Forth 求值式:(請不要輸入這些文字)

(100+50)/2 ==> 100 50 + 2/
((2*7) + (13*5)) ==> 2 7 * 13 5 * +

如果這些表示式難倒你了,試著一次輸入一個字,並用 .S 觀察 stack 的內容吧。

Problems:

Convert the following algebraic expressions to their equivalent Forth expressions. (Do not enter these because they are not Forth code!)

(12 * ( 20 - 17 ))

(1 - ( 4 * (-18) / 6) )

( 6 * 13 ) - ( 4 * 2 * 7 )

Use the words you have learned to write these new words:

SQUARE ( N -- N*N , calculate square )

DIFF.SQUARES ( A B -- A*A-B*B , difference of squares )

AVERAGE4 ( A B C D -- [A+B+C+D]/4 )

HMS>SECONDS ( HOURS MINUTES SECONDS -- TOTAL-SECONDS , convert )

Answers to the problems can be found at the end of this tutorial.

Character Input and Output

Stack 頂端的數字可以代表任何東西,也許是地球上的藍鯨個數、也許是你的體重。它也可能是一個 ASCII 字元。試著輸入下面的範例:

72 EMIT 105 EMIT

你應該會看到 OK 之前出現了 "Hi"。其中 72 代表 ASCII 中的 'H',而 105 則是 'i'。EMIT 會在 stack 中取出一個數,並輸出成字元。如果你想找到任一字元的 ASCII 值,可以用 CHAR 這個指令。輸入:

CHAR W .
CHAR % DUP . EMIT
CHAR A DUP .
32 + EMIT

There is an ASCII chart in the back of this manual for a complete character list.

注意這個 CHAR 有點不太一樣,因為他的輸入並不是從 stack 來的,而是接在後面。在 stack diagram 中我們把輸入參數放在角括號中來表示。

CHAR ( <char> -- char , get ASCII value of a character )

用 EMIT 輸出字串會很累,幸好有一個比較好的方法。試試:

: TOFU ." Yummy bean curd!" ;
TOFU

." (念成 "dot quote"),會把下一個雙引號之前的所有東西輸出到螢幕上。注意雙引號後要留一個空白。如果要換行的話,可以用 CR 輸出一個換行字元。

: SPROUTS ." Miniature vegetables." ;
: MENU
	CR TOFU CR SPROUTS CR
;
MENU

你也可以用 SPACE 送出一個空白字元,或用 SPACES 送出一堆空白字元。

CR TOFU SPROUTS
CR TOFU SPACE SPROUTS
CR 10 SPACES TOFU CR 20 SPACES SPROUTS

至於字元輸入,Forth 使用 KEY 來達成。 KEY 會等待使用者按下一個鍵,然後將輸入的值放進 stack。

: TESTKEY ( -- )
	." Hit a key: " KEY CR
	." That = " . CR
;
TESTKEY

(Note: 在某些具輸入緩衝機制的系統裡,你需要在輸入後按下 ENTER 鍵)

EMIT ( char -- , output character )

KEY ( -- char , input character )

SPACE ( -- , output a space )

SPACES ( n -- , output n spaces )

CHAR ( <char> -- char , convert to ASCII )

CR ( -- , start new line , carriage return )

." ( -- , output " delimited text )

Compiling from Files

PForth 可以從文字檔裡讀入程式,因此你可以使用任意編輯器來撰寫程式。

Sample Program

將以下的程式碼輸入文字檔中。

\ Sample Forth Code
\ Author: your name

: SQUARE ( n -- n*n , square number )
	DUP *
;

: TEST.SQUARE ( -- )
	CR ." 7 squared = "
	7 SQUARE . CR
;

現在將檔案存入磁碟中。

\ 字元後面的文字會被視為註解,就像是 BASIC 中的 REM 或 C 裡的 /*---*/。當然,括號中的文字也會被視為註解。

Using INCLUDE

"INCLUDE" 在 Forth 中表示編譯該檔案。

如果你存了一個檔案叫 WORK:SAMPLE,可以輸入:

INCLUDE SAMPLE.FTH

Forth 會編譯這個檔案並告訴你新增了多少位元組到字典中。要測試的話可以輸入:

TEST.SQUARE

你定義的 SQUARE 與 TEST.SQUARE 已經加進字典中了。現在我們可以做一件程式語言中不常見的事,我們可以用 FORGET 讓 Forth 忘掉特定指令。

FORGET SQUARE

這會從字典中移除掉 SQUARE 和與它相關的所有指令,像是 TEST.SQUARE。如果你現在試著執行 TEST.SQUARE,Forth 會告訴你找不到這個 word。

現在改變一下檔案並重新載入吧。回到文字編輯器中,並做兩項變更:(1) 把 TEST.SQUARE 中的 7 改成 15 (2) 在 SQUARE 的定義之前加上一行:

ANEW TASK-SAMPLE.FTH

現在可以存檔並回到 Forth 視窗。

你可能會好奇 ANEW 代表什麼。 ANEW 通常放在檔案的開頭,它會在字典中產生一個標記用的 word,這個 word 通常會加上 "TASK-" 作為前導字,後面再加上檔名。當你重新載入一個檔案,ANEW 會自動忘掉舊的程式碼,這樣可以不用在每次重新載入之前還要手動做 FORGET。如果沒有忘掉舊的程式碼而不斷加入,字典最後可能會被塞滿。

如果你有一個需要許多檔案的大型專案,你可以準備一個檔案來載入所有你要的檔案。有時候你只需要某些可能已經載入過的程式碼,這時可以用 INCLUDE?,在字典中找不到時才會載入。在下一個範例中,我假設 WORK: 中有一個檔案叫做 SAMPLE。如果檔名不同請替換為正確的檔名。請輸入:

FORGET TASK-SAMPLE.FTH
INCLUDE? SQUARE WORK:SAMPLE
INCLUDE? SQUARE WORK:SAMPLE

只有第一個 INCLUDE? 會執行載入檔案的動作。

Variables

跟其他編譯式語言相比,Forth 並不常大量使用變數,因為數值通常存在 stack 裡了。當然,也是會出現需要變數的時候。要建立一個變數,使用 VARIABLE 如下:

VARIABLE MY-VAR

這會建立一個叫做 MY-VAR 的變數,在記憶體中會保留一塊空間用來儲存 32-bit 的值。因為 VARIABLE 會在字典中建立新的 word,所以被視為「定義指令」。現在輸入:

MY-VAR .

你看到的數字是這個變數在記憶體中的位址。要將資料儲存到記憶體中可以使用 ! (念成 "store")。雖然它看起來像是個驚嘆號,但對 Forth 編程員來說它是將 32-bit 資料寫入記憶體的指令。要讀出特定記憶體址所儲存的值,可以用 @ (念成 "fetch")。試著輸入以下程式:

513 MY-VAR !
MY-VAR @ .

這會將變數 MY-VAR 的值設為 513。然後讀出其值並輸出。這些 word 的 stack diagram 列在下面:

@ ( address -- value , FETCH value FROM address in memory )

! ( value address -- , STORE value TO address in memory )

VARIABLE ( <name> -- , define a 4 byte memory storage location)

有一個方便用來檢查變數值的 word 叫做 ? (念成 "question")。試試:

MY-VAR ?

如果 ? 未定義,那我們可以自行定義如下:

: ? ( address -- , look at variable )
	@ .
;

想像正在寫一個遊戲,而你想記錄最高分,你可以將最高分數保存在一個變數中。當你收到一個新分數,你可以拿它和目前最高分比較,參考前一節的介紹將這些程式碼寫到檔案中:

VARIABLE HIGH-SCORE

: REPORT.SCORE ( score -- , print out score )
	DUP CR ." Your Score = " . CR
	HIGH-SCORE @ MAX ( calculate new high )
	DUP ." Highest Score = " . CR
	HIGH-SCORE ! ( update variable )
;

將檔案存入磁碟,並用 INCLUDE 來編譯它。測試你的 word 如下:

123 REPORT.SCORE
9845 REPORT.SCORE
534 REPORT.SCORE

Forth 中的 @ 和 ! 會作用在 32-bit 的資料上。有些 Forth 是 16-bit 的 Forth,它們會提取 (fetch) 或儲存 (store) 16-bit 的資料。Forth 中也有一些 word 可以處理 8-bit 和 16-bit 的資料存取。C@C! 通常作用在 b-bit 的字元上。'C' 代表的是 "Character",因為 ASCII 字元為 8-bit 數字。另外,16-bit "Word" 可以使用 W@W! 來處理。

另一個有用的 word 是 +! (念成 "plus sotre")。它會把一個值加到記憶體中的 32-bit 資料。試試:

20 MY-VAR !
5 MY-VAR +!
MY-VAR @ .

Forth 也提供一些類似 VARIABLE 的 word。例如 VALUEARRAY。也可以看一下 "local variables" 這一節,區域變數只會在 Forth word 執行時存在於 stack 中。

關於存取記憶體的警告: 現在你已經學了夠多可以讓 Forth 變得危險的東西了,電腦的操作是基於在正確的記憶體位址放置正確的數值,而現在你已經知道如何在任意記憶體位址寫入新值了。因為記憶體位置也只是一個數字,也許你可以 (但不應該) 輸入:

73 253000 ! ( Do NOT do this. )

253000 會被視為一個記憶體位址,而你可以將那個位置的值設為 73。我不知道這會發生什麼事,也許不會有事,這就像你拿著來福槍對著公寓的牆開槍,你無法預測你會打到什麼或射中誰。因為你和這個作業系統裡的其他程式一起共享記憶體,你可以很容易地讓電腦發生奇怪現象,甚至當機。不過不用太擔心,電腦當機不像撞毀一台車,並不會傷害電腦。你只要重開機就好。最糟的情況是如果你剛好正在寫資料到磁碟,你可能會遺失檔案。這就是為什麼我們需要備份。這個問題不只在 Forth,在很多強大的語言中也會發生。在 BASIC 中比較不會,因為 BASIC 保護了很多動作,包含撰寫強力的程式。

另一個會造成問題的是執行所謂的 "奇位址記憶體存取"。68000 處理器會將 word 與 longword、16 與 32-bit 數字排在偶位址。如果你執行 @ 或 !、或 W@ 或 W! 存取一個奇位址,68000 處理器會引發 exception 並試著異常中止 (abort)。

Forth 可以透過補捉 exception 而避免這種情況,讓你可以回到 OK 提示訊息。如果你真的需要在奇位址存取資料,可以試試 ODD@ODD!。另外 C@ 與 C! 可以同時作用在奇位址和偶位址。

Constants

如果你的程式中有一個數字會出現很多次,我們建議你將它定義為一個常數,只要輸入:

128 CONSTANT MAX_CHARS
MAX_CHARS .

我們只是定義了一個叫做 MAX_CHARS 的 word,並傳回一個值到 stack。除非修改程式並重新編譯,否則常數的值是不能變更的。使用 CONSTANT 可以增加程式的可讀性並減少 bug。想像當你在程式中頻繁使用一個數字 128,就說是 8 次好了。隨後你決定將這個數改為 256。如果你直接在整個程式碼中替換掉這個數,那你可能會改到本來不想變更的。如果你手動修改,也可能會有漏網之魚,尤其是當你的程式分散在多個檔案時。使用 CONSTANT 可以讓這個動作輕鬆一點,而且最後的程式跟直接使用數字一樣又快又小。我建議將使用到的任何數字都定義為常數。

Logical Operators

接下來兩節要來關心一下決策。這一節處理的是回答一個像是 "這個值是否太大?" 或 "這個猜測跟答案接不接近?" 這類問題。這類問題的答案不是 TRUE 就是 FALSE。Forth 用 0 代表 FALSE,而 -1 代表 TRUE。因為 TRUE 和 FALSE 被定義為常數,所以都是大寫的。試試:

23 71 = .
18 18 = .

你會注意到第一行輸出 0 或 FALSE;而第二行輸出 -1 或 TRUE。其中等號在 Forth 中是一個問句,而不是一個敘述。它詢問 stack 最上面兩筆資料是不是相等的,而不是將它們設為一樣的。你也可以問其他問題,例如:

23 198 < .
23 198 > .
254 15 > .

在加州,可以喝酒的年齡是 21。你可以幫酒保寫一個簡單的 word 來判斷了。輸入:

: DRINK? ( age -- flag , can this person drink? )
	20 >
;

20 DRINK? .
21 DRINK? .
43 DRINK? .

其中 stack diagram 中的 FLAG 代表一個邏輯值。

Forth 提供幾個特別的 word 可以和 0 比較:0=0>0<。用 0> 會比分別使用 0 和 > 的速度還快。輸入:

23 0> . ( print -1 )
-23 0> . ( print 0 )
23 0= . ( print 0 )

對於更複雜的決策,你可以使用布林運算子 ORAND,以及 NOT。當 stack 最上面兩筆資料的其中一個或兩個同時為 true,OR 會傳回 TRUE。

TRUE TRUE OR .
TRUE FALSE OR .
FALSE FALSE OR .

AND 只有在兩者同時為 true 時才會傳回 TRUE。

TRUE TRUE AND .
TRUE FALSE AND .

NOT 會反轉 stack 最上面的 flag。輸入:

TRUE .
TRUE NOT .

你也可以將邏輯運算子組合使用:

56 3 > 56 123 < AND .
23 45 = 23 23 = OR .

這裡列出其中一些 word 的 stack diagram。更完整的內容請參考詞彙表

< ( a b -- flag , flag is true if A is less than B )

> ( a b -- flag , flag is true if A is greater than B )

= ( a b -- flag , flag is true if A is equal to B )

0= ( a -- flag , true if a equals zero )

OR ( a b -- a||b , perform logical OR of bits in A and B )

AND ( a b -- a&b , perform logical AND of bits in A and B )

NOT ( flag -- opposite-flag , true if false, false if true )

Problems:

1) Write a word called LOWERCASE? that returns TRUE if the number on top of the stack is an ASCII lowercase character. An ASCII 'a' is 97 . An ASCII 'z' is 122 . Test using the characters " A ` a q z { ".

CHAR A LOWERCASE? . ( should print 0 )
CHAR a LOWERCASE? . ( should print -1 )

Answers to the problems can be found at the end of this tutorial.

Conditionals - IF ELSE THEN CASE

你現在可以用前一節學到的 TRUE 和 FALSE 作為 flag 了。"流程控制" 指令會從 stack 取出一個 flag,並根據其值決定是否執行分支跳躍 (branch)。輸入以下程式碼:

: .L ( flag -- , print logical value )
	IF ." True value on stack!"
	ELSE ." False value on stack!"
	THEN
;

0 .L
FALSE .L
TRUE .L
23 7 < .L

當 stack 裡的值為 TRUE 時,你可以看到第一個部份被執行;如果是 FALSE,則第一個部份會被略過,並執行第二部份。如果你輸入以下程式碼,你會發現有趣的事:

23 .L

stack 上的值會被視為 true。流程控制指令會將 0 以外的值都視為 TRUE。

ELSEIF...THEN 結構中是可選的。試試這個:

: BIGBUCKS? ( ammount -- )
	1000 >
	IF ." That's TOO expensive!"
	THEN
;

531 BIGBUCKS?
1021 BIGBUCKS?

許多 Forth 也支援 CASE,類似 'C' 裡的 switch()。輸入:

: TESTCASE ( N -- , respond appropriately )
	CASE
		0 OF ." Just a zero!" ENDOF
		1 OF ." All is ONE!" ENDOF
		2 OF WORDS ENDOF
		DUP . ." Invalid Input!"
	ENDCASE CR
;

0 TESTCASE
1 TESTCASE
5 TESTCASE

關於更多 CASE 資訊可以查詢詞彙表

Problems:

1) Write a word called DEDUCT that subtracts a value from a variable containing your checking account balance. Assume the balance is in dollars. Print the balance. Print a warning if the balance is negative.

VARIABLE ACCOUNT

: DEDUCT ( n -- , subtract N from balance )
	????????????????????????????????? ( you fill this in )
;

300 ACCOUNT ! ( initial funds )
40 DEDUCT ( prints 260 )
200 DEDUCT ( print 60 )
100 DEDUCT ( print -40 and give warning! )

Answers to the problems can be found at the end of this tutorial.

Loops

另外一對有用的 word 是 BEGIN...UNTIL。這個指令是用來循環直到給定的條件為 true 為止。試試這個:

: COUNTDOWN  ( N -- )
	BEGIN
		DUP . CR       ( print number on top of stack )
		1-  DUP  0<    ( loop until we go negative )
	UNTIL
;

16 COUNTDOWN

這個 word 會從 N 倒數至 0。

如果你知道迴圈執行的次數,你可以使用 DO...LOOP 結構。輸入:

: SPELL
	." ba"
	4 0 DO
		." na"
	LOOP
;

這會印出 "ba",後面跟著四個 "na"。在 stack 中,結束的值放在起始值之前,小心不要顛倒了。設計為這個順序的理由是為了以後可以方便把迴圈次數當做參數傳進來。考慮底下一個畫出字元圖的 word:

: PLOT# ( n -- )
	0 DO
		[CHAR] - EMIT
	LOOP CR
;

CR 9 PLOT# 37 PLOT#

如果你需要存取迴圈的計數器,可以用 I。底下是一個簡單的 word,用來印出數字和對應的 ASCII 字元。

: .ASCII ( end start -- , dump characters )
	DO
		CR I . I EMIT
	LOOP CR
;

80 64 .ASCII

如果想在 DO LOOP 結束前就離開,可以使用 LEAVE。輸入:

: TEST.LEAVE  ( -- , show use of leave )
	100 0
	DO
		I . CR  \ print loop index
		I 20 >  \ is I over 20
		IF
			LEAVE
		THEN
	LOOP
;
TEST.LEAVE  \ will print 0 to 20

請參考手冊學習 +LOOPRETURN 的用法。(FIXME)

另一個有用的迴圈結構是 BEGIN WHILE REPEAT,它可以讓你在每一次執行動作之前都做一次測試。 WHILE 會測試 stack 中的 flag,只要是 true 就會繼續執行下去。

: SUM.OF.N ( N -- SUM[N] , calculate sum of N integers )
	0  \ starting value of SUM
	BEGIN
		OVER 0>   \ Is N greater than zero?
	WHILE
		OVER +  \ add N to sum
		SWAP 1- SWAP  \ decrement N
	REPEAT
	SWAP DROP  \ get rid on N
;

4 SUM.OF.N    \ prints 10   ( 1+2+3+4 )

Problems:

1) Rewrite SUM.OF.N using a DO LOOP.

2) Rewrite SUM.OF.N using BEGIN UNTIL.

3) For bonus points, write SUM.OF.N without using any looping or conditional construct!

Answers to the problems can be found at the end of this tutorial.

Text Input and Output

你已經在前面學會怎麼做單一字元的 I/O 了,而這一節的重點是使用字串。你可以透過 S" 在程式中使用字串,注意 S" 的後面要有一個空白;字串的結尾用 "。試試:

: TEST S" Hello world!" ;
TEST .S

這個 TEST 會在 stack 裡留下兩個數字:第一個數字是第一個字元的位址;第二個數字是字串中的字元個數。你可以用下面的方法印出字串中的字元:

TEST DROP       \ get rid of number of characters
DUP C@ EMIT     \ prints first character, 'H'
CHAR+ DUP C@ EMIT  \ prints second character, 'e'
\ and so on

其中的 CHAR+ 會前進到下一個字元的位址。你可以用 TYPE 印出整個字串。

TEST  TYPE
TEST  2/  TYPE   \ print half of string

如果可以用一個位址就代表字串,而不必傳入字串的長度該有多好。C 會在字串最後放一個 0 做為結尾,不過 Forth 還有另外的方法。有一種字串是在第一個位元組存入字元個數,接著才是字串本身,這種字串可以用 C" (念成 "c quote") 來建立。

: T2 C" Greetings Fred" ;
T2 .

顯示出來的是字串的啟始位址,而這個位址的值是 1 byte 的字串長度。現在輸入:

T2 C@ .

你應該可以看到印出 14。C@ 會根據 stack 中的位址取出 1 byte 資料。你也可以把這種字串用 COUNT 轉換成起始位址和字串長度的格式。

T2 COUNT .S
TYPE

COUNT 這個指令會把取出字串長度和字串的起始位址,但它只能處理長度小於 256 的字串,因為儲存長度的空間最大只能存 255。不過 TYPE 可以處理更長的字串,因為它參考的是 stack 裡的字串長度值。這些指令的 stack diagram 如下:

CHAR+ ( address -- address' , add the size of one character )

COUNT ( $addr -- addr #bytes , extract string information )

TYPE ( addr #bytes -- , output characters at addr )

其中 $addr 代表儲存長度值的位址,而 $ 號通常代表這個 word 跟字串處理有關。

你可以簡單地用 ACCEPT 來輸入字串。(你可能會將接下來的範例保存在檔案中,因為實在太方便了。) ACCEPT 從鍵盤接收字元,並儲存在指定的位址,直到超過長度限制,或是輸入 EOL 字元。你可以設計一個 word 負責處理文字輸入:

: INPUT$ ( -- $addr )
	PAD  1+ ( leave room for byte count )
	127 ACCEPT ( recieve a maximum of 127 chars )
	PAD C! ( set byte count )
	PAD ( return address of string )
;

INPUT$ COUNT TYPE

這段程式會接受一段字串輸入並印出來。你可以在一個寫信程式中使用它。

: FORM.LETTER ( -- )
	." Enter customer's name." CR
	INPUT$
	CR ." Dear " DUP COUNT TYPE CR
	." Your cup that says " COUNT TYPE
	." is in the mail!" CR
;

ACCEPT ( addr maxbytes -- numbytes , input text, save at address )

你也可以利用 INPUT$ 寫一個讀取數字用的 word:

: INPUT# ( -- N true | false )
	INPUT$ ( get string )
	NUMBER? ( convert to a string if valid )
	IF DROP TRUE ( get rid of high cell )
	ELSE FALSE
	THEN
;

這個 word 會傳回一個單精度的數字和一個 TRUE,或只是傳回 FALSE。NUMBER? 會在輸入字串內含有效數字時傳回一個倍精度數字。因為倍精度數字是 64-bit,所以我們丟掉最上面的 32 bit,得到一個 32 bit 的單精度數字。

Changing Numeric Base

我們的數字系統是十進位 (以 10 為基底)。這代表像 527 這樣的數字相當於 (5*100 + 2*10 + 7*1)。以 10 作為數字的基底完全是隨便訂定的,可能是因為人有 10 隻手指吧。巴比倫人則是以 60 為基底,也就是一小時有 60 分鐘的概念。電腦硬體以 2 為基底 (二進位)。電腦中像 1101 這樣的數字相當於 (1*8 + 1*4 + 0*2 + 1*1),加總起來會得到 8+4+1=13。10 在二進位中代表 (1*2 + 0*1),也就是 2。同樣地,以 N 為基底時的 10 就會是 N。

因為 Forth 可以在任意基底下工作,所以可以輕鬆悠遊於不同的數值基底。試著輸入下列程式碼:

DECIMAL 6 BINARY .
1 1 + .
1101 DECIMAL .

另一個有用的基底是十六進位,以 16 為基底。因為我們的數字只有 0 到 9,因此超過 10 的基底不容易表示。以十六進位來說,我們用字母 A 到 F 來代表 10 到 15。因此十六進位的 3E7 相當於 (3*256 + 14*16 + 7*1)。

DECIMAL 12 HEX .  \ print C
DECIMAL 12 256 *   7 16 * +  10 + .S
DUP BINARY .
HEX .

有一個叫做 BASE 的變數用來記錄目前的數值基底。HEXDECIMALBINARY 就是在改變這個變數的值。你也可以改成任何你想要的值:

7 BASE !
6 1 + .
BASE @ . \ surprise!

現在你的基底變成 7 了,不過當你載入或印出 BASE 時,只會顯示 10,因為 7 在七進位表示成 10。

PForth 定義了 .HEX,可以在任意基底下都用十六進位表示法來印出一個數字。

DECIMAL 14 .HEX

你也可以為任一個基底定義出像是 .HEX 的指令,只要在印出數字時暫時改掉基底,最後再還原就可以了。試試下面的 word:

: .BIN ( N -- , print N in Binary )
	BASE @ ( save current base )
	2 BASE ! ( set to binary )
	SWAP . ( print number )
	BASE ! ( restore base )
;

DECIMAL
22 .BIN
22 .

Answers to Problems

If your answer doesn't exactly match these but it works, don't fret. In Forth, there are usually many ways to the same thing.

Stack Manipulations

1) SWAP DUP
2) ROT DROP
3) ROT DUP 3 PICK
4) SWAP OVER 3 PICK
5) -ROT 2DUP

Arithmetic

(12 * (20 - 17)) ==> 20 17 - 12 *
(1 - (4 * (-18) / 6)) ==> 1 4 -18 * 6 / -
(6 * 13) - (4 * 2 * 7) ==> 6 13 * 4 2 * 7 * -

: SQUARE ( N -- N*N ) 
	DUP *
;

: DIFF.SQUARES ( A B -- A*A-B*B )
SWAP SQUARE 
SWAP SQUARE - 
;

: AVERAGE4 ( A B C D -- [A+B+C+D]/4 )
	+ + + ( add'em up )
	-2 ashift ( divide by four the fast way, or 4 / )
;

: HMS>SECONDS ( HOURS MINUTES SECONDS -- TOTAL-SECONDS )
		-ROT SWAP ( -- seconds minutes hours )
		60 * + ( -- seconds total-minutes )
		60 * + ( -- seconds )
;

Logical Operators

: LOWERCASE? ( CHAR -- FLAG , true if lowercase )
    DUP 123 <
    SWAP 96 > AND
;

Conditionals

: DEDUCT ( n -- , subtract from account )
    ACCOUNT @ ( -- n acc 
    SWAP - DUP ACCOUNT ! ( -- acc' , update variable )
    ." Balance = $" DUP . CR ( -- acc' )
    0< ( are we broke? )
    IF ." Warning!! Your account is overdrawn!" CR
    THEN
;

Loops

: SUM.OF.N.1 ( N -- SUM[N] )
    0 SWAP \ starting value of SUM
    1+ 0 \ set indices for DO LOOP
    ?DO \ safer than DO if N=0
        I +
    LOOP
;

: SUM.OF.N.2 ( N -- SUM[N] )
    0 \ starting value of SUM
    BEGIN ( -- N' SUM )
        OVER +
        SWAP 1- SWAP
        OVER 0<
    UNTIL
    SWAP DROP
;

: SUM.OF.N.3 ( NUM -- SUM[N] , Gauss' method )
    DUP 1+   \ SUM(N) = N*(N+1)/2
    * 2/
;

Back to pForth Home Page