Easy Forth:在浏览器中了解REPL

2021-02-17 18:34:19

这本小电子书在这里教您一种称为Forth的编程语言。 Forth与大多数其他语言不同。它不是功能性的也不是面向对象的,它不需要类型检查,并且基本上具有零语法。它写于70年代,但今天仍用于某些应用。

您为什么要学习这种奇怪的语言?您学习的每种新编程语言都可以帮助您以新的方式思考问题。 Forth很容易学习,但是它要求您以不同于以往的方式思考。这使它成为扩展您的编码视野的理想语言。

本书包括我用JavaScript编写的Forth的简单实现。它绝不是完美的,并且缺少您在realForth系统中期望的许多功能。只是这里为您提供了一种简单的方法来尝试示例。 (如果您是Forth专家,请在此处做出贡献并使其变得更好!)

我将假设您至少知道另一种编程语言,并对堆栈如何作为数据结构有一个基本的了解。

将Forth与其他大多数语言区分开来的是它对堆栈的使用。在Forth中,一切都围绕着堆栈旋转。每当您键入数字时,它都会被压入堆栈。如果要将两个数字加在一起,则键入+会将堆栈中的前两个数字相加,将它们相加,然后将结果放回堆栈中。

让我们来看一个例子。在解释器中键入以下内容(不要复制粘贴),在每行之后键入Enter。

每次您键入一行,然后按Enter键,Forth解释器都会执行该行,并附加字符串ok以使您知道没有错误。您还应该注意,在执行每一行时,顶部的区域会充满数字。该区域是我们对堆栈的可视化。它应该看起来像这样:

现在,在同一个解释器中,键入单个+,然后按Enter键。堆栈中的顶部两个元素2和3已被5取代。

再次输入+并按Enter键,前两个元素将替换为6。如果再输入+一遍,即使堆栈上只有一个元素,Forth也会尝试将前两个元素弹出堆栈。这导致堆栈下溢错误:

Forth不会强制您将每个令牌单独输入。在下一个编辑器中键入以下内容,然后按Enter键:

运算符出现在操作数之后的这种样式称为“反向波兰语注释”。让我们尝试一些复杂的事情,并计算10 *(5 + 2)。在解释器中键入以下内容:

Forth的优点之一是操作顺序完全基于程序中的顺序。例如,执行52 + 10 *时,解释器将5压入堆栈,然后加2,然后将它们相加并推入结果7,然后将10压入堆栈,再乘以7和10。因此,不需要给低优先级的组运算符加上括号。

大部分Forth单词都会以某种方式影响堆栈。有些从堆栈中取出值,有些将新值保留在堆栈中,有些则将两者混合。这些“堆栈效应”通常使用以下形式的注释来表示(之前-之后)。例如,+是(n1 n2-sum)-n1和n2是堆栈中的前两个数字,而sum是堆栈中剩余的值。

Forth的语法非常简单。 Forth代码被解释为一系列空格分隔的单词。几乎所有非空白字符在单词中都是有效的。当Forth解释器读取一个单词时,它将检查是否在称为字典的内部结构中存在定义。如果找到,则执行该定义。否则,假定该单词为数字,并将其压入堆栈。如果单词不能转换为数字,则会发生错误。

foo?表示Forth无法找到foo的定义,并且不是有效数字。

我们可以使用两个特殊词创建自己的foo定义::(colon)和; (分号)。 :是告诉Forth我们要创建定义的方式。 :之后的第一个单词成为定义名称,其余单词(直到;为止)组成了定义的主体。通常,在名称和定义的正文之间包含两个空格。尝试输入以下内容:

警告:常见的错误是错过;之前的空格。单词。因为前言是用空格分隔的并且可以包含大多数字符,所以是一个完全有效的单词,不会解析为两个单独的单词。

如您所希望的那样,我们的foo字仅会将100加到栈顶的值。它不是很有趣,但是应该让您了解简单定义的工作方式。

现在,我们可以开始研究Forth的一些预定义单词。首先,让我们来看一些用于操纵堆栈顶部元素的词语。

dup是“ duplicate”的缩写–它复制堆栈的顶部元素。例如,尝试一下:

您可能已经猜到了交换,交换堆栈的前两个元素。例如:

over不太明显:它从堆栈顶部获取第二个元素并将其复制到堆栈顶部。运行此:

最后,腐烂“旋转”了堆叠的前三个元素。从堆栈顶部开始的第三个元素移到堆栈顶部,将其他两个元素向下推。

Forth中最简单的输出单词是..您可以使用。在当前行的输出中输出堆栈的顶部。例如,尝试运行此(确保包括所有空格!):

1。 2。 3。 4 5 6。 。 。 1 2 3 6 5 4好

按顺序进行此操作,我们按1,然后将其弹出并输出。然后,对2和3进行相同的操作。接下来,将4、5和6压入堆栈。然后将它们弹出并一个接一个地输出。这就是为什么输出中的最后三个数字相反:堆栈是后进先出。

可以用于将数字输出为ASCII字符。就像 。输出堆栈顶部的数字,以asciicharacter的形式输出该数字。例如:

在这里我不会给出输出,以免破坏惊喜。也可以写成:

与。不同,emit不会在每个字符后输出任何空格,从而使您能够构建任意的输出字符串。

最后,我们有了。" –输出字符串的特殊词。 。"在交互模式下,单词在定义中的作用不同。 。"标记要输出的字符串的开头,并用"标记字符串的结尾。结束词不是一个单词,因此不需要用空格分隔。这是一个例子:

:打印堆栈顶部cr dup。"堆栈的顶部是" 。 cr。"看起来像'" dup发出。" '在ascii"中; 48打印堆栈顶部

48 print-stack-top栈顶是48,看起来像' 0'。在ascii中

现在到有趣的东西!像大多数其他语言一样,第四语言具有条件和循环来控制程序的流程。要了解它们的工作原理,首先,我们需要了解Forth中的布尔值。

实际上,Forth中没有布尔类型。尽管规范的true值为-1(所有布尔运算符返回0或-1),但数字0被视为false,其他任何数字均为true。

您可以使用<和>小于和大于。 <检查堆栈顶部的第二个项目是否小于堆栈顶部的项目,反之亦然,对于&gt ;:

3 4< 20 30<和.3 4< 20 30>或.3 4<反转。

第一行等于3< 4和20< 30基于C的语言。第二行等效于3< 4 | 20> 30.第三行等于!(3< 4)。

and,or和invert都是按位运算。对于格式正确的标记(0和-1),它们可以按预期工作,但对于任意数字,它们给出的结果将不正确。

现在我们终于可以使用条件了。 Forth中的条件只能在定义内部使用。 Forth中最简单的条件语句是ifthen,它相当于大多数语言中的标准if语句。以下是使用if then的定义示例。在此示例中,我们还使用了mod单词,该单词返回堆栈中前两个数字的模。在这种情况下,最上面的数字是5,另一个是在调用buzz?之前放在堆栈上的内容。因此,5 mod 0 =是一个布尔表达式,用于检查堆栈的顶部是否可被5整除。

请务必注意,“ the”一词标记了if语句的结尾。例如,这等效于Bash中的fi或Ruby中的end。

要意识到的另一件重要事情是,如果if在检查真假时消耗了栈顶值。

if else then在大多数语言中等效于if / else语句。这是其用法的一个示例:

:是零吗? 0 =如果。"是的!"其他。"不!"那么; 0是零?1是零?2是零?

这次,if子句(后续)是if和else之间的所有内容,而else子句(替代)是else和then之间的所有内容。

在Forth中,do循环与大多数基于C的语言中的for循环非常相似。在do循环的主体中,特殊字i将当前循环索引压入堆栈。

堆栈中的前两个值给出i值的起始值(包括)和结束值(不包括)。起始值取自堆栈的顶部。这是一个例子:

:嘶嘶声? 3 mod 0 = dup如果。"嘶嘶声然后;:嗡嗡声? 5 mod 0 =如果。"嗡嗡声然后;:嘶嘶声? dup嘶嘶声?交换嗡嗡声?或反转;:做嘶嘶声25 1我做嘶嘶声吗?如果我 。然后循环;做嘶嘶声

嘶嘶声?使用3 mod 0 =检查栈顶是否被3整除。然后,它使用dup复制此结果。该值的顶部副本由if占用。第二个副本留在堆栈上,用作fizz?的返回值。

如果堆栈顶部的数字可被3整除,则字符串" Fizz"将被输出,否则将没有输出。

嘶嘶声?调用dup复制堆栈顶部的值,然后调用fizz ?,将顶部副本转换为布尔值。之后,堆栈的顶部由原始值组成,并且由fizz?返回的布尔值。 swap交换这些,因此原始的栈顶值又回到顶部,而布尔值在下面。接下来我们称为buzz ?,它将布尔值替换为栈顶值。现在,堆栈上的前两个值是布尔值,表示该数字是3还是5可以整除。此后,我们调用或查看其中两个值是否为真,然后求反以取反该值。逻辑上,fizz-buzz的主体?等效于:

因此,嘶嘶声?返回一个布尔值,指示该参数是否不能被3或5整除,因此应该被打印。最后,fizz-buzz从1循环到25,调用fizz-buzz?在i上,如果发出嘶嘶声,则输出i,返回true。

如果您在弄清楚fizz-buzz内部发生了什么情况?,下面的示例可能会帮助您了解其工作原理。我们要做的就是执行fizz-buzz定义中的每个字吗?在单独的行上。执行每一行时,请观察堆栈以查看其变化:

:嘶嘶声? 3 mod 0 = dup如果。"嘶嘶声然后;:嗡嗡声? 5 mod 0 =如果。"嗡嗡声然后; 4dupfizz?swbbuzz?或反转

4 4<-Topdup 4 4<-Topfizz吗? 4 0<-Topswap 0 4<-Topbuzz? 0 0<-Topor 0<-Topinvert -1<-Top

请记住,堆栈上的最终值是fizz-buzz?word的返回值。在这种情况下,这是事实,因为数字不能被3或5整除,因此应打印出来。

5 5<-Topdup 5 5<-Topfizz吗? 5 0<-Topswap 0 5<-Topbuzz? 0 -1<-Topor -1< Topinvert 0<-Top

在这种情况下,原始栈顶值可被5整除,因此不应打印任何内容。

Forth还允许您将值保存在变量和常量中。变量使您可以跟踪更改的值,而不必将其存储在堆栈中。常量为您提供了一种引用不变值的简单方法。

由于局部变量的作用通常由堆栈承担,因此Forth中的变量更多地用于存储跨多个字可能需要的状态。

这基本上将特定的存储位置与名称平衡相关联。平衡现在是一个词,它所做的就是将其内存位置推入堆栈:

您应该在堆栈上看到值1000。此Forth实施任意开始在存储位置1000处存储变量。

这个单词 !在变量引用的存储位置存储一个值,而单词@从存储位置获取该值:

这次,您应该在堆栈上看到值123。 123 balance将值和存储位置压入堆栈,然后!将该值存储在该内存位置。同样,@根据内存位置检索值,并将该值压入堆栈。如果您使用过C或C ++,则可以将balance视为由@取消引用的指针。

这个单词 ?定义为@。并显示变量的当前值。单词+!用于将变量的值增加一定量(例如,基于C的语言中的+ =)。

可变余额ok123余额!好平衡吗? 123 ok50余额+! okbalance? 173好

如果您的值不变,则可以将其存储为常量。常量在一行中定义,如下所示:

这将创建一个新的常量,其值为42。与变量不同,常量仅表示值,而不是存储位置,因此无需使用@。

运行此命令会将值84压入堆栈。答案就像是它代表的数字一样(就像其他语言中的常量和变量一样)。

Forth并不完全支持数组,但确实允许您分配连续内存区域,就像C语言中的数组一样。要分配此内存,请使用分配字。

可变数字3个单元分配10个数字0个单元格+20个数字1个单元格+30个数字2个单元格+40个数字3个单元格+!

本示例创建一个称为数字的存储位置,并在该位置之后保留三个内存单元,总共有四个存储单元。 (celljust乘以cell-width,在此实现中为1。)

数字0 +给出数组中第一个单元的地址。 10个数字0 +!将值10存储在数组的第一个单元格中。

变量个数3个单元分配:个(偏移量-addr)个单元号+; 10 0号!20 1号!30 2号!40 3号!2号?

number将一个偏移量转换为数字,然后返回该偏移量处的内存地址。 30 2号!在数字2的偏移量2处存储30,而数字2则在数字2偏移量处打印值。

Forth有一个叫做key的特殊单词,用于接受键盘输入。执行该关键字时,执行会暂停直到按下某个键为止。按下某个键后,该键的键代码将被压入堆栈。请尝试以下操作:

运行此行时,您会发现起初没有任何反应。这是因为解释器正在等待您的键盘输入。尝试按A键,您应该看到该键的键码65在当前行上显示为输出,现在按B键,然后按C键,您应该看到以下内容:

Forth有另一种循环,称为begin until。在基于C的语言中,这就像while循环一样。每次碰到“直到”一词时,解释器都会检查堆栈的顶部是否非零(真)。如果是,它将跳回到匹配的开始。如果不是,则继续执行。

这将一直打印关键代码,直到您按空格键为止。您应该会看到以下内容:

打印键码80 82 73 78 84 189 75 69 89 67 79 68 69 32 ok

key等待键输入,然后dup复制key中的键代码。然后使用。输出键码的最上面的副本,并使用32 =检查键码是否等于32。如果是,则跳出循环,否则循环返回。

现在是时候将它们组合起来并制作游戏了!我没有输入所有代码,而是将其预加载到编辑器中。

在查看代码之前,请尝试玩游戏。要开始游戏,请执行单词start。然后使用箭头键移动蛇。如果输了,可以重新开始跑步。

可变的snake-x-head500单元分配的可变的snake-y-head500单元分配的可变的apple-x变量的苹果y0恒定的左1恒定的向上2恒定的右3恒定的向下24恒定的宽度24恒定的高度24可变的高度可变的方向可变长度:蛇形x(偏移量-地址)单元格蛇形x-头+;:蛇y(偏移-地址)单元蛇y-头+;:转换xy(XY-偏移)24单元* +;:绘制(颜色xy-)转换xy图形+! ; ::绘制白色(xy-)1腐烂绘制;:绘制黑色(xy-)0腐烂绘制;:绘制墙宽度0做我0绘制黑色i高度1-绘制黑色循环高度0做0我画黑色宽度1-我画黑色循环;:初始化蛇4长度!长度@ 1 + 0做12 i-i snake-x! 12我是蛇!向右循环! ;:set-apple-position apple-x!苹果! ;:初始化苹果4 4 set-apple-position;:初始化宽度0做高度0做j我画白循环画壁初始化蛇初始化苹果;:上移-1蛇头+! ;:左移-1蛇x头+! ;:向下移动1个蛇头+! ;:右移1个蛇x头+! ;:蛇头方向@左移=如果左移否则上移=如果上移否则右移=如果右移否则向下移=如果先移下然后然后下降; \移动每个段一条蛇向前移动:蛇尾0长度@我是否蛇x @我1 +蛇x!我是y蛇@我是1 + y蛇! -1 + loop;:水平方向@ dup左=交换右=或;:等垂直方向@ dup up =向下交换=或;:如果向上方向,则向上呈水平!然后;:如果向左,则向左转为垂直!然后;:如果向下方向,则将水平调低!然后;:如果方向正确,则向右转为垂直!然后;:改变方向(键-)37 over =如果是左转,否则38 over =如果是转弯,否则39 over =如果是右转,否则40 over =如果是转弯,然后再下降;: -输入last-key @ change-direction 0 last-key! ; \在可玩区域内获取随机的x或y位置:随机位置(-pos)宽度4-随机2 + ;;:移动苹果apple-x @ apple-y @绘制白色随机位置random-set-苹果位置;:蛇1长度+! ;:检查苹果蛇x头@苹果x @ =蛇y头@苹果y @ =并且如果移动苹果长蛇则;:检查碰撞(-标志)\得到当前x / y位置蛇形x头@蛇形y头@ \在当前位置获取颜色convert-xy图形+ @ \在堆栈0上保留布尔标志0 =;:绘制蛇形长度@ 0我是否是蛇形x @蛇-y @绘制黑色循环的长度@蛇-x @长度@蛇-y @绘制白色;:绘制苹果apple-x @苹果-y @绘制黑色;:游戏循环(-)开始绘制-snake draw-apple 100睡眠检查输入-snake-tail移动-snake-head检查苹果检查冲突,直到。"游戏结束;:开始初始化游戏循环;

在我们深入研究此代码之前,有两个免责声明。首先,这是可怕的代码。我绝不是Forth专家,所以可能我完全以错误的方式做各种事情。其次,该游戏使用了一些非标准技术来与JavaScript交互。我现在将详细介绍这些。

您可能已经注意到,此编辑器与其他编辑器不同:它内置了HTML5Canvas元素。我创建了一个非常简单的内存映射界面,可以将其绘制到此画布上。 画布分为24 x 24“像素”,可以是黑色或白色。 第一个像素位于变量图形给定的内存地址处,其余像素与变量偏移。 因此,例如 ......