Download PDF version of this article
这篇以及其他 acmqueue 文章已被翻译成葡萄牙语
葡萄牙语版 Q

在 Racket 中创建语言

有时候你只需要制造一个更好的捕鼠器。


Matthew Flatt,犹他大学


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

由于程序员经常构建特定于任务的工具,因此提高他们生产力的一种方法是为他们提供更好的工具制造工具。当工具采用程序生成器的形式时,这个想法导致了用于创建可直接扩展的语言的库。甚至可以鼓励程序员从一种更适合任务的语言的角度来思考问题。这种方法有时被称为面向语言的编程1

Racket 既是一种编程语言,也是一个用于构建编程语言的框架。一个 Racket 程序可以包含定义,这些定义扩展了语言的语法,以便在同一程序的后续部分中使用,并且语言扩展可以打包为模块,以便在多个程序中使用。Racket 支持从相对简单的语言扩展到完全新的语言的平滑过渡,因为像任何其他软件一样,编程工具很可能从简单开始,并随着对语言需求的增加而增长。

作为一个示例任务,考虑实现一个文本冒险游戏(也称为互动小说),玩家在游戏中输入命令以在虚拟世界中移动并与对象互动


你站在一片草地上。
北方有一栋房子。
> north
你站在一栋房子前面。
这里有一扇门。
> open door
门锁着。
>

为了使游戏有趣,程序员必须用具有丰富行为的地点和事物来填充虚拟世界。几乎任何编程语言都可以实现这个虚拟世界,但是选择正确的语言结构(即正确的工具)来表示每个游戏元素是开发过程中的关键步骤。

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


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


一些编程语言包含一些结构——例如重载或惰性求值——聪明的程序员可以利用这些结构来编码特定领域的语言。Racket 的设计更直接地解决了这个问题;它为程序员提供了显式扩展编程语言语法的工具。有些任务只需要对核心语言进行少量扩展,而另一些任务则受益于创建全新的语言。Racket 支持频谱的两端,并且它以一种允许从一端平滑过渡到另一端的方式进行。随着程序员对特定任务的需求或雄心壮志的增长,程序员可以利用 Racket 用于语言扩展和构建的统一框架的更多功能。


此处介绍的文本冒险示例说明了从 Racket 中的简单嵌入到单独的特定领域语言(包括用于语法着色的 IDE 支持)的演变过程,并沿途解释了相关的 Racket 细节;无需事先了解 Racket。喜欢更完整语言介绍的读者应查阅《Racket 指南》。2


这个例子在多种意义上都是一个“玩具”,但它也是行业实践的比例模型。大多数视频游戏开发商都使用自定义语言,包括用于实现神秘海域视频游戏系列内容的基于 Racket 的语言。3 显然,当数十亿美元的娱乐资金处于危险之中时,编程语言的选择至关重要——甚至到了创建新的专用语言的地步。


纯 Racket 世界

我们的文本冒险游戏包含一组固定的地点,例如草地、房屋或沙漠,以及一组固定的事物,例如门、钥匙或花朵。玩家使用命令在世界中导航并与事物互动,这些命令被解析为一个或两个单词:单个动词(即不及物动词,因为它没有目标对象),例如 helplook;或动词后跟事物的名称(即及物动词后跟名词),例如 open doorget key。导航词,例如 northin 被视为动词。用户可以使用 saveload 动词保存游戏,这些动词在任何地方都有效,并提示用户输入文件名。


要在 Racket 中实现文本冒险游戏,你将首先为三个游戏元素中的每一个声明结构类型



(structverb
(aliases            ;  符号列表
 Desc               ;  字符串
 transitive?))      ;  布尔值

(structthing
(name               ;  符号
 [state#:mutable]  ;  任何值
 actions))          ;  动词-函数对列表

(structplace
(desc               ;  字符串
 [things#:mutable];  事物列表
 actions))          ;  动词-函数对列表

Racket 是 Lisp 的一种方言,也是 Scheme 的后代,因此其语法使用括号和自由的标识符语法(例如,transitive?是一个标识符)。分号引入以换行符结尾的注释。方括号与圆括号可以互换,但在某些上下文中按惯例使用,例如将字段名称与修饰符分组。#:mutable修饰符将字段声明为可变的,因为字段默认是不可变的。


代码中的第一个struct形式将 verb 绑定到函数,为每个字段取一个参数并创建一个 verb 实例。例如,你可以定义一个south动词,别名为s,如下所示


(definesouth (verb (list 'south 's) "go south" #false))

Lisp 和 Racket 程序倾向于使用字符串作为要向最终用户显示的文本——例如,动词描述,例如"go south"。符号,用前导单引号书写(例如,'south),更常用于内部名称,例如动词别名。


给定south的定义和一个事物flower,你可以定义一个meadow地点,其中south动词将玩家移动到desertplace



(definemeadow (place "你在一片草地上。"
                      (list flower)
                      (list (cons south
                                  (lambda() desert)))))

函数list创建列表,而cons将两个值配对。cons函数通常将元素与列表配对以形成新列表,但这里cons用于将动词与实现动词响应的函数配对。 lambda 形式创建一个匿名函数,在本例中该函数期望零个参数。

当动词的响应函数产生一个地点时,例如desert示例中的地点,游戏执行引擎会将玩家移动到返回的地点。同时,游戏引擎对保存和加载游戏状态的支持需要地点与其名称之间的映射。(地点可以实现为可以序列化的对象,但恢复游戏既需要反序列化,也需要更新 Racket 级变量,例如meadow。)record-element!函数实现名称和地点之间的映射



(definenames (make-hash))    ; 符号到地点/事物
(defineelements (make-hash)) ;  地点/事物到符号

(define(record-element! name val)
 (hash-set! names name val)
 (hash-set! elements val name))

(define(name->element name)  (hash-ref names name))
(define(element->name obj)  (hash-ref elements obj))

因此,草地的完整实现是


(definemeadow (place ....)) ; 如上
(record-element! 'meadow meadow)

事物的定义和注册方式必须与地点大致相同。动词必须收集到一个列表中,供游戏的命令解析器使用。最后,解析和执行引擎需要一组在任何地方都有效的动词,每个动词都有其响应函数。所有这些部分构成了游戏实现中有趣的部分,而解析和执行引擎只是几十行静态基础设施。

完整的游戏实现可在线获取

https://queue.org.cn/downloads/2011/racket/0-longhand/txtadv+world.rkt

https://queue.org.cn/downloads/2011/racket/0-longhand/README.txt

请注意,构建虚拟世界所需的代码特别冗长。


语法抽象

虽然上一节的数据表示选择对于 Racket 程序来说是典型的,但 Racket 程序员不太可能编写重复的代码来直接定义和注册地点,因为它包含如此多的样板lists、conses 和lambdas。相反,Racket 程序员会编写



(define-place meadow
  "你在一片草地上。"
  [flower]
  ([south desert]))

,并使用基于模式的宏将define-place形式添加到 Racket 中。这种宏的最简单形式使用define-syntax-rule:


(define-syntax-rule (define-place id desc [thng] ([vrb  expr]))
 (begin
   (defineid(place desc
                     (listthng )
                     (list(consvrb(lambda() expr )))))
   (record-element!'id  id )))

形式紧跟在define-syntax-rule之后的是模式,模式之后的形式是模板。与模式匹配的宏的使用将被宏的模板替换,模数为模式变量对其匹配项的替换。此模式中的id, desc, thng, vrbexpr


标识符是模式变量。define-place请注意,desert形式不能是函数。south之后的south表达式通常是一个表达式,其求值必须延迟到输入meadow命令时。更重要的是,该形式应绑定变量


函数define-place,以便命令的 Racket 表达式可以直接引用该地点。此外,变量的源名称(而不是其值)用于在元素表中注册该地点。



(define-syntax-rule (define-place id desc
到目前为止的宏仅匹配一个地方中的一个事物和一个动词和响应表达式。要推广到任意数量的事物、动词和表达式,你需要向模式添加省略号thng ...]
                      [                      ([vrb expr...))
 (begin
]  defineid (place desc
                     (list thng    ()
                     (list (cons vrb (lambda () expr ))
 ...)))
]  record-element! 'id id )))

省略号以显而易见的方式工作,借助这种通用的define-place,你可以将仙人掌和钥匙都放在沙漠中,并对除north以外的方向动词做出响应,方法是留在沙漠中



(define-place desert
 "你在沙漠中。"
 [cactus key]
 ([north meadow]
  [south desert]
  [east desert]
  [west desert]))

事物的宏同样简单明了


(define-syntax-rule (define-thing id
到目前为止的宏仅匹配一个地方中的一个事物和一个动词和响应表达式。要推广到任意数量的事物、动词和表达式,你需要向模式添加省略号                      ([vrb expr...)
 (begin
]  defineid
      (thing'id #false(list(consvrb (lambda() expr )) ...)))
]  record-thing! 'id id )))

动词稍微棘手,因为你想使简单动词的指定特别紧凑,并且你需要一种模式用于不及物动词,另一种模式用于及物动词。以下示例说明了目标语法



(define-verbs all-verbs
  [quit]
  [north (=n) "go  north"]
  [knock_ _]
  [get _ (=grab  take) "take"])

此示例定义了四个动词quit,作为没有别名的不及物动词;north,作为别名为n且首选描述为go north; knock_的不及物动词;get,作为没有别名的及物动词(以下划线表示);以及,作为别名为grabtake且首选描述为的及物动词。最后,所有这些动词都收集到一个列表中,该列表绑定到all-verbs


,供游戏的命令解析器使用。实现define-verbs=grab_形式需要更通用的模式匹配,以支持不同形状的动词规范并匹配作为字面量。define-verbs可以推迟处理单个动词的工作到define-one-verb宏,该宏使用grabdefine-syntax:



(宏,该宏使用可以推迟处理单个动词的工作到
 (define-syntax (= _)
   [(syntax-rules id (= one-verb ...) desc)
aliasdefineid (verb (list 'id      ( ...) desc 'alias#false
))]    syntax-rules id _ (= one-verb ...) desc)
aliasdefineid (verb (list 'id      ( ...) desc     [(#true
))]    syntax-rules id)
     (defineid (verb (list 'id))]   ) ( 'idsymbol->string'alias))]
))]    syntax-rules id _)
     (defineid (verb(list'id ) ( ) ( 'idsymbol->string    [())]))

函数=grab_ )define-syntax=grab_之后的括号中表示


(define-verbs all-verbs
是字面量,而不是模式变量,在随后的模式中。之后的每个模式都有一个对应的模板。因此,在
  [get _ (=  ....) "take"])

grab take实现中,可以推迟处理单个动词的工作到扩展将最后一个子句转换为


(define-one-verb get _ (=   ....) "take")

的使用。这与syntax-rules的第一个模式匹配,并扩展为


(defineget (verb (list 'get 'grab 'take) "take" #true))


最后,创建define-everywhere形式,用于定义在整个世界中都有效的动词响应,正如诸如savegrab:


(define-syntax-rule (define-everywhere id ([vrb  expr ] ...load
之类的动词所需的那样。defineid (list (cons vrb (lambda () expr )) ...)))

(  (
define-everywhere  everywhere-actionsquit (begin (  ([ printf) ("Bye!\n"))]
exitsave (   [)]
exit (save-game)]
load-game))

函数define-place, define-thing   ....

define-verb


语法抽象的示例。它们抽象了重复的语法模式,以便程序员可以避免样板代码,并专注于创建有趣的动词、地点和事物。

修订后的游戏实现具有虚拟世界的紧凑且可读的实现,可在线获取

https://queue.org.cn/downloads/2011/racket/1-monolith/txtadv+world.rkt

https://queue.org.cn/downloads/2011/racket/1-monolith/README.txt


语法扩展placegrabrecord-element!对编写单个文本冒险游戏感兴趣的 Racket 程序员很可能会在此时停止扩展语言。但是,如果文本冒险引擎应该可重用于多个世界,则 Racket 程序员很可能会超越语法抽象,转向语法扩展define-place抽象和扩展之间的区别部分在于观察者的角度,但扩展表明诸如define-place之类的函数可以保持私有,而definegrablambda.


则导出供世界定义模块使用,具有独立于实现的语义。在世界定义模块中,诸如实现, define-place, define-thingdefine-everywhere之类的宏与诸如


之类的内置形式具有相同的状态。要进行这种转变,你可以将
(定义放在它们自己的模块中,称为 world.rkt。#lang)

(racket)
(require)
("txtadv.rkt") ...
(define-verbs ....) ...

define-everywhere ....实现define-thing ....   [grabsave-gamedefine-place ....


之类的内置形式具有相同的状态。要进行这种转变,你可以将
(此模块导入 txtadv.rkt,后者导出、等等,以及动词响应中使用的函数,例如
        。同时,txtadv.rkt 保留了实现世界数据类型的结构和其他函数的私有性。

           [
        save-game
        ....)

(structprovide)
....
(define-syntax-rule (define-verbs  define-thing) ....)
....

函数define-place define-everywhereverb  ....要进行这种转变,你可以将define-verbs  ....定义放在它们自己的模块中,称为 world.rkt。


#lang racket实现开头的行表示该模块以verb语言实现。在 world.rkt 中,verb还导入 txtadv.rkt 模块导出的语法扩展和函数。实现由于宏绑定是 Racket 语言的一部分,而不是作为单独的预处理器实现的,因此宏绑定可以像变量绑定一样与模块导入和导出一起工作。特别是,verb宏的定义可以看到

构造函数,因为词法作用域的规则,而 world.rkt 模块中的代码无法直接访问

,因为相同的范围规则。由于 world.rkt 中

的使用扩展为

的使用,因此 Racket 需要大量的语言机制来在宏扩展存在的情况下维护词法作用域,但结果是语法扩展对于程序员来说很容易。

模块化游戏实现可在线获取

https://queue.org.cn/downloads/2011/racket/2-modules/txtadv.rktverbhttps://queue.org.cn/downloads/2011/racket/2-modules/world.rkt定义放在它们自己的模块中,称为 world.rkt。https://queue.org.cn/downloads/2011/racket/2-modules/README.txt


模块语言define-place define-everywhere虽然 world.rkt 模块无法直接访问构造函数,例如


之类的内置形式具有相同的状态。,但该模块仍然可以访问所有 Racket 语言,并且可以通过 #lang

访问任何其他模块的导出。对 world.rkt 的更多约束可能适合于确保满足 txtadv.rkt 的假设。,但该模块仍然可以访问所有 Racket 语言,并且可以通过要施加进一步的控制,你可以将 txtadv.rkt 从导出语言扩展的模块转换为导出语言的模块。然后,world.rkt 不是以define-place define-everywhere.


开头,而是以



之类的内置形式具有相同的状态。要进行这种转变,你可以将
(此模块导入 txtadv.rkt,后者导出define-verbs  define-thing
        (开头。要进行这种转变,你可以将))
....

目前,表示 world.rkt 的语言使用 S 表达式表示法(即括号),而 txtadv.rkt 定义语法形式。稍后,S 表达式和语法形式规范将组合成一个名称,类似于。除了更改 world.rkt 之外,你还可以更改 txtadv.rkt 以导出 racket 的所有内容all-from-out。代替定义放在它们自己的模块中,称为 world.rkt。(all-from-out racket)开头。,你可以使用


(except-out (all-from-out racket) require)定义放在它们自己的模块中,称为 world.rkt。从 world.rkt 中排除lambda形式。或者,你可以显式导出 racket 中的某些部分,而不是使用lambda,然后命名要排除的绑定。lambdatxtadv.rkt 的导出完全决定了 world.rkt 中可用的绑定——不仅是函数,还包括语法形式,例如


。例如,txtadv.rkt 可以向 world.rkt 提供一个绑定,该绑定实现与通常的不同类型的函数,例如具有惰性求值的函数。更常见的是,模块语言可以替换隐式包装模块主体的实现#%module-begindefine-everywhere形式。具体来说,txtadv.rkt 可以提供一个替代的define-thing#%module-bodydefine-place,它强制 world.rkt 具有单个

形式、单个


形式、一系列

声明和一系列

声明;如果 world.rkt 有任何其他形式,则可以将其作为语法错误拒绝。这些约束可以强制执行限制以限制 txtadv.rkt 语言的功能,但它们也可以用于提供特定领域的检查和错误消息。

函数。例如,txtadv.rkt 可以向 world.rkt 提供一个使用 txtadv.rkt 语言实现的游戏可在线获取实现https://queue.org.cn/downloads/2011/racket/3-module-lang/txtadv.rktdefine-everywherehttps://queue.org.cn/downloads/2011/racket/3-module-lang/world.rkt


https://queue.org.cn/downloads/2011/racket/3-module-lang/README.txt

函数   ...., define-placedefine-thing替换在实现中需要,后跟desert,然后允许任意数量的其他声明。该模块必须以 place 表达式结尾,该表达式用作游戏的起始位置。



(静态检查
  形式以与任何其他 Racket 定义相同的方式绑定名称,并且对动词、地点或事物的每次引用都是对已定义名称的 Racket 级引用。这种方法使
  [verb-response]
  ([表达式(在 Racket 中实现)可以轻松引用虚拟世界中的其他事物和地点。然而,这也意味着,错误地将引用用作事物可能会导致运行时错误。例如,在]))

中将错误地引用为事物define-place room


"你在房子里。"   ...., define-placedefine-thingtrophy desert


out house-front


(宏,该宏使用仅当玩家进入 )

房间时才会触发故障,并且当游戏引擎尝试打印地点内的事物时会失败。许多语言提供类型检查或其他静态类型,以确保不存在某些运行时错误。Racket 宏可以实现具有静态检查的语言,宏甚至可以实现语言扩展,这些扩展在基本语言中执行静态检查,而基本语言将类似的检查推迟到运行时。具体来说,你可以调整以检查某些引用,例如要求地点中的初始事物列表仅包含定义为事物的名称。类似地,可以检查用作带有响应的动词的名称,以确保它们被声明为动词,适当地及物或不及物。id实现静态检查通常需要比模式匹配宏更具表现力的宏。在 Racket 中,任意编译时代码都可以充当语法形式的扩展器,因为宏定义的最通用形式是define-syntax-ruleid transformer-expr宏,该宏使用,其中define-syntaxtransformer-exprdefine-syntax是一个编译时表达式,它生成一个函数。该函数必须接受一个参数,该参数是


语法形式用法的表示形式,并且该函数必须生成用法扩展的表示形式。就像许多语言提供类型检查或其他静态类型,以确保不存在某些运行时错误。Racket 宏可以实现具有静态检查的语言,宏甚至可以实现语言扩展,这些扩展在基本语言中执行静态检查,而基本语言将类似的检查推迟到运行时。具体来说,你可以调整define-place define-everywhere加上和单个模式的简写一样,是单参数函数的简写,该函数分解特定形状的表达式(匹配模式)并构造新表达式作为结果(基于模板)。定义放在它们自己的模块中,称为 world.rkt。用于的编译时语言可以与周围的运行时语言不同,但.


为编译时表达式的语言播种,其语言与运行时表达式的语言基本相同。可以使用的编译时语言可以与周围的运行时语言不同,但(require (for-syntax ....))



(的编译时语言可以与周围的运行时语言不同,但
 (struct而不是仅仅使用
]  id     将新绑定引入到编译时阶段,并且可以通过包装在
     begin-for-syntax)  中的定义将本地绑定添加到编译时阶段。
例如,要静态检查动词、事物和地点,可以定义一个新的类型化结构(lambda(typed) (; 一个标识符))))

type;  一个字符串    #:property#prop:procedure


(self stxtyped-id self)

identifier被写成符号,但带有typed-id self前缀,因此typed #'gen-desert"place"而不是仅仅使用将绑定gen-desert与类型而不是仅仅使用关联。id.

#:property prop:procedure而不是仅仅使用声明中的define-place子句使类型化实例充当函数(原因稍后解释)。该函数除了隐式idself而不是仅仅使用参数外,还接受一个参数,但它忽略该参数并返回define-place实例的。你可以使用,方法是将



(define-syntax-rule (define-place id ....)
 (begin
]  define。你可以使用 (形式更改为将地点名称)) 绑定到编译时
]  宏,该宏使用id (而不是仅仅使用 #'。你可以使用 typed-id self))
]  record-element! 'id id )))

记录。同时,而不是仅仅使用将生成的名称idgen-id。你可以使用绑定到运行时地点记录idplace  ....place; 如前所述id。由于typed-id self.

记录充当函数,因此的使用扩展为,因此仍然可以用作对的直接引用。同时,其他宏可以查看绑定并确定其扩展将具有类型。其他宏通过使用仍然可以用作对check-type 宏来检查类型。check-type的实现在完整的在线代码中,但其基本特征是它使用编译时函数syntax-local-value仍然可以用作对来获取标识符的编译时值;然后


函数define-place宏使用typed?来检查编译时值是否为类型声明,在这种情况下,它使用placetyped-typedefine-place来检查声明的类型是否为预期类型。只要类型检查通过,typed?就会扩展为其第一个参数。place宏使用



(define-syntax-rule (define-place id
                     desc
到目前为止的宏仅匹配一个地方中的一个事物和一个动词和响应表达式。要推广到任意数量的事物、动词和表达式,你需要向模式添加省略号thng    (]
                      [                      ([vrb expr...))
 (begin
]  define。你可以使用
check-typedplace desc
来检查list (仍然可以用作对 thng 处的的事物列表是否仅包含定义为事物的名称。宏还使用...)
来检查list (cons (仍然可以用作对 vrb 来检查在)
中具有响应的动词是否被定义为不及物动词lambda      (expr ))
             ()))
]  宏,该宏使用id (而不是仅仅使用 #'。你可以使用 typed-id self))
]  record-element! 'id id )))

函数可以推迟处理单个动词的工作到"thing")  从 world.rkt 中排除"intransitive  verb"前缀,因此define-thing                          (处的的事物列表是否仅包含定义为事物的名称。 ())  .

                    ...


宏必须更改为类似地将每个动词声明为类型

"transitive verb"


"intransitive verb"macro 更改为将其绑定声明为,并检查处理的每个动词是否定义为。具有静态检查的游戏代码可在线获取define-syntaxhttps://queue.org.cn/downloads/2011/racket/4-type/txtadv.rkt


https://queue.org.cn/downloads/2011/racket/4-type/world.rkt

https://queue.org.cn/downloads/2011/racket/4-type/README.txt


之类的内置形式具有相同的状态。check-form  的实现使用

syntax-case
north,,它提供
的模式匹配功能,但将每个模式与表达式而不是固定模板配对。
get_,新语法_,为其他 Racket 程序员定义自定义文本冒险语言的 Racket 程序员特别有可能在此处停止。但是,如果文本冒险语言要供不太熟悉 Racket 的其他人使用,则不同的表示法可能更合适。例如,其他人可能更喜欢 world.rkt 中的以下表示法_
 "take"
....
#reader
save
 (   [)

 (save-game)
....
"txtadv-reader.rkt"
===VERBS===
get
  n
....
 "go  north"
 knock_
 get
 grab take, ===EVERYWHERE===]
===THINGS===
---cactus---
....

"Ouch!"实现grabdefine-everywhere===PLACES===syntax-casegrab#reader---desert---syntax-case"你在沙漠中。"#reader [===VERBS===cactus#reader.


keynorth    startsouth    desert在这种表示法中,程序的各个部分不是由诸如前缀,因此check-form之类的形式引入,而是由诸如,但该模块仍然可以访问所有 Racket 语言,并且可以通过之类的标签引入。部分中的名称隐式定义动词,并在之后通过逗号分隔的序列列出别名,后跟动词的可选描述。类似地,部分中的每个名称都隐式定义对动词的响应;响应仍然写为 Racket 表达式,但如果需要,它们可以是任何替代表示法。每个事物和地点都由其自己的小节定义,例如部分中的名称隐式定义动词,并在之后通过逗号分隔的序列列出别名,后跟动词的可选描述。类似地,,对象动词响应的方式与中相同。通过以#lang reader "txtadv-reader.rkt"而不是


#lang s-exp "txtadv.rkt"#lang reader "txtadv-reader.rkt"开头,在 world.rkt 中启用非 S 表达式语法。与

语言构造函数不同,


语言构造函数将程序文本的解析推迟到命名模块导出的任意解析函数,在本例中为

txtadv-reader.rkt

。来自

的解析器负责处理文本的其余部分,并将其转换为 S 表达式表示法,包括引入

txtadv.rkt部分中的名称隐式定义动词,并在之后通过逗号分隔的序列列出别名,后跟动词的可选描述。类似地,作为已解析


world.rkt

模块的模块语言。 take更准确地说,读取器函数将输入解析为语法对象,该对象类似于富含词法上下文和源位置信息的 S 表达式。它还充当宏转换器参数和结果的代码表示形式。语法对象抽象提供了字符级解析和树状宏转换的清晰分离。语法对象的源位置部分自动将宏扩展的结果连接回原始源;如果在从 take.


生成的代码中发生运行时错误,则该错误可以指向相关源。

具有非括号语法的游戏代码可在线获取https://queue.org.cn/downloads/2011/racket/5-lang/txtadv-reader.rkthttps://queue.org.cn/downloads/2011/racket/5-lang/txtadv.rkt部分中的名称隐式定义动词,并在之后通过逗号分隔的序列列出别名,后跟动词的可选描述。类似地,https://queue.org.cn/downloads/2011/racket/5-lang/world.rkt

https://queue.org.cn/downloads/2011/racket/5-lang/README.txthttps://queue.org.cn/downloads/2011/racket/5-lang/txtadv-reader.rkt中的解析器以特别原始的方式使用正则表达式实现。 Racket 发行版包含更好的解析工具,例如 Lex 和 Yacc 风格的解析器生成器。

IDE 支持

S 表达式表示法的好处之一是编程环境的功能可以轻松适应语法扩展,因为语法着色和括号匹配可以独立于宏扩展。对于描述世界的新语法,其中一些好处仍然存在,因为解析器将源位置与标识符保持在一起,并且代码最终扩展为 Racket 级绑定形式。例如,DrRacket 中的“检查语法”按钮可以自动从

的绑定实例绘制箭头到

的每个绑定用法。

DrRacket 需要语言实现者提供更多帮助来实现 IDE 功能,例如语法着色,这取决于语言的字符级语法。填写此示例文本冒险语言的这一部分需要两个步骤

1. 将语言的读取器安装为


txtadv


库集合,而不是依赖于相对路径,例如

。移动到库集合的命名空间允许 DrRacket 和程序就正在使用的语言达成一致(无需 IDE 的项目风格配置)。之类的内置形式具有相同的状态。2. 将一个函数添加到读取器模块,该函数标识对语言的其他支持,例如实现即时语法着色的模块。同样,由于 DrRacket 和模块使用模块语言的相同规范,因此语法颜色可以精确地针对模块的语言和内容进行定制。带有 DrRacket 插件用于语法着色的游戏代码可在线获取define-place define-everywherehttps://queue.org.cn/downloads/2011/racket/6-color/txtadv.rkthttps://queue.org.cn/downloads/2011/racket/6-color/world.rkthttps://queue.org.cn/downloads/2011/racket/6-color/README.txthttps://queue.org.cn/downloads/2011/racket/6-color/lang/color.rkthttps://queue.org.cn/downloads/2011/racket/6-color/lang/reader.rkt此插件根据游戏语言的语法而不是 Racket 的默认规则对程序进行着色,以红色突出显示词法语法错误。更多语言Racket 发行版的源代码包含数十个独特的行。最常见的是#lang racket/base


Racket 发行版中存在不同的语言是出于不同的原因,并且它们对 Racket 的语言创建工具的使用程度也不同。Racket 开发者不会轻易创建新语言,但新语言的好处有时会超过学习语言变体的成本。Racket 用户和核心 Racket 开发者都可以同样轻松地获得这些好处。


Racket 对 S 表达式语言和语言扩展的支持尤其丰富,本文中的示例仅触及了该工具箱的表面。Racket 用于非 S 表达式语法的工具箱仍在发展中,尤其是在可组合解析器和语言触发的 IDE 插件方面。幸运的是,Racket 的之类的内置形式具有相同的状态。协议将剩余的大部分工作从核心系统移出并放入库中。这意味着 Racket 用户和核心 Racket 开发者一样有能力开发改进的语法工具。


参考文献

1. Ward, M. 1994. 面向语言的编程。《软件 - 概念与工具》15(4): 147-161。


2. Flatt, M.,Findler, R. B.,PLT。2011。《Racket 指南》; http://docs.racket-lang.org/guide

3. Liebgold, D. 2011. 游戏开发中的函数式 mzScheme DSL。《函数式编程商业用户会议》报告。

喜欢还是讨厌?请告诉我们

[email protected]


Matthew Flatt 是犹他大学计算机学院的副教授,他在该学院研究可扩展编程语言、运行时系统和函数式编程的应用。他是 Racket 编程语言的开发者之一,也是入门编程教科书《如何设计程序》(MIT Press,2001 年)的合著者。

© 2011 1542-7730/11/1000 $10.00

acmqueue

最初发表于 Queue 杂志第 9 卷,第 11 期
数字图书馆 中评论本文





更多相关文章

Matt Godbolt - C++ 编译器中的优化
在向编译器提供更多信息时需要权衡:这可能会使编译速度变慢。链接时优化等技术可以为您提供两全其美的效果。编译器中的优化不断改进,即将到来的间接调用和虚函数分派的改进可能很快会带来更快的多态性。


Ulan Degenbaev、Michael Lippautz、Hannes Payer - 作为合资企业的垃圾回收
跨组件追踪是一种解决跨组件边界引用循环问题的方法。一旦组件可以形成具有跨 API 边界的非平凡所有权的任意对象图,就会出现此问题。CCT 的增量版本在 V8 和 Blink 中实现,从而能够以安全的方式有效且高效地回收内存。


David Chisnall - C 不是一种低级语言
鉴于最近的 Meltdown 和 Spectre 漏洞,值得花一些时间研究根本原因。这两种漏洞都涉及处理器推测性地执行超出某种访问检查的指令,并允许攻击者通过侧信道观察结果。导致这些漏洞以及其他几个漏洞的功能的添加,是为了让 C 程序员继续相信他们正在使用低级语言进行编程,但这种情况已经几十年没有发生过了。


Tobias Lauinger、Abdelberi Chaabane、Christo Wilson - 你不应该依赖我
大多数网站都使用 JavaScript 库,其中许多库已知存在漏洞。了解问题的范围以及包含库的许多意外方式,只是改进情况的第一步。这里的目标是,本文中包含的信息将有助于为社区提供更好的工具、开发实践和教育工作。





© 保留所有权利。

© . All rights reserved.