Forth入门

作者: 成建文

写在前面的话

我一直以为西文是编程的基础。这当然不是因为计算机是西人发明的,而是因为让计算机识别和处理汉字几乎是不可能的。到今天汉字还不得不依赖于西文来输入。所以不掌握西文,简直无法进行编程学习。

一年多以前,我开始关注儿童编程教育,然后我觉得语言不应该成为学习电脑编程的问题。

Forth是一个非常简单的,甚至没有什么语法的语言,它将指令或者数据输送给计算机,而计算机则一个一个的执行它们(或者保存它们),仅此而已。所以我觉得如果想用汉字进行编程,Forth是一个很好的开始。因此我找到了Easy Forth。我觉得它非常符合我对汉语编程的理解,所以我迫不及待的将它译为中文,同时在翻译的过程中也做了一些汉语编程的工作。

下面开始的内容,就是这本我翻译并做了一些汉语编程(实际上就是定义基于汉语的字典)的工作成果,我希望你有一个愉快的学习过程。

关于这本教材

这本教材教给你学习一门新的编程语言:Forth。Forth是一门不同于其它任何语言的编程语言,它既不是函数式,也不是面向对象的,它没有类型检查,它甚至没有任何语法可言。它产生于上个世纪的七十年代,现在仍然在这些领域使用着。

为什么你需要学习一门新的语言?因为新的语言会让你用新的思路来考虑问题。Forth不但很容易学习,而且它能让你用不同于以往任何方式来考虑问题,它是一门能够拓展你思路的非常好的语言。

这本手册包括了一个简单的基于JavaScript实现的最基本的Forth解释器。它能够让你书中的示例进行演示。

我们生活中经常会看到一些清单,它们将信息按顺序整理为一个列表,这样可以很方便得管理这些信息,被整理为这样的信息我们称其为序列(或者列表)。

序列虽然看起来都一样,但是在操作和存储上却不尽相同,栈就是其中的一种。栈的操作特点是后进先出,即出栈的元素总是最后入栈的元素,类似于我们往袋子里装东西,最后装进去的东西总是被最先取出来。

栈的好处在于我们只需要存和取就可以了,不需要指定其它参数,例如存放的位置或者方向。

栈的缺点也显而易见,它只能对栈最外面的元素进行存取,栈里面的元素则没办法直接使用。

组是另外一种常见的序列,它的特点是通过一个顺序号对组中的每个元素进行存取。尽管在存取的时候需要指定一个顺序号,但它提供了对元素随机存取的便利性,所以也是非常常用的一种序列。

组的缺点除了在存取时需要指定顺序号以外,在使用前还需要预先确定它的长度,即组中所有元素的个数,这也为组的使用带来了不便。

Forth

这里的Forth提供有一个交互式界面,它是下面这个样子。

 

它提供一个输入区(蓝色背景),你可以一行一行的输入程序的内容;Forth对输入的每行内容进行解释和执行,并将结果在显示区内(绿色背景);最上面的则是栈情区(红色背景),它会显示当前栈内元素的情况。

先输入一些值

Forth中一切都是围绕着栈进行的。任何时候你输入一个数,它就会被放入栈中。如果你想将两个数相加,那么输入“和”(这是一个预先定义的字,Forth会执行这个字)可以将栈最外面的两个数取出来进行相加,然后将结果再放入栈中。

我们可以来看一个例子。把下面的内容依次输入(不能拷贝/粘贴)到Forth中,在每一行的最后按“回车”键。

1
2
3
 

每次你通过“回车”键输入一行,Forth解释器就会解释并执行这一行中的内容,然后会有一句“完成!”或者其它的错误信息显示出来。在Forth执行每一行内容时,你也通过栈情区看到看到栈的内容,它看起来应该是这样的:

1 2 3

继续输入“和”,然后按“回车”键,栈最外面的两个数“2”和“3”,就会被“5”替代。

1 5

这个时候,你的显示区应该是这个样子:

1 完成! 2 完成! 3 完成!完成!

再次输入“和”,并按“回车”键,最外面的两个数会又被替换为6。如果你输入更多的“和”,尽管堆栈中只剩一个数,Forth仍然会试图从堆栈中取出两个值,这样就会产生“空栈!”的错误:

1 完成! 2 完成! 3 完成!完成!完成!空栈!

Forth不是必须把每个要输入的数作为单独的一行进行输入,试着输入下面的内容,并按“回车”键:

123 456和
 

现在栈情的显示应该为:

579

我们来看一个更为复杂的例子,比如计算“10 × (5 + 2)”。在Forth中输入下面的内容:

5 2和10积
 

Forth完全按照输入内容的顺序执行。例如当执行“5 2和10积”时,Forth先将5入栈,再将2入栈,然后将它们出栈相加,并将结果7入栈,然后将10入栈,最后将7和10出栈并相乘,并将结果70入栈。因此,在Forth中永远不需要使用括号这样的符号对内容进行优先级设定。

在上面的例子中,我们接触到了Forth语言中最为基本的概念:字。输入到Forth中的内容被解释为一个一个的字,不同类型的字会被进行不同的操作。

Forth的规则非常简单,它的内容被解释为单独的字,然后依次得到执行。Forth的字可以是一个汉字,或者是由“0-9”构成的数字,再或者由任意英文字母“a-z”构成的西文单词,比如“中”、“2020”、“this”都是Forth的字。如果连续两个字都是数字或者都是都是西文单词,它们之间应该使用一个空格进行隔离,否则显然Forth会将它识别为一个字,而不是两个字。

数字

显然,数字是一种是用0-9以及小数点“.”构成的,它会被Forth放入栈中,以被后续的字进行处理。

Forth中的数字可以是正负整数或者小数,你也可以用科学计数法来表示它。

运算字

一类重要的字是运算字,它们对数字进行各种运算处理。

栈操作

Forth中大多数的字会对栈进行操作,有些字需要出栈,而有些则需要入栈,还有一些既要出栈还要入栈。这些字对栈的操作我们使用“(出栈操作 入栈操作)”的方式来表示。例如“和”的栈操作为“(值值 值)”,它表示“和”会出栈两个值,并入栈一个值。

和(值值|值)

“和”的意思就是将两个数字相加,它从栈中取两个数字,将它们相加,然后将其结果入栈。例如:

100 200和
 

结果你会看到下面的栈情:

300

差(值值|值)

“差”的意思就是将两个数字相减,它从栈中取两个值,将它们相减,然后将其结果入栈。例如:

200 100差
 

结果你会看到下面的栈情:

100

积(值值|值)

“积”的意思就是将两个数字相乘,它从栈中取两个值,将它们相乘,然后将其结果入栈。例如:

100 200积
 

结果你会看到下面的栈情:

20000

商(值值|值)

“商”的意思就是将两个数字相除,它从栈中取两个值,将它们相除,然后将其结果入栈。例如:

400 33商
 

结果你会看到下面的栈情:

12

余(值值|值)

“余”的意思就是将两个数字相除,然后获取它的余数。它从栈中取两个值,将它们相除,然后将余数入栈。例如:

400 33余
 

结果你会看到下面的栈情:

4

除(值值|值值)

如果你既想得到商,还想得到余数,你可以使用“除”。它从栈中取两个值,将它们相除,然后将余数和商入栈。例如:

400
33
除
 

结果你会看到下面的栈情:

4 12

负(值|值)

“负”的意思就是将数字取反。如果它是正数,它将变为负数;反之如果它是一个负数,它将变为一个正数。例如:

400
负
 

结果你会看到下面的栈情:

-400

增(值|值)

“增”的意思就是将数字加一,它从栈中取一个值,将它加上一,然后将结果入栈。例如:

100
增
 

结果你会看到下面的栈情:

101

减(值|值)

“减”的意思就是将数字减一,它从栈中取一个值,将它减去一,然后将结果入栈。例如:

100
减
 

结果你会看到下面的栈情:

99

倍(值|值)

“倍”的意思就是将数字翻倍,它从栈中取一个值,将它乘以2,然后将结果入栈。例如:

100
倍
 

结果你会看到下面的栈情:

200

衰(值|值)

“衰”的意思就是将数字减倍,它从栈中取一个值,将它除以2,然后将结果入栈。例如:

100
衰
 

结果你会看到下面的栈情:

50

方(值|值)

“方”的意思就是计算一个数字的平方,它从栈中取一个值,将它乘以它自己,然后将结果入栈。例如:

100
方
 

结果你会看到下面的栈情:

10000

根(值|值)

“根”的意思就是计算一个数字的平方根,它从栈中取一个值,计算它的平方根,然后将结果入栈。例如:

100
根
 

结果你会看到下面的栈情:

10

幂(值值|值)

“幂”的意思对两个数字进行幂运算,即nm。它从栈中取两个值,分别作为底数和指数,对其进行计算,然后将结果入栈。例如:

3
5
幂
 

结果你会看到下面的栈情:

243

是非运算

有时候我们需要让计算机来判断某个表达式是否正确,比如一个值是不是大于100,或者一个值是否是正数,等等,这种运算称为是非运算。

是非运算产生的结果是“是”和“非”。除了可以比较、判断等这些对数值的是非运算,还可以对是非值进行运算,比如一个值大于100并且小于200。

Forth中“非”用“0”表示,而“是”则用一个“-1”表示。

大、小、等(值值|值)

“大”、“小”和“等”用于判断两个值的大小或者它们是否相等,例如

3 4大
 

结果你会看到下面的栈情:

0

是、非(值|值)

“是”和“非”用于查看是非是否为“是”或者“非”,即是否为“-1”或者“0”,例如:

-1是
 

结果你会看到下面的栈情:

-1

且(值值|值)

“且”用来比较两个是非值是否全为“是”,否则它产生“非”值。例如:

-1 0且
 

结果你会看到下面的栈情:

0

或(值值|值)

“或”用来比较两个是非值是否是否至少有一个为“是”,否则它产生“非”值。例如:

-1 0或
 

结果你会看到下面的栈情:

-1

非(值|值)

“非”用来将“是”改为“非”,而将“非”改为“是”,例如:

-1非
 

结果你会看到下面的栈情:

0

最后我们来看一个稍微复杂的例子:

3 4小
20 30小
且

第一行内容等同于“3小于4”,第二行内容等同于“20小于30”,第三行内容等同于“并且”,所以整个程序的意思就是“3小于4并且20小于30”。

 

最后你会看到下面的栈情:

-1

栈操作

有些字不对数字本身做任何运算,然而它们仍然会对栈中的值作出改变。

重(值|值值)

“重”的意思就是重复,它出栈一个数,复制后将它们入栈。例如:

1 2 3重
 

结束后你会看到下面的栈情:

1 2 3 3

弃(值|)

“弃”简单的出栈一个数,试一下下面的例子:

1 2 3弃

会得到如下的栈情:

1 2
 

换(值值|值值)

“换”,正如你猜的那样,它出栈两个数,然后把它们交换后入栈。例如:

1 2 3 4换

得到栈情:

1 2 4 3
 

挖(值3值2值1|值2值1值3)

“挖”的意思就是将栈从最外面数的第三个数“挖”出来放到栈的最外面。例如:

1 2 3挖

得到:

2 3 1
 

输出

输出字是用于输出(显示)的。

印(值|)

最常用的显示字是“印”,它出栈一个数,并将其显示在终端上。试着输入下面的内容:

1印2印3印4 5 6印印印
 

你会看到这样的输出:

1印2印3印4 5 6印印印 1 2 3 6 5 4 完成!

我们看一下它的执行过程:想将“1”入栈,然后将其出栈并显示;然后对“2”和“3”重复这样的过程。最后将“4”、“5”和“6”依次入栈,再将它们出栈并显示。最后这三个值的顺序被颠倒的原因是栈是一个后进先出的容器:最后入栈的值总是最先出栈。

字(值|)

“字”能够出栈一个值,然后将其转换为所表示的符号并显示。例如:

 31243 32534字字
 

这里就不剧透了。这段内容也可以写成:

 32534字31243字

和“印”不同,“字”在显示时不会增加任何东西,这让你能够完整的显示一句话。

行(|)

“行”是另起一行的意思,它让显示的位置移动到新的一行,这只会影响以后的显示:

行100印行200印行300印
 

得到下面的结果:

行100印行200印行300印 100 200 300 完成!

没有定义的字在输入后会显示“未定义!”这样的错误信息,你可以在下面试着输入“晕”(一个没有定义的令字)然后按“回车”键。

 

你就会看到这样的结果:

未定义!

“未定义!”的意思是Forth没有找到“晕”的定义内容。如果你希望让Forth能够执行它,你就必须用“定”来为它进行定义。

用“定”和“毕”对一个字进行定义,其中“定”表示定义开始,而“毕”则表示定义结束。“定”后面紧跟着的就是需要定义的字,然后就是所定义的内容(直到“毕”为止)。因为Forth每次只能输入一行内容,所以在使用“定”的时候,它们应该写在同一行内容里,例如:

定晕100和毕
1000
晕
晕
晕
 

当我们定义了“晕”这个字以后,每次输入“晕”,都会给栈最后的值加上100。这个例子虽然简单,但它让你明白如何定义一个字。

控制

控制字本身并不做任何操作,它只对后面的字是否被执行,以及如何执行产生影响。和“定”一样,控制字只能出现在同一行内容。

若则(值|)

“若则”是对是否执行提供控制,它控制的依据是一个是非值,它等同于我们常说的“如果…”。在下面这个“若则”的令规例子中,我们使用了“余”来判断出栈的值是否是5的倍数(同5相除,余数为0),来决定是否显示“是”。

定例5余0等若26159字则毕
3例
4例
5例
 

结果如下:

3例 完成! 4例 完成! 5例 是 ok

需要注意的是“则”表示“若”的结束。之后字不再受若的约束。

若否则(值|)

“若否则”等同于“如果/否则”这样的情况,这里有一个例子:

定例0等若26159字否21542字则毕
0例
1例
2例
 

结果:

0例 是 完成! 1例 否 完成! 2例 否 完成!

在这种情况下,“是”的内容为“若”和“否”之间的内容,“非”的内容则为“否”和“则”之间的内容。

复返(值值|)

“复返”用于控制内容的反复执行。在在反复执行的内容中,“报”则可以将当前的反复值入栈。

“复返”出栈的两个值分别确定了反复值的开始值和结束值,先出栈的为开始值,这里有一个例子:

定例10 0复报印返毕
例
 

它会产生下面的结果:

0 1 2 3 4 5 6 7 8 9 完成!

程序“10 0报印返”的意思就是“依次从0到10运行:显示反复值”。

拍七令

拍七令是一个多人玩的报数游戏,当报到含有7的数,或者能被7整出的数时,报数人必须喊“过”(或者用其它动作替代),否则即认为是犯规。

我们可以用“复返”编写一个拍七令游戏:

定七99 1复报7余0等报10商7等报10余7等或或若36807字32字否报印则返毕
七
 

“7余0等”判断当前反复值是否能被7整除,“10商7等”判断当前反复值除以10以后是否得到7,“10余7等”则判断当前反复值除以10以后是否余数为7。如果符合这其中任何一个条件,则会显示“过”字;否则则会显示当前的反复值。

变数和常数

Forth语言还可以将值保存在一个变数或者常数中。保存在变数中的值可以进行更改,而保存在常数中的值则不可以进行修改。

变数

栈通常用于保存当前正在处理的一些值,Forth中的变数常常被用来在各个处理过程之间需要共同使用的一些值。

用“变”来定义一个新的变数:

变通

这里定义的“通”既不是一个令,也不是一个值,它是一个代表计算机中某个位置的名,通过它可以对该位置中所保存的信息进行读和写。当我们输入一个被定义了的变数名时,它做的仅仅是将它所代表的计算机中的位置入栈,例如:

变通
通
 

我们看到栈中会出现一个数值,它表示“通”所代表的计算机中的一个位置的值。

“写”会将一个值写入到计算机位置中,“读”则会从这样的计算机地址中获取它所保存的值。

变通
123通写
通读
 

最后你应该能在栈中看到“123”这个值。“123通”会将值和保存这个值的内存地址入栈,然后“写”就将它们分别出栈,并将值写入在这个地址中。同样的,“读”则基于所给出的内存地址读取所保存的值,并将它入栈。

“啥”被定义为“读印”,它会将变数的值显示出来。

变通
123通写
通啥
 

运行这段内容,你会看到这样的结果:

变通 完成! 123通写 完成! 通啥 123 完成!

常数

如果你有一个常数,你可以把它保存到一个常数中。“常”用来定义一个常数:

42
常
答

这样就定义了一个新的常数“答”,它代表的值为“42”。和变数不同,常数只是代表这个值,它不是内存中的一个地址,所以不需要使用“读”来读取它所代表的值。

42常答
2答积
 

这段内容会在栈中生成值“84”,“答”被用于表示它所代表的那个值。

组数

Forth可以通过对变数进行扩展,使其能够支持组数。

变组
3扩
10组0和写
20组1和写
30组2和写
40组3和写
 

这个例子定义了一个变数“组”,然后又扩展了3个内存单元,这样通过变数“组”就成为了一个组数,我们可以通过这个组数所代表的内存地址和一个顺序号的和,来得到相应的内存地址。“组0和”得到组数的第一个的地址,“组1和”则得到组数的第二个地址,以此类推,我们可以得到这个组数所有(4个)的地址。

我们可以很简单的定义一个组数的读写:

变组3扩
定量组和毕
10 0量写
20 1量写
30 2量写
40 3量写

2量啥
 

“量”通过出栈获取一个偏移位置,然后同“组”所代表的内存地址相加,从而得到这个偏移位置的内存地址。“30 2量写”则将30写入在组数的第二个偏移位置中。

结束语

真正的Forth要比我在这里说的(以及我编写的这个解释器)强大得多。一个真正的Forth系统能让你修改编译器如何工作,建立新的令字,以及让你完全定义你的环境,甚至基于Forth创建你自己的语言。

一个更好的学习Forth的书是Leo Brodie编写的Forth起步,它在线免费,告诉你很多这里没有的有趣的东西。它还有一些很好的练习题,让你测试一下你的水平。你可能需要下载安装SwiftForth来运行书中的代码。