acm-header
登录

ACM通信

实践

在球拍中创造语言


在球拍中创造语言,插图

信贷:纳内特Hoogslag

回到顶部

为一项简单的工作选择正确的工具很容易:当您需要更换玩具中的电池时,螺丝刀通常是最佳选择,而grep是检查文本文档中的单词的明显选择。对于更复杂的任务,工具的选择很少如此直接,特别是对于编程任务,程序员拥有无与伦比的能力来构建他们自己的工具。程序员经常通过创建新的工具程序来解决编程问题,例如从数据表生成源代码的脚本。

由于程序员经常构建特定于任务的工具,因此提高他们工作效率的一种方法是为他们提供更好的工具制作工具。当工具采用程序生成器的形式时,这个想法就会导致创建直接可扩展语言的库。程序员甚至可能被鼓励从一种能够更好地支持该任务的语言的角度考虑问题。这种方法有时被称为语言作为编程3.

球拍既是一种编程语言,也是构建编程语言的框架。球拍程序可以包含扩展该语言语法的定义,以便以后在同一程序中使用,并且语言扩展可以打包为模块,以便在多个程序中使用。球拍支持从相对简单的语言扩展到全新语言的平滑路径,因为编程工具和其他软件一样,很可能从简单开始,然后随着对语言需求的增加而增长。

以文本冒险游戏(游戏邦注:也称为互动小说)为例,玩家在虚拟世界中输入命令,并与物体进行互动:

ins01.gif

为了让游戏变得有趣,程序员必须在虚拟世界中植入具有丰富行为的地方和事物。大多数编程语言都可以实现这个虚拟世界,但选择正确的语言结构(也就是正确的工具)来表示每个游戏元素是开发过程中的关键步骤。

正确的结构允许轻松创建命令、位置和事物,避免容易出错的样板代码来设置世界的状态和连接,同时也允许编程语言的全部功能来实现行为。

在通用编程语言中,没有任何内置的语言结构可能是完美的。例如,位置和事物可以是对象,而命令可以作为方法实现。然而,游戏的玩家并不调用方法,而是输入命令,这些命令必须被解析并动态映射到对地点和事物的响应。类似地,保存和加载游戏需要检查和恢复地点和事物的状态,这在一定程度上是对象序列化的问题,但也需要设置变量以解编组值(或者通过字典间接地从一个对象引用到另一个对象)。

有些编程语言包含诸如重载或惰性等构造,聪明的程序员可以利用这些构造来编码特定于领域的语言。《球拍》的设计更直接地解决了这个问题;它为程序员提供了使用新语法显式扩展编程语言的工具。有些任务只需要对核心语言进行很小的扩展,而其他任务则受益于创建全新的语言。球拍支持光谱的两端,它这样做的方式允许从一端到另一端的平稳进展。随着程序员对特定任务的需求或野心的增长,程序员可以利用球拍的统一框架进行更多的语言扩展和构建。

这里给出的文本冒险示例说明了从简单嵌入到单独的领域特定语言(包括对语法着色的IDE支持)的发展过程,并解释了相关的球拍细节;不需要事先了解球拍。喜欢更完整的语言介绍的读者应该咨询球拍指南1

这个例子是一个多重意义上的世界“玩具”,但它也是一个工业实践的规模模型。大多数电子游戏开发者都使用自定义语言,包括基于球拍的语言未知的游戏系列。2显然,当数以十亿计的娱乐资金处于危险之中时,编程语言的选择非常重要,甚至到了创造新的、特殊用途的语言的程度。

回到顶部

喧闹的世界

我们的文字冒险游戏包含了一套固定的的地方,如草地,房子,或沙漠,和一套固定的的事情例如门、钥匙或花。玩家在游戏世界中导航,并使用被解析为一个或两个单词的命令与事物进行互动:一个动词(即不及物动词,因为它没有目标对象),如帮助;或动词后接事物名称(即及物动词后接名词),如开着的门得到关键.导航词,例如都被视为动词。用户可以使用保存而且负载谓词,它们在任何地方都可以使用,并提示用户输入文件名。

要在《球拍》中实现一款文本冒险游戏,你需要先声明以下三个游戏元素的结构类型图1

球拍是Lisp的一种方言,是Scheme的后代,因此它的语法使用括号和标识符的自由语法(例如,传递?是一个标识符)。分号引入以换行符结束的注释。方括号可以与圆括号互换,但在某些上下文中按照约定使用,例如将字段名与修饰符组合在一起。的#:可变Modifier将字段声明为可变的,因为默认情况下字段是不可变的。

第一个结构体窗体在代码中绑定动词函数为每个字段接受一个参数并创建一个动词实例。例如,您可以定义a动词与别名年代作为

ins02.gif

Lisp和球拍程序倾向于使用字符串作为要显示给最终用户的文本,例如,动词描述,例如“南”.用前导单引号(例如,“南),通常用于内部名称,例如动词别名。

给定的定义一件事,你可以定义a草地的地方动词将玩家移动到a沙漠如图所示图2

列表函数创建一个列表,而缺点对两个值。的缺点函数通常将元素与列表配对以形成一个新的列表,但在这里缺点用于将谓词与实现谓词响应的函数配对。的λForm创建了一个匿名函数,在本例中它期望没有参数。

当动词的响应函数产生一个位置时,例如沙漠在这个例子中,游戏执行引擎将把玩家移动到返回的位置。同时,游戏引擎对保存和加载游戏状态的支持需要在地点和它们的名称之间进行映射。(地点可以实现为对象,可以序列化,但恢复游戏需要反序列化和更新球拍级别的变量,如草地)。的record元素!函数实现名称和位置之间的映射,如图3

事物的定义和登记必须与地方的定义和登记方式大体相同。动词必须被收集到一个列表中供游戏的命令解析器使用。最后,解析和执行引擎需要一组无处不在的谓词,每个谓词都有其响应函数。所有这些组成了游戏实现的有趣部分,而解析和执行引擎则是几十行静态基础设施。看到附带的侧边栏以连结到完整的游戏实施。构建虚拟世界所需的代码特别冗长。

回到顶部

抽象语法

虽然前面讨论的数据表示选择是典型的球拍程序,但球拍程序员不太可能编写直接定义和注册位置的重复代码,因为它包含了太多的样板列表,缺点es,λ。相反,球拍的程序员会写

ins03.gif

然后加上一个define-placeform到球拍使用基于模式的宏。这种宏的最简单形式使用define-syntax-rule描述如图4

紧接其后的表格define-syntax-rule是一个模式,模式后面的形式是a模板.与模式匹配的宏的使用被宏的模板替换,模替换模式变量为他们的比赛。的Id, desc, thng, VRB,expr此模式中的标识符是模式变量。

注意define-place形式不能成为函数。的沙漠表达后一般来说,表达式的求值必须延迟到命令输入。更重要的是,表单应该绑定变量meadow,以便用于命令的Racket表达式可以直接引用该位置。此外,变量的源名称(与它的值相反)用于注册元素表中的位置。

define-place宏到目前为止只匹配一个地方的一个东西和一个动词和响应表达式。若要概括任意数量的事物、动词和表达式,可以向模式中添加省略号图5

动词稍微复杂一些,因为你想让简单动词特别紧凑,便于指定,你需要一种不及物动词和另一种及物动词的模式。下面的例子说明了目标语法:

ins04.gif

ins05.gif

这个例子定义了四个动词:辞职用作不及物动词,不带别名;作为带别名的不及物动词n以及首选的描述“北”敲门作为及物动词(由下划线表示),不带别名;而且得到作为带别名的及物动词抓住而且和首选的描述“花”.最后,所有这些动词被收集到一个绑定到的列表中所有的动词供游戏的命令解析器使用。

实现定义动词Form需要一种更通用的模式匹配,以支持动词规范的不同形状,并将=和_作为字面量进行匹配。的一个实现定义动词能否将单个动词的处理工作推迟到define-one-verb宏,它使用define-syntax而且语法规则所示图6

define-place, define-thing,define-verb宏是抽象语法.它们抽象了重复的语法模式,因此程序员可以避免样板代码,并集中精力创建有趣的动词、地点和事物。

修改后的游戏实现,有一个紧凑和可读的虚拟世界的实现,可在线(参见附带的侧边栏的链接)。

回到顶部

语法扩展

如果一个Racket的程序员对编写一款文本冒险游戏感兴趣,那么他很可能会在这个时候停止扩展语言。如果文本冒险引擎可以在多个世界中重复使用,那么Racket程序员很可能会在语法抽象之外更进一步语法扩展

抽象和扩展之间的区别部分取决于旁观者的看法,但扩展表明功能如的地方而且record元素!可以保密,而define-place导出,以便在具有独立于实现的语义的世界定义模块中使用。在定义世界的模块中,宏如define-place具有与内置表单相同的状态,例如定义而且λ

要进行这种转换,可以将定义动词、define-place define-thing,define-everywhere定义在它们自己的模块中,称为world.rkt。

ins06.gif

该模块导入txtadv。rkt,出口定义动词以及动词应答中使用的函数,如存盘而且读取游戏.与此同时,txtadv。RKT保持实现世界数据类型的结构和其他函数的私有性。

ins07.gif

#朗启动每个模块的球拍行表示该模块是用球拍语言实现的。在世界。Rkt,则需要额外导入txtadv导出的语法扩展和函数。rkt模块。

由于宏绑定是球拍语言的一部分,而不是作为一个单独的预处理器实现,宏绑定可以像变量绑定一样处理模块导入和导出。特别是定义定义动词宏可以看到动词构造函数由于词法作用域的规则,而代码在世界。由于相同的作用域规则,RKT模块不能直接访问动词。因为使用了定义动词在世界。rkt扩展到动词的使用,在宏扩展存在的情况下,球拍需要大量的语言机制来维持词汇作用域,但结果是语法扩展对程序员来说很容易。

模块化的游戏实现可以在线获得(参见附带的侧边栏的链接)。

回到顶部

模块语言

尽管世界。RKT模块不能直接访问构造函数等函数动词,该模块仍然可以访问所有的球拍语言和,通过需要,任何其他模块的导出。对世界有更多的限制。RKT可能适合于确保txtadv的假设。rkt感到满意。

要进行进一步控制,可以转换txtadv。从导出语言扩展的模块到导出语言扩展的模块的RKT。然后,相反地,从#朗球拍,世界。rkt始于

ins08.gif

就目前而言,s-exp表明语言的世界。rkt使用s表达式表示法(即圆括号),而txtadv. rkt使用s表达式表示法。RKT定义了语法形式。之后,s表达式和语法形式规范被组合成一个名称,类似于#朗球拍

随着世界的变化。Rkt,你可以改变txtadv。从RKT出口一切球拍:

ins09.gif

而不是(all-from-out球拍),你可以用(除外(全球拍)要求)拒绝的需要从world.rkt形式。或者,代替使用all-from-out然后命名要保留的绑定,您可以显式地只导出其中的某些片段球拍

txtadv的出口。RKT完全决定了世界中可用的绑定。rkt不仅有函数,还有句法形式等需要λ.例如,txtadv。RKT可以提供λ绑定到世界。RKT,它实现了一种不同于通常的功能λ,例如具有惰性求值的函数。

更常见的是,模块语言可以取代# % module-begin形式,隐式封装模块的主体。具体来说,txtadv。RKT可以提供替代方案# %模块体这力量的世界。RKT有一个单定义动词形式,一个define-everywhere形,序define-thing声明,以及一系列的define-place声明;如果世界。RKT有任何其他形式,它可以作为语法错误被拒绝。这样的约束可以实施限制,以限制txtadv的功能。RKT语言,但它们也可以用于提供特定于领域的检查和错误消息。

游戏实现了txtadv。RKT语言可在网上获得(请参阅附带的侧边栏url信息)。的# % module-begin在实现中需要替换定义动词紧随其后的是define-everywhere,然后允许任意数量的其他声明。该模块必须以一个place表达式结束,该表达式用作游戏的起始位置。

回到顶部

静态检查

define-verb, define-place,define-thing表单以与其他任何球拍定义相同的方式绑定名称,对动词、地点或事物的每个引用都是对已定义名称的球拍级引用。这种方法使得在球拍中实现的动词-响应表达式可以很容易地指代虚拟世界中的其他事物和地点。然而,这也意味着,将引用误用为一个东西会导致运行时错误。例如,错误地把沙漠作为一种东西

ins10.gif

只有当玩家进入房间时才会触发失败,而当游戏引擎试图打印房间内的东西时便会失败。

许多语言提供类型检查或其他静态类型,以确保没有某些运行时错误。球拍宏可以实现带有静态检查的语言,宏甚至可以实现语言扩展,在将类似的检查推迟到运行时的基本语言中执行静态检查。具体来说,你可以进行调整define-verb, define-place,define-thing检查某些引用,例如要求位置中的初始事物列表只包含被定义为事物的名称。类似地,可以检查作为带有响应的动词使用的名称,以确保它们被声明为动词,适当地及物或不及物。

实现静态检查通常需要比模式匹配宏表达能力更强的宏。在Racket中,任意编译时代码都可以充当语法形式的扩展器的角色,因为宏定义的最一般形式是

ins11.gif

在哪里transformer-expr生成函数的编译时表达式。函数必须接受一个实参,该实参是类的使用的表示id语法形式和函数必须产生使用扩展的表示。以同样的方式define-syntax-rule是define-syntax+语法规则一个单一的模式,语法规则是一个实参函数的简写,该函数分离特定形状的表达式(匹配模式)并为结果构造一个新表达式(基于模板)。

用于。的编译时语言transformer-expr可以与周围的运行时语言不同,但是#朗球拍使用与运行时表达式基本相同的语言为编译时表达式播下种子。可以将新的绑定引入到编译时阶段(要求(语法……)而不是仅仅需要,并且本地绑定可以通过包装的定义添加到编译时阶段begin-for-syntax

例如,要静态地检查动词、事物和地点,begin-for-syntax可以定义一个新的输入结构如图7要关联绑定gen-desert的类型“地方”.的#:属性道具:过程条款中的声明输入使类型化实例充当函数(原因在后面解释)。除了隐式函数外,该函数还有一个实参自我参数,但它忽略参数并返回输入实例的id

您可以使用输入通过改变define-place窗体来绑定地名id一个编译时输入记录。与此同时,define-place绑定生成的名称gen-id中显示的运行时位置记录图8

由于类型化记录充当函数,因此使用id扩大到gen-id,所以id仍然可以作为一个地方的直接参考。同时,其他宏可以查看id绑定并确定其展开将具有该类型“地方”

类来检查类型检查类型宏。的实现检查类型是在完整的代码在线,但其基本特性是它使用了一个编译时函数syntax-local-value获取标识符的编译时值;检查类型宏然后使用类型的?检查编译时值是否为类型声明,在这种情况下,它使用typed-type检查声明的类型是否为预期的类型。只要类型检查通过,检查类型展开到它的第一个参数。

define-place宏使用check-typed检查位置上的事物列表是否只包含被定义为事物的名称。的define-place宏也使用check-typed检查动词是否有应答的地方都被定义为不及物动词(参见图9).

define-one-verb宏必须更改以类似地将每个动词声明为任意一种类型“及物动词”“不及物动词”.的define-thing宏更改来声明其绑定为“东西”,它检查每个处理的动词是否定义为“及物动词”


球拍宏可以实现带有静态检查的语言,宏甚至可以实现语言扩展,在将类似的检查推迟到运行时的基本语言中执行静态检查。


看到侧边栏对于在线可用的代码的游戏与静态检查。

的实现检查记录表使用syntax-case的模式匹配功能语法规则,但将每个模式与表达式配对,而不是固定的模板。

回到顶部

新语法

一个为其他Racket程序员定义了自定义文本冒险语言的Racket程序员很可能会在这一点上止步不前。如果文本冒险语言是给那些不太熟悉《球拍》的人使用的,那么另一种符号可能是合适的。例如,其他人可能更喜欢world.rkt中的以下符号:

ins12.gif

在这种表示法中,而不是像定义动词而且define-everywhere,程序的部分由标记引入,例如= = = = = =动词而且= = =各个领域= = =.名字的= = = = = =动词节隐式定义动词,在后面通过逗号分隔的序列列出别名,后面是动词的可选描述。类似地,中的每个名称= = = = = =Section隐式定义了对动词的响应;响应仍然被写成球拍表达式,但如果需要,它们可以用任何替代符号表示。每件事和地点都有自己的分段定义,比如仙人掌,每宾语动词的反应方式与in相同= = = = = =

在world中启用了非s表达式语法。从RKT开始#朗读者“txtadv-reader.rkt”而不是#朗s-exp“txtadv.rkt”.的读者语言构造函数,不同于s-exp语言构造函数,将程序文本的解析交给命名模块导出的任意解析函数,在本例中为txtadv-reader.rkt。txtadv-reader中的解析器。rkt负责处理其余的文本并将其转换为s表达式表示法,包括引入txtadv。RKT作为解析世界的模块语言。rkt模块。

更准确地说,reader函数将输入解析为语法对象,该语法对象类似于s表达式,其中包含丰富的词汇上下文和源位置信息。它还充当宏转换器参数和结果的代码表示。语法对象抽象提供了字符级解析和树结构宏转换的清晰分离。语法对象的源位置部分自动将宏展开的结果连接回原始源;如果在world.生成的代码中发生运行时错误。Rkt,那么错误就可以指向相关的源。

带有非括号语法的游戏代码可以在线获得(参见侧边栏的网址).

txtadv-reader中的解析器。RKT是用正则表达式以一种特别原始的方式实现的。球拍发行版包含更好的解析工具,如Lex-和yacc风格的解析生成器。

回到顶部

IDE支持

s表达式表示法的好处之一是编程环境的功能很容易适应语法扩展,因为语法着色和括号匹配可以独立于宏展开。在描述世界的新语法中,其中一些好处是完整的,因为解析器用标识符保留源位置,而且代码最终扩展到球拍级别的绑定形式。例如,DrRacket中的Check Syntax按钮可以自动从cactus的绑定实例绘制箭头到cactus的每个绑定使用。

DrRacket需要语言实现者为IDE特性提供更多的帮助,比如语法着色,这些特性依赖于语言的字符级语法。填写这段示例文本冒险语言需要两个步骤:

  • 将该语言的阅读器安装为txtadv库集合,而不是依赖于相对路径,如txtadv-reader.rkt。移动到库集合的命名空间允许DrRacket和程序就使用哪种语言达成一致(不需要IDE的项目风格配置)。
  • 函数中添加一个函数txtadvReader模块,它标识对该语言的其他支持,例如实现动态语法着色的模块。同样,由于dr球拍和模块使用相同的模块语言规范,语法颜色可以精确地根据模块的语言和内容定制。

带有DrRacket插件的语法着色游戏代码可以在网上找到。你可以在附带的侧边栏

这个插件根据游戏语言的语法而不是球拍的默认规则为程序着色,突出红色的词汇语法错误。

回到顶部

更多的语言

球拍发行版的源代码包括几十个独特的#朗行。最常见的是#朗拍/基地的简化版#朗球拍.其他常见的行包括#朗潦草/手册文档来源,#朗拍/单位对于外部可链接组件,#朗方案对于遗留模块,和# lang / infotab设置为图书馆元数据。大多数球拍语言使用s表达式符号,但是潦草/手动是一个明显的例外;即使是喜欢用圆括号的Racketeers也承认s表达式对于文档散文来说是一个糟糕的符号。

球拍发行版中不同的语言因不同的原因而存在,它们在不同程度上使用球拍的语言创建工具。球拍开发人员不会轻易创建新语言,但新语言的好处有时会超过学习一种语言变体的成本。这些好处对于球拍用户和核心的球拍开发者来说都是唾手可得的。

球拍对s表达式语言和语言扩展的支持特别丰富,本文中的示例只触及了该工具的皮毛。球拍的非s表达式语法工具箱仍在不断发展,特别是在可组合解析器和语言触发的IDE插件方面。幸运的是,球拍的#朗协议将大部分剩余的工作移出核心系统,移到库中。这意味着,球拍用户和核心球拍开发者一样有权开发改进的语法工具。

ACM队列的q戳相关文章
queue.acm.org

不熟悉的DSL
Debasish Ghosh
http://queue.acm.org/detail.cfm?id=1989750

LINQ的世界
埃里克·梅耶尔
http://queue.acm.org/detail.cfm?id=2024658

群众的OCaml
Yaron明斯基
http://queue.acm.org/detail.cfm?id=2038036

回到顶部

参考文献

1.弗拉特,M,芬德勒,R.B. PLT。2011.球拍指南;http://docs.racket-lang.org/guide

2.游戏开发中的功能mzScheme dsl。函数式编程的商业用户(2011年)

3.面向语言的程序设计。软件概念和工具, 4(1994), 147161。

回到顶部

作者

马修Flatt他是犹他大学计算学院的副教授,主要研究可扩展编程语言、运行时系统和函数式编程的应用。他是Racket编程语言的开发者之一,也是入门编程教材的合著者,如何设计程序(麻省理工学院出版社,2001)。

回到顶部

数据

F1图1。根据肯·克雷默的研究,iPhone的价值分配。

F2图2。示例定义的地方。

F3图3。注册游戏元素定义。

F4图4。的define-place宏。

F5图5。广义define-place而且define-thing宏。

F6图6。的define-one-verb而且define-everywhere宏。

F7图7。的输入编译时的结构。

F8图8。修改后的define-place带有类型声明的宏。

F9图9。修改后的define-place带有类型检查的宏。

回到顶部


©2012 acm 0001-0782/12/0100 $10.00

允许为个人或课堂使用部分或全部作品制作数字或硬拷贝,但不得为盈利或商业利益而复制或分发,且副本在首页上附有本通知和完整的引用。除ACM外,本作品的其他组件的版权必须受到尊重。允许有信用的文摘。以其他方式复制、重新发布、在服务器上发布或重新分发到列表,都需要事先获得特定的许可和/或费用。请求发布的权限permissions@acm.org传真(212)869-0481。

数字图书馆是由计算机协会出版的。版权所有©2012 ACM, Inc.


没有发现记录

Baidu
map