Forth 语言简明教程 |
作者 Richard E. Haskell |
| 编译 赵宇 张文翠 |
第四课 Forth 判断
4.1 分支指令和循环
所有的计算机都必须有某种办法来产生条件分支(IF …… THEN)和实现循环, Forth 使用下面这些“良好定义”的结构:
IF ... ELSE ... THEN
DO ... LOOP
BEGIN ... UNTIL
BEGIN ... WHILE ... REPEAT
BEGIN ... AGAIN
这些语句的工作方式与它们在其它语言所表现的不同。字 IF、UNTIL 和 WHILE 运行时希望堆栈上有 true/false 标志,一个 false 标志的值是 0 ,一个 true 标志的值是 -1.
F-PC 定义两个常数
-1 CONSTANT TRUE
0 CONSTANT FALSE
标志可以通过各种方式产生,但通常的方式都是使用某些条件表达式,它们把标志留在堆栈上。
我们先来看看 Forth 的条件字然后给出分支和循环语句的一些例子:
4.2 条件字和true/false 标志
下面这些 Forth 条件字产生 true/false 标志 :
< ( n1 n2 -- f ) ( "less-than" )
如果 n1 小于 n2 则标志 f 为真
> ( n1 n2 -- f ) ( "greater-than" )
如果 n1 大于 n2 则标志 f 为真
= ( n1 n2 -- f ) ( "equals" )
如果 n1 等于 n2 则标志 f 为真
<> ( n1 n2 -- f ) ( "not-equal" )
如果 n1 小等于 n2 则标志 f 为真
<= ( n1 n2 -- f ) ( "less-than or equal" )
如果 n1 小于或者等于 n2 则标志 f 为真
>= ( n1 n2 -- f ) ( "greater-than or equal" )
如果 n1 大于或者等于 n2 则标志 f 为真
0< ( n -- f ) ( "zero-less" )
如果 n 小于 0 (负数)则标志 f 为真
0> ( n -- f ) ( "zero-greater" )
如果 n 大于 0 (正数)则标志 f 为真
0= ( n -- f ) ( "zero-equals" )
如果 n 等于 0 则标志 f 为真
0<> ( n -- f ) ( "zero-not-equal" )
如果 n 小等于 0 则标志 f 为真
0<= ( n -- f ) ( "zero-less-than or equal" )
如果 n 小于或者等于 0 则标志 f 为真
0>= ( n -- f ) ( "zero-greater-than or equal" )
如果 n 大于或者等于 0 则标志 f 为真
以下条件字比较堆栈上的两个无符号数
U< ( u1 u2 -- f ) ( "U-less-than" )
如果 u1 小于 u2 则标志 f 为真。
U> ( u1 u2 -- f ) ( "U-greater-than" )
如果 u1 大于 u2 则标志 f 为真。
U<= ( u1 u2 -- f ) ( "U-less-than or equal" )
如果 u1 小于等于 u2 则标志 f 为真。
U>= ( u1 u2 -- f ) ( "U-greater-than or equal" )
如果 u1 大于等于 u2 则标志 f 为真。
4.3 Forth 逻辑操作
有些 Forth 有一个字 NOT ,它可以反转堆栈上的标志值。在 F-PC 系统中,字 NOT 执行一个堆栈上的字的 1 补码。只要 TRUE 是 -1 ( 16 进制 FFFF ) , 则 NOT TRUE 就是 FALSE 。
你必须小心的是:由于任何的非 0 值都会作为 TRUE 对待,而除 16 进制 FFFF 外的任何值 进行 1 的补码运算之后都不会产生 0 ( FALSE )。你可以使用比较字 0= 来产生标志。
除了逻辑操作符 NOT 外, Forth 也支持下列的双目逻辑操作符:
AND ( n1 n2 -- and )
在堆栈上留下 n1 AND n2 这是一个按位与运算,例如,如果你输入
255 15 AND ( mask lower 4 bits )
在栈顶将留下值 15
OR ( n1 n2 -- or )
在堆栈上留下 n1 OR n2 ,这是按位运算,例如如果你输入:
9 3 OR
将在堆栈上留下值 11
XOR ( n1 n2 -- xor )
在堆栈上留下 n1 XOR n2 ,这是按位运算,例如如果你输入
240 255 XOR ( Hex F0 XOR FF = 0F )
将在栈顶留下值 15
4.4 IF 语句
Forth 的 IF 语句与其它语言的不同。你所熟悉的一个典型 IF ... THEN ... ELSE 语句大概是这样的:
IF <cond> THEN
<true statements>
ELSE
<false statements>
而在 Forth 中, IF 语句是这样的:
<cond> IF <true statements>
ELSE <false statements>
THEN
注意,在 IF 字执行的时候, true/false 标志必须在栈顶。如果栈顶上是一个真标志,则 <true statements> 被执行,如果栈顶上是一个假标志,则 <false statements> 被执行。在 <true statements> 或者 <false statements> 被执行之后,字 THEN 后面的语句被执行。 ELSE 子句是可选的
IF 字必须在冒号定义内使用,作为一个例子,定义下列字:
: iftest ( f -- )
IF CR ." true statements"
THEN CR ." next statements" ;
: if.else.test ( f -- )
IF CR ." true statements"
ELSE CR ." false statements"
THEN CR ." next statements" ;
然后你输入:
TRUE iftest
FALSE iftest
TRUE if.else.test
FALSE if.else.test
4.5 DO 循环
Forth 的 DO 循环必须在冒号定义中使用,为了说明它是如何工作的,定义下列字:
: dotest ( limit ix -- )
DO
I .
LOOP ;
然后你输入:
5 0 dotest
值 0 1 2 3 4 将打印到屏幕上,试一下。
DO 循环是这样工作的: 字 DO 从参数栈顶上取两个值并把它们放到返回栈上。这时这两个值已经不在参数栈上了。字 LOOP 将索引值加 1 并把结果与限值进行比较。如果增量之后的索引值小于限值,则分支到 DO 下面的字。如果增量之后的索引值等于限值,则分支到 LOOP 之后的字。我们将在第九课中仔细研究 DO 循环是如何实现的。
Forth 字 I 把索引值从返回栈复制到参数栈顶。因此上面的例子可以解释如下:
5 \ 5
0 \ 5 0
DO
I \ ix ( ix = 0,1,2,3,4)
.
LOOP
注意限值必须比你希望的最大索引值还要大 1 ,例如:
11 1 DO
I .
LOOP
将打印出值 1 2 3 4 5 6 7 8 9 10
字 +LOOP
Forth 的 DO 循环索引值可以是 1 以外的其它值,这时需要用字 +LOOP 来替代 LOOP 。 可以通过下列的例子来看工作情况:
: looptest ( limit ix -- )
DO
I .
2 +LOOP ;
然后你输入
5 0 looptest
值 0 2 4 将打印出来。
字 +LOOP 从参数栈顶取得值并把它加到返回栈的索引值中,之后的动作与 LOOP 一样,只要是增量后的索引值小于限值,它就分支到 DO 后面的语句(如果增量值为正)。如果增量的值为负,则当增量的索引值小于限值时就退出循环。例如你可以输入
: neglooptest ( limit ix -- )
DO
I .
-1 +LOOP ;
然后输入
0 10 neglooptest
值 10 9 8 7 6 5 4 3 2 1 0 将打印在屏幕上。
嵌套循环 – 字 J
Forth 的循环可以嵌套。这时就要有两对索引/限值被移到返回栈上。字 I 把内层循环的索引值从返回栈复制到参数栈上,字 J 把外层循环的索引值从返回栈复制到参数栈上。
作为一个嵌套循环的例子,定义下面的字:
: 1.to.9 ( -- )
8 1 DO
CR
3 0 DO
J I + .
LOOP
3 +LOOP ;
如果你执行这个字,下面的内容将打印到屏幕上:
1 2 3
4 5 6
7 8 9
你明白这是为什么吗?
嵌套的循环在 Forth 中比在其它高级语言中用得少。更好的办法是定义一个小的字,它只包含一个 DO 循环,然后在另外的循环中调用这个字。
字 LEAVE
Forth 字 LEAVE 可以用在 DO 循环中以退出循环。它通常是用在 DO 循环的 IF 语句中。字 LEAVE 可以立即通出 DO 循环( LOOP 之后那个字的地址作为第三个字保存在返回栈上)。还有一个相关的字 ?LEAVE (flag --) 在栈顶为真时退出 DO 循环,这就不用使用 IF 语句了。
作为一个例子,假设你想定义一个字 find.n ,它查找一个指定值在字表中的索引值(也就是这个值在表中的位置),如果找到则返回真,否则在栈顶返回假。首先用 Forth 语句构造表:
CREATE table 50 , 75 , 110 , 135 , 150 , 300 , 600 ,
将在代码段中创建表

表中值的数目是 imax ( 在我们的情况下是 7). 要查找的值是 n. 在被执行时这些值必须在堆栈是,下面是 find.n 的定义:
: find.n ( imax n -- ff | index tf )
0 SWAP ROT \ 0 n imax
0 DO \ 0 n
DUP I table \ 0 n n ix pfa
SWAP 2* + \ 0 n n pfa+2*ix
@ = \ 0 n f
IF \ 0 n
DROP I TRUE \ 0 ix tf
ROT LEAVE \ ix tf 0
THEN
LOOP \ 0 n
DROP ; \ 0 | ix tf
研究这个定义一直到你明白它是如何工作的时候为止。通常情况下,在使用 DO 循环时的堆栈情况在执行完 DO 时和执行室外 LOOP 时是一样的,你常常需要使用 DUP 在 DO 循环中复制值并在离开循环时用 DROP 去除一些值。特别注意 ROT 在 LEAVE 之前使用以建立堆栈以使得真标志留在堆栈顶。
4.6 UNTIL 循环
Forth 的 UNTIL 循环必须用于冒号定义中, UNTIL 循环的格式是:
BEGIN <Forth statements> <flag> UNTIL
如果 <flag> 是假,程序分支到 BEGIN 之后的字。如果 <flag> 是真 , 程序执行 UNTIL 之后的字。
下面的两个 Forth 字能够检测和读出键盘的输入
KEY? ( -- flag )
如果键盘有键按下,返回真标志。
KEY ( -- char )
等待键盘按下并将 ASCII 码返回到栈顶。
F-PC 字 EMIT ( char -- )
将在屏幕上打印栈顶 ASCII 码对应的字符。
定义下面的字
: dowrite ( -- )
BEGIN
KEY \ char
DUP EMIT \ print on screen
13 = \ if equal to CR
UNTIL ; \ quit
执行这个字将在屏幕上打印出所有你输入的字符,直到你打入了 <Enter> 键 (ASCII 码 = 13). 注意 UNTIL 从堆栈上移去标志。
4.7 WHILE 循环
Forth 的 WHILE 循环必须在冒号定义中使用, WHILE 循环的格式是
BEGIN <words> <flag> WHILE <words> REPEAT
如果 <flag> 是真,在字 WHILE 和 REPEAT 之间的字被执行,然后再分支到 BEGIN 后面的字。如果 <flag> 是假,程序分支到 REPEAT 之后的字。
作为一个例子,考虑下面求 n 阶乘的算法:
x = 1
i = 2
DO WHILE i <= n
x = x * i
i = i + 1
ENDDO
factorial = x
下面的 Forth 字计算阶乘
: factorial ( n -- n! )
1 2 ROT \ x i n
BEGIN \ x i n
2DUP <= \ x i n f
WHILE \ x i n
-ROT TUCK \ n i x i
* SWAP \ n x i
1+ ROT \ x i n
REPEAT \ x i n
2DROP ; \ x
注意,为了使 WHILE 循环能够正常工作,在 BEGIN 和 REPEAT 之间的堆栈安排必须相同。还要注意的是,尽管上面的算法使用了 3 个变量 x、i 和 n , 但 Forth 实现却不使用任何变量!这是 Forth 的特点。你可以发现在 Forth 中使用变量比在其它语言中使用变量要少得多。
可以输入以下内容测试阶乘的定义
3 factorial .
4 factorial .
0 factorial .
4.8 练习
练习 4.1 Fibonacci 序列是一个数值序列,其中的每个数(从第三个开始)都是它紧邻的前两个数之和。于是开始几个数看起来像是这样:
1 1 2 3 5 8 13 21 34
定义一个 Forth 字
fib ( n -- )
它将打印所有值小于 n 的 fibonacci 序列,通过下面方法来测试你的字:
1000 fib
练习 4.2 创建一个表称为 weights ,它包含下列值
75 135 175 115 220 235 180 167
定义一个 Forth 字称为
heaviest ( pfa -- max.value )
它将按照栈顶的值从表中打印最大值,如果你输入
weights heaviest .
值 235 将要打印出来
|