Racket编程指南——17 创造语言
17 创造语言
前一章中定义的宏功能允许程序员定义语言的语法扩展,但宏有两种限制:
宏不能限制上下文中可用的语法或改变包围的表的意义;
宏只能在语言的词汇约定参数内扩展语言的语法,例如用括号将宏名称与其子表分组,以及用标识、关键字和核心语法。
读取器和展开器层之间的区别在《列表和Racket语法》中介绍。
也就是说,宏只能扩展语言,并且只能在展开器层进行扩展。Racket提供了额外的功能,用于定义展开器层的起始点、读取器层、定义读取器层的起始点,以及将读取器和展开器起始点封装为方便命名的语言。
17.1 模块语言 |
17.1.1 隐式表绑定 |
17.1.2 使用#lang s-exp |
17.2 读取器扩展 |
17.2.1 源位置 |
17.2.2 可读表 |
17.3 定义新的#lang语言 |
17.3.1 指定#lang语言 |
17.3.2 使用#lang reader |
17.3.3 使用#lang s-exp syntax/module-reader |
17.3.4 安装语言 |
17.3.5 源处理配置 |
17.3.6 模块处理配置 |
17.1 模块语言
当使用通常写法module表来编写模块时,在新模块名称之后指定的模块路径将为模块提供初始导入。由于初始导入模块甚至决定了模块主体中可用的最基本绑定,例如require,因此初始导入可以称为模块语言(module language)。
最常见的模块语言是racket或racket/base,但你可以通过定义合适的模块来定义自己的模块语言。例如,使用provide子表,如all-from-out、except-out和rename-out,你可以添加、删除或重命名racket中的绑定,以生成模块语言,它是racket}的变体:
《module表》介绍了module表的通常写法。
> (module raquet racket (provide (except-out (all-from-out racket) lambda) (rename-out [lambda function])))
> (module score 'raquet (map (function (points) (case points [(0) "love"] [(1) "fifteen"] [(2) "thirty"] [(3) "forty"])) (list 0 2))) > (require 'score) '("love" "thirty")
17.1.1 隐式表绑定
如果在定义自己模块语言时试图从racket中删除太多内容 ,那么生成的模块将不再作为模块语言正常工作:
> (module just-lambda racket (provide lambda))
> (module identity 'just-lambda (lambda (x) x)) eval:2:0: module: no #%module-begin binding in the module's
language
in: (module identity (quote just-lambda) (lambda (x) x))
#%module-begin表是一种封装模块主体的隐式表。它必须由要用作模块语言的模块提供:
> (module just-lambda racket (provide lambda #%module-begin))
> (module identity 'just-lambda (lambda (x) x)) > (require 'identity) #<procedure>
racket/base提供的其它隐式表包括:用于函数调用的#%app、用于文本的#%datum和用于没有绑定的标识#%top:
> (module just-lambda racket (provide lambda #%module-begin ; ten needs these, too: #%app #%datum))
> (module ten 'just-lambda ((lambda (x) x) 10)) > (require 'ten) 10
隐式表,如#%app可以在一个模块中显式地使用,但它们的存在主要是为了允许模块语言限制或更改隐式使用的含义。例如,lambda-calculus模块语言可能会将函数限制为单个参数,限制函数调用以提供单个参数,将模块主体限制为单个表达式,禁止使用文本,并将未绑定标识视为未解释的符号:
> (module lambda-calculus racket (provide (rename-out [1-arg-lambda lambda] [1-arg-app #%app] [1-form-module-begin #%module-begin] [no-literals #%datum] [unbound-as-quoted #%top])) (define-syntax-rule (1-arg-lambda (x) expr) (lambda (x) expr)) (define-syntax-rule (1-arg-app e1 e2) (#%app e1 e2)) (define-syntax-rule (1-form-module-begin e) (#%module-begin e)) (define-syntax (no-literals stx) (raise-syntax-error #f "no" stx)) (define-syntax-rule (unbound-as-quoted . id) 'id))
> (module ok 'lambda-calculus ((lambda (x) (x z)) (lambda (y) y))) > (require 'ok) 'z
> (module not-ok 'lambda-calculus (lambda (x y) x)) eval:4:0: lambda: use does not match pattern: (lambda (x)
expr)
in: (lambda (x y) x)
> (module not-ok 'lambda-calculus (lambda (x) x) (lambda (y) (y y))) eval:5:0: #%module-begin: use does not match pattern:
(#%module-begin e)
in: (#%module-begin (lambda (x) x) (lambda (y) (y y)))
> (module not-ok 'lambda-calculus (lambda (x) (x x x))) eval:6:0: #%app: use does not match pattern: (#%app e1 e2)
in: (#%app x x x)
> (module not-ok 'lambda-calculus 10) eval:7:0: #%datum: no
in: (#%datum . 10)
模块语言很少重新定义#%app、#%datum和#%top,但重新定义#%module-begin往往更为有用。例如,当使用模块构建HTML页面的描述时,如果描述从模块导出为页(page),那么另一个#%module-begin可以帮助消除provide和准引用样板,就像在"html.rkt"所示:
"html.rkt"
#lang racket (require racket/date) (provide (except-out (all-from-out racket) #%module-begin) (rename-out [module-begin #%module-begin]) now) (define-syntax-rule (module-begin expr ...) (#%module-begin (define page `(html expr ...)) (provide page))) (define (now) (parameterize ([date-display-format 'iso-8601]) (date->string (seconds->date (current-seconds)))))
使用"html.rkt"模块语,可以描述一个简单的网页,而无需显式定义或导出页,并以quasiquote模式而不是表达式模式开始:
> (module lady-with-the-spinning-head "html.rkt" (title "Queen of Diamonds") (p "Updated: " ,(now))) > (require 'lady-with-the-spinning-head) > page '(html (title "Queen of Diamonds") (p "Updated: " "2022-11-14"))
17.1.2 使用#lang s-exp
在#lang级别实现语言比声明单个模块更复杂,因为#lang允许程序员控制语言的多个不同方面。然而,s-exp语言作为一种元语言,使用带#lang简写的模块语言:
#lang s-exp module-name form ...
等同于
(module name module-name form ...)
其中name源自包含#lang程序的源文件。名称s-exp是S-expression的缩写,它是读取器级词汇约定的传统名称:括号、标识、数字、带反斜杠转义的双引号字符串等等。
使用#lang s-exp,前面的lady-with-the-spinning-head例子可以写得更简洁:
#lang s-exp "html.rkt" (title "Queen of Diamonds") (p "Updated: " ,(now))
在这个指南的稍后边,《定义新的#lang语言》会讲解如何定义自己的#lang语言,但是首先我们讲解你如何写针对Racket读取器(reader)级的扩展。
17.2 读取器扩展
(part ("(lib scribblings/reference/reference.scrbl)" "parse-reader")) in The Racket Reference provides more on 读取器扩展.
Racket语言的读取器(reader)层可以通过#reader表进行扩展。读取器扩展实现为一个以#reader命名的模块。该模块导出将原始字符解析为扩展器层使用的表的函数。
#reader的语法是
#reader ‹module-path› ‹reader-specific›
其中,‹module-path›命名了一个模块,该模块提供read和read-syntax函数。‹reader-specific›部分是由‹module-path›中的read和read-syntax函数确定的字符序列。
例如,假设文件"five.rkt"包含
"five.rkt"
#lang racket/base (provide read read-syntax) (define (read in) (list (read-string 5 in))) (define (read-syntax src in) (list (read-string 5 in)))
那么,程序
#lang racket/base '(1 #reader"five.rkt"234567 8)
等价于
#lang racket/base '(1 ("23456") 7 8)
因为"five.rkt"的read和read-syntax函数都从输入流中读取五个字符,并把它们放入字符串,然后放入列表。"five.rkt"中的读取器函数不必遵循Racket词法约定,将连续序列234567视为单个数字。由于只有23456部分被read或read-syntax使用,所以7仍然需要以通常的Racket方式进行解析。类似地,"five.rkt"中的读取器函数不必忽略空白,并且
#lang racket/base '(1 #reader"five.rkt" 234567 8)
等价于
#lang racket/base '(1 (" 2345") 67 8)
因为紧跟"five.rkt"后面的第一个字符是空格。
REPL中也可以使用#reader表:
> '#reader"five.rkt"abcde '("abcde")
17.2.1 源位置
read和read-syntax的区别在于,read用于数据,而 read-syntax用于解析程序。更准确地说,当通过Racket的read解析封闭流时,将使用read函数,当Racket的read-syntax函数解析封闭流时,使用read-syntax。没有什么需要read和read-syntax用同样的方式解析输入,但使它们不同会混淆程序员和工具。
read-syntax函数可以返回与read相同类型的值,但它通常应该返回一个语法对象(syntax object),该对象将解析的表达式与源位置连接起来。与"five.rkt"示例不同,read-syntax函数通常直接实现以生成语法对象,然后read可以使用read-syntax并去掉语法对象包装来产生原始结果。
下面的"arith.rkt"模块实现了一个读取器,用于将简单的中缀算术表达式解析为Racket表。例如,1*2+3解析为Racket表(+ (* 1 2) 3)。支持的运算符是+、-、*和/,而操作数可以是无符号整数或单字母变量。该实现使用port-next-location获取当前源位置,并使用 datum->syntax将原始值转换为语法对象。
"arith.rkt"
#lang racket (require syntax/readerr) (provide read read-syntax) (define (read in) (syntax->datum (read-syntax #f in))) (define (read-syntax src in) (skip-whitespace in) (read-arith src in)) (define (skip-whitespace in) (regexp-match #px"^\\s*" in)) (define (read-arith src in) (define-values (line col pos) (port-next-location in)) (define expr-match (regexp-match ; Match an operand followed by any number of ; operator–operand sequences, and prohibit an ; additional operator from following immediately: #px"^([a-z]|[0-9]+)(?:[-+*/]([a-z]|[0-9]+))*(?![-+*/])" in)) (define (to-syntax v delta span-str) (datum->syntax #f v (make-srcloc delta span-str))) (define (make-srcloc delta span-str) (and line (vector src line (+ col delta) (+ pos delta) (string-length span-str)))) (define (parse-expr s delta) (match (or (regexp-match #rx"^(.*?)([+-])(.*)$" s) (regexp-match #rx"^(.*?)([*/])(.*)$" s)) [(list _ a-str op-str b-str) (define a-len (string-length a-str)) (define a (parse-expr a-str delta)) (define b (parse-expr b-str (+ delta 1 a-len))) (define op (to-syntax (string->symbol op-str) (+ delta a-len) op-str)) (to-syntax (list op a b) delta s)] [_ (to-syntax (or (string->number s) (string->symbol s)) delta s)])) (unless expr-match (raise-read-error "错误的算术表达式" src line col pos (and pos (- (file-position in) pos)))) (parse-expr (bytes->string/utf-8 (car expr-match)) 0))
如果在表达式位置使用"arith.rkt"读取器,则其解析结果将被视为Racket表达式。但是,如果它以引号形式使用,那么它只生成一个数字或一个列表:
> #reader"arith.rkt" 1*2+3 5
> '#reader"arith.rkt" 1*2+3 '(+ (* 1 2) 3)
"arith.rkt"读取器也可以用于毫无意义的位置。由于read-syntax实现跟踪源位置,语法错误至少可以根据其原始位置(在错误消息的开头)引用部分输入:
> (let #reader"arith.rkt" 1*2+3 8) repl:1:27: let: bad syntax (not an identifier and expression
for a binding)
at: +
in: (let (+ (* 1 2) 3) 8)
17.2.2 可读表
读取器扩展以任意方式解析输入字符的能力可能是很强大,但许多词汇扩展需要一种不太通用但更易于组合的方法。与扩展器级别的Racket语法可以通过宏扩展的方式大致相同,读取器级别的Racket语法可以通过可读表(readtable)进行组合扩展。
例如,默认的Racket读取器是递归下降的解析器,可读表将字符映射到解析处理程序。例如,默认可读表将映射到一个处理程序,该处理程序递归解析子表,直到找到一个)。current-readtable的参数确定了read或read-syntax使用的可读表。而不是直接解析原始字符,读取器扩展可以安装一个扩展的可读表,然后链接到read或read-syntax。
有关《parameters》的介绍,请参见《动态绑定:parameterize》。
make-readtable函数构造了一个新的可读表,作为现有可读表的扩展。它接受字符、字符映射类型和(特定类型的映射)解析程序方面的一系列规范。例如,要扩展可读表,以便$可以用于开始和结束中缀表达式,请实现一个read-dollar函数并使用:
(make-readtable (current-readtable) #\$ 'terminating-macro read-dollar)
read-dollar的协议要求函数接受不同数量的参数,这取决于它是否在read模式下使用还是在read-syntax模式下使用。在read模式下,解析器函数有两个参数:触发解析器的字符和正在读取的输入端口。在read-syntax模式下,函数必须接受四个额外的参数,以提供字符的源位置。
下面的"dollar.rkt"模块根据被"arith.rkt"提供的read和read-syntax函数定义了一个read-dollar函数,并将read-dollar与新的read和read-syntax函数放在一起,这些函数安装了可读表e并将其链接到Racket的read或read-syntax:
"dollar.rkt"
#lang racket (require syntax/readerr (prefix-in arith: "arith.rkt")) (provide (rename-out [$-read read] [$-read-syntax read-syntax])) (define ($-read in) (parameterize ([current-readtable (make-$-readtable)]) (read in))) (define ($-read-syntax src in) (parameterize ([current-readtable (make-$-readtable)]) (read-syntax src in))) (define (make-$-readtable) (make-readtable (current-readtable) #\$ 'terminating-macro read-dollar)) (define read-dollar (case-lambda [(ch in) (check-$-after (arith:read in) in (object-name in))] [(ch in src line col pos) (check-$-after (arith:read-syntax src in) in src)])) (define (check-$-after val in src) (regexp-match #px"^\\s*" in) ; skip whitespace (let ([ch (peek-char in)]) (unless (equal? ch #\$) (bad-ending ch src in)) (read-char in)) val) (define (bad-ending ch src in) (let-values ([(line col pos) (port-next-location in)]) ((if (eof-object? ch) raise-read-error raise-read-eof-error) "expected a closing `$'" src line col pos (if (eof-object? ch) 0 1))))
使用此读取器扩展,可以在表达式的开头使用单个#reader,以启用切换到中缀运算的$的多种用法:
> #reader"dollar.rkt" (let ([a $1*2+3$] [b $5/6$]) $a+b$) 35/6
17.3 定义新的#lang语言
将模块作为启动的源程序加载时
#lang language
language决定了在读取器级别解析模块其余部分的方式。读取器级别解析必须生成一个module表作为语法对象。与以往一样,module后面的第二个子表指定了控制模块主体表含义的模块语言(module language)。因此,在language之后指定的#lang控制模块的读取器级别和扩展器级别解析。
17.3.1 指定#lang语言 |
17.3.2 使用#lang reader |
17.3.3 使用#lang s-exp syntax/module-reader |
17.3.4 安装语言 |
17.3.5 源处理配置 |
17.3.6 模块处理配置 |
17.3.1 指定#lang语言
language的语法有意与require或模块语言(module language)中使用的模块路径的语法重叠,因此像racket、racket/base、slideshow或scribble/manual这样的名称既可以用作#lang语言,也可用作模块路径。
同时,language的语法比模块路径更受限制,因为只有a-z、A-Z、0-9、/(不在开头或结尾)、language名称中允许使用_、-和+。这些限制使#lang语法尽可能简单。反过来,保持#lang语法简单也很重要,因为语法本身就不灵活且不可扩展;#lang协议允许language以一种几乎不受约束的方式细化和定义语法,但#lang协议本身必须保持固定,以便各种不同的工具能够“引导”到扩展世界。
幸运的是,#lang协议提供了一种自然的方式来引用语言,而不是严格的language语法:通过定义一个实现自己的嵌套协议的language。我们已经看到了一个例子(在《使用#lang s-exp》里):s-exp的language允许程序员使用通用的模块路径语法指定模块语言。同时,s-exp负责#lang语言的读取器级别职责。
不同于racket,s-exp不能作用带有require的模块路径。尽管#lang的language语法与模块路径语法重叠,但language不能直接用作模块路径。相反,language通过尝试两个位置获得模块路径:首先,它为language查找主模块的读取器子模块。如果这不是有效的模块路径,那么language将用/lang/reader作为后缀。(如果两者都不是有效的模块路径,则会引发错误。)生成的模块使用与#reader类似的协议提供read和read-syntax函数。
《读取器扩展》介绍了#reader.
将#lang的language转换为模块路径的一个结果是,该语言必须安装在集合(collection)中,类似于"racket"或"slideshow"是随Racket分发的集合。然而,还有一个例外:reader语言允许你使用通用模块路径指定语言的读取器级实现。
17.3.2 使用#lang reader
#lang的reader语言类似于s-exp,因为它是一种元语言。s-exp允许程序员在扩展器解析层指定模块语言,而reader允许程序员指定读取器层的语言。
#lang reader后面必须有一个模块路径,并且指定的模块必须提供两个函数:read和read-syntax。该协议与#reader实现的协议相同,但对于#lang,read和read-syntax函数必须生成一个基于模块输入文件其余部分的module表。
下面的"literal.rkt"模块实现了一种语言,该语言将其整个正文视为文本,并将文本导出为数据字符串:
"literal.rkt"
#lang racket (require syntax/strip-context) (provide (rename-out [literal-read read] [literal-read-syntax read-syntax])) (define (literal-read in) (syntax->datum (literal-read-syntax #f in))) (define (literal-read-syntax src in) (with-syntax ([str (port->string in)]) (strip-context #'(module anything racket (provide data) (define data 'str)))))
"literal.rkt"语言在生成的module表达式上使用strip-context,因为read-syntax函数应该返回一个没有词法上下文的语法对象。此外,"literal.rkt"语言创建了一个名为anything的模块,这是一个随意的选择;该语言旨在在文件中使用,当普通书写的模块名称出现在require文件中时,它将被忽略。
"literal.rkt"语言可以在模块中使用"tuvalu.rkt":
"tuvalu.rkt"
#lang reader "literal.rkt" Technology! System! Perfect!
导入"tuvalu.rkt"将data绑定到模块内容的字符串版本:
> (require "tuvalu.rkt") > data "\nTechnology!\nSystem!\nPerfect!\n"
17.3.3 使用#lang s-exp syntax/module-reader
解析模块主体通常不像"literal.rkt"中那样简单。更典型的模块解析器必须迭代以解析模块体的多个表单。一种语言也更有可能扩展Racket语法——也许通过readtable——而不是完全替换Racket句法。
syntax/module-reader模块语言对语言实现的公共部分进行抽象,以简化新语言的创建。在最基本的形式中,用syntax/module-reader实现的语言只指定了该语言使用的模块语言,在这种情况下,该语言的读取器层与Racket相同。例如,使用
"raquet-mlang.rkt"
#lang racket (provide (except-out (all-from-out racket) lambda) (rename-out [lambda function]))
以及
"raquet.rkt"
#lang s-exp syntax/module-reader "raquet-mlang.rkt"
那么
#lang reader "raquet.rkt" (define identity (function (x) x)) (provide identity)
由于"raquet-mlang.rkt"将lambda导出为function,因此实现并导出identity函数。
syntax/module-reader语言接受许多可选的规范来调整语言的其它特性。例如,替换的read和read-syntax解析语言可以使用#:read和#:read-syntax。以下内容"dollar-racket.rkt"语言使用"dollar.rkt"(见《可读表》)来构建类似racket,但带有$转义为简单中缀算术:
"dollar-racket.rkt"
#lang s-exp syntax/module-reader racket #:read $-read #:read-syntax $-read-syntax (require (prefix-in $- "dollar.rkt"))
require表出现在模块的末尾,因为syntax/module-reader的所有关键字标记的可选规范必须出现在任何助手导入或定义之前。
下面的模块使用"dollar-racket.rkt"来使用$转义实现cost函数:
"store.rkt"
#lang reader "dollar-racket.rkt" (provide cost) ; Cost of ‘n' $1 rackets with 7% sales ; tax and shipping-and-handling fee ‘h': (define (cost n h) $n*107/100+h$)
17.3.4 安装语言
到目前为止,我们已经使用reader元语言来访问"literal.rkt"和"dollar-racket.rkt"。如果你想直接使用#lang literal之类的,那你必须把"literal.rkt"移到名为"literal"的Racket集合中(另请参见《添加集合》)。具体而言,将"literal.rkt"移到任何目录名"literal/main.rkt"的reader子模块中,如下所示:
"literal/main.rkt"
#lang racket (module reader racket (require syntax/strip-context) (provide (rename-out [literal-read read] [literal-read-syntax read-syntax])) (define (literal-read in) (syntax->datum (literal-read-syntax #f in))) (define (literal-read-syntax src in) (with-syntax ([str (port->string in)]) (strip-context #'(module anything racket (provide data) (define data 'str))))))
然后,将"literal"目录作为包安装:
cd /path/to/literal ; raco pkg install
移动文件并安装包后,可以在#lang后面直接使用literal。
#lang literal Technology! System! Perfect!
有关使用宏的详细信息参见《(part ("(lib scribblings/raco/raco.scrbl)" "top"))》。
你还可以使用Racket包管理器(参见《(part ("(lib pkg/scribblings/pkg.scrbl)" "top"))》)将您的语言提供给其他人安装。创造"literal"包并将其注册到Racket包目录(请参见《(part ("(lib pkg/scribblings/pkg.scrbl)" "concept:catalog"))》)后,其他人就可以使用raco pkg安装它:
raco pkg install literal
安装后,其他人可以用同样的方式调用该语言:在源文件的顶部使用#lang literal。
如果你使用公共源代码库(例如GitHub),你可以将你的包链接到源代码。当你改进这个包,其他人可以使用raco pkg更新其的版本:
raco pkg update literal
了解有关Racket包管理器的更多信息,请参见(part ("(lib pkg/scribblings/pkg.scrbl)" "top"))。
17.3.5 源处理配置
Racket发行版包括用于编写平常文档的Scribble语言,其中Scribble扩展了普通Racket以更好地支持文本。下面是一个示例Scribble文档:
#lang scribble/base |
@(define (get-name) "Self-Describing Document") |
@title[(get-name)] |
The title of this document is ``@(get-name).'' |
如果你将该程序放在DrRacket的定义区域中并单击Run,那么是乎不会发生什么。scribble/base语言只绑定并导出doc作为文档的描述,类似于"literal.rkt"将字符串导出为data的方式。
然而,只需在DrRacket中打开一个带有scribble/base语言的模块,就会出现一个Scribble HTML按钮。此外,DrRacket知道如何通过将文档中与文字相对应的部分着色为绿色来为Scribble语法着色。语言名称scribble/base不是硬连接的DrRacket。相反,scribble/base语言的实现提供了按钮和语法着色信息,以响应DrRacket的查询。
如果你已经安装了安装语言中描述的literal语言,那你可以调整"literal/main.rkt",以便DrRacket将literal语言中的模块内容视为纯文本,而不是(错误地)视为Racket语法:
"literal/main.rkt"
#lang racket (module reader racket (require syntax/strip-context) (provide (rename-out [literal-read read] [literal-read-syntax read-syntax]) get-info) (define (literal-read in) (syntax->datum (literal-read-syntax #f in))) (define (literal-read-syntax src in) (with-syntax ([str (port->string in)]) (strip-context #'(module anything racket (provide data) (define data 'str))))) (define (get-info in mod line col pos) (lambda (key default) (case key [(color-lexer) (dynamic-require 'syntax-color/default-lexer 'default-lexer)] [else default]))))
这个修改后的literal实现提供了一个get-info函数。get-info函数由read-language(DrRacket调用)源输入流和位置信息,以防查询结果应取决于语言名称后面的模块内容(literal不是这样)。get-info的结果是两个参数的函数。第一个参数总是一个符号,表示工具从语言中请求的信息类型;如果语言无法识别查询或没有查询信息,则第二个参数是返回的默认结果。
DrRacket获得一种语言的get-info结果后,它使用'color-lexer查询调用该函数;结果应该是一个在输入流上实现语法着色解析的函数。对于literal,syntax-color/default-lexer模块提供了一个适用于纯文本的default-lexer语法着色解析器,因此literal加载并返回该解析器以响应'color-lexer查询。
编程工具用于查询的符号集完全在工具和选择与之合作的语言之间。例如,除了'color-lexer之外,DrRacket还使用'drracket:toolbar-buttons查询来确定工具栏中哪些按钮可以使用该语言在模块上操作。
syntax/module-reader语言允许你通过#:info可选规范指定get-info处理。#:info函数的协议与原始get-info协议略有不同;修订后的协议允许syntax/module-reader自动处理未来的语言信息查询。
17.3.6 模块处理配置
假设文件"death-list-5.rkt"包含
"death-list-5.rkt"
#lang racket (list "O-Ren Ishii" "Vernita Green" "Budd" "Elle Driver" "Bill")
如果你直接require "death-list-5.rkt",那么它会以通常的Racket结果格式打印列表:
> (require "death-list-5.rkt") '("O-Ren Ishii" "Vernita Green" "Budd" "Elle Driver" "Bill")
但是,如果"death-list-5.rkt"是由"kiddo.rkt"所要求的,该"kiddo.rkt"是用scheme而不是用racket实现的:
"kiddo.rkt"
#lang scheme (require "death-list-5.rkt")
然后,如果你在DrRacket中运行"kiddo.rkt"文件,或者直接使用racket运行该文件,"kiddo.rkt"导致"death-list-5.rkt"以传统的Scheme格式打印其列表,而不是使用前导引号:
("O-Ren Ishii" "Vernita Green" "Budd" "Elle Driver" "Bill")
"kiddo.rkt"示例说明了打印结果值的格式如何依赖于程序的主模块而不是用于实现它的语言。
更广泛地说,只有当用一种语言编写的模块直接用racket运行(而不是导入导另一个模块中),才会调用该语言的某些特性。一个例子是结果打印样式(如上所示)。另一个例子是REPL行为。这些特性是语言的运行时配置(run-time configuration)的一部分。
与语言的语法着色属性(如《源处理配置》中所描述的),运行时配置本身是模块的属性,而不是表示模块的源文本属性。为此,即使模块被编译成字节码形式并且源代码不可用,模块的运行时配置也需要可用。因此,运行时配置不能由我们从语言的解析器模块导出的get-info函数处理。
相反,它将由一个新的configure-runtime(配置运行时)子模块处理,我们将在解析后的module表中添加该子模块。当一个模块直接用racket运行,racket会查找一个configure-runtime子模块。如果它存在,racket就运行它。但是,如果将模块导入到另一个模块中,则会忽略'configure-runtime子模块。(如果'configure-runtime子模块不存在,racket会照常对模块求值。)这意味着'configure-runtime子模块可用于直接运行模块时需要执行的任何特殊设置任务。
回到literal语言(请参见《源处理配置》),我们可以调整语言,以便直接运行literal模块可以使其打印出字符串,而在更大的程序中使用literal模块只需提供data而不打印。为了完成这项工作,我们需要一个额外的模块。(为了清楚起见,我们将把这个模块实现为一个单独的文件现有文件的子模块。)
.... (主要安装或用户空间) |- "literal" |- "main.rkt" (带读卡器子模块) |- "show.rkt" (新增)
"literal/show.rkt"模块将提供一个show函数以被应用到一个literal(文本)模块的字符串内容,同时也提供一个show-enabled参数,它控制是否show的实际打印结果。
"literal/main.rkt"中新的configure-runtime子模块将show-enabled参数设置为#t。最终的效果是,show将打印给定的字符串,但只有当使用literal语言的模块直接运行时(因为只有这样才能调用configure-runtime子模块)。
这些更改在以下修订的"literal/main.rkt"中实现:
"literal/main.rkt"
#lang racket (module reader racket (require syntax/strip-context) (provide (rename-out [literal-read read] [literal-read-syntax read-syntax]) get-info) (define (literal-read in) (syntax->datum (literal-read-syntax #f in))) (define (literal-read-syntax src in) (with-syntax ([str (port->string in)]) (strip-context #'(module anything racket (module configure-runtime racket (require literal/show) (show-enabled #t)) (require literal/show) (provide data) (define data 'str) (show data))))) (define (get-info in mod line col pos) (lambda (key default) (case key [(color-lexer) (dynamic-require 'syntax-color/default-lexer 'default-lexer)] [else default]))))
然后"literal/show.rkt"模块必须提供show-enabled参数和show函数:
"literal/show.rkt"
#lang racket (provide show show-enabled) (define show-enabled (make-parameter #f)) (define (show v) (when (show-enabled) (display v)))
有了literal的所有片段后,尝试直接运行"tuvalu.rkt"的以下变体,并通过另一个模块中的require:
"tuvalu.rkt"
#lang literal Technology! System! Perfect!
当直接运行时,我们会看到这样打印的结果,因为我们的configure-runtime子模块将show-enabled参数设置为#t:
Technology!
System!
Perfect!
但是,当导入到另一个模块中时,打印将被抑制,因为不会调用configure-runtime子模块,因此show-enabled参数将保持其默认值#f。
Racket编程指南——17 创造语言相关推荐
- Racket编程指南——23 Racket和Scheme的方言
23 Racket和Scheme的方言 我们使用"Racket"来指Lisp语言的一种特定方言,它基于Lisp家族的Scheme分支.尽管Racket与Scheme相似,但模块上的 ...
- Racket编程指南——4 表达式和定义
4 表达式和定义 <Racket概要>这一章介绍了一些基本的Racket的句法表:定义.过程程序.条件表达式等等.本节提供这些形式的更详细信息,以及一些附加的基本表. 4.1 标记法 4. ...
- Racket编程指南——21 运行和创建可执行文件
21 运行和创建可执行文件 在开发程序时,很多Racket程序员使用DrRacket编程环境.要在没有开发环境的情况下运行程序,请使用racket(用于基于控制台的程序)或gracket(对于GUI程 ...
- Racket编程指南——20 并行
20 并行 Racket提供两种形式的并行(parallelism):前程(futures)和现场(places).在提供多个处理器的平台上,并行可以提高程序的运行时性能. 关于Racket里顺序性能 ...
- Racket编程指南——13 类和对象
13 类和对象 本章基于一篇论文[Flatt06]. 一个类(class)表达式表示一类值,就像一个lambda表达式一样: (class superclass-expr decl-or-expr . ...
- Racket编程指南——8 输入和输出
8 输入和输出 一个Racket端口对应一个流的Unix概念(不要与racket/stream的流混淆). 一个Racket端口(port)代表一个数据源或数据池,诸如一个文件.一个终端.一个TCP连 ...
- 高质量C++/C编程指南 1
高质量C++/C编程指南 文件状态 [ ] 草稿文件 [√] 正式文件 [ ] 更改正式文件 文件标识: 当前版本: 1.0 作 者: 林锐 博士 完成日期: 2001年7月24日 http://ma ...
- 超级宝典编程指南(红蓝宝书)-读书笔记
学习中and持续更新中-- 超级宝典= 客户端:指我们需要执行,存储在CPU中的指令(OpenGLAPI,C和C++代码),客户端会把渲染命令发送给服务器执行. 服务器:接收客户端的指令后调用GPU芯 ...
- C语言SOCKET编程指南
转载自:http://blog.sina.com.cn/s/blog_79b01f66010163q3.html 这篇文章完全可以作为c语言socket编程指南,无论在任何系统下.感谢作者fenglo ...
- c语言odbc编程,c语言之odbc编程指南c语言之odbc编程指南.doc
c语言之odbc编程指南c语言之odbc编程指南 ?摘要本文在介绍了ODBC(开放性数据库连接,Open? DataBase? Connectivity)运行机制的基础上,着重讨论了VisualC++ ...
最新文章
- .NetCore Docker
- Java 处理 Exception 的 9 个最佳实践!
- linux I/O--I/O多路复用--介绍(二)
- 饮冰三年-人工智能-Python-16Python基础之迭代器、生成器、装饰器
- CPU异常分析(以trap00为例)
- SuperTuxKart 0.10 测试版发布
- Web3D编程入门总结——WebGL与Three.js基础介绍
- ArcGis-学习笔记6-4 空间插值简介
- java wmic_wmic
- 动环监控串口,动环监控系统接口
- mysql limit sql注入_LIMIT子句中的盲 SQL注入漏洞利用
- fpga的EPCS 配置的2种方法(图文讲解,哈哈,网上互相抄袭的一些文字说明太不明了了)
- zz--WINCE TCPMP应用四:利用TCPMP插件开发程序
- Lua脚本编写Wireshark插件解析第三方私有协议
- 锤子T1手机 只显示logo无法开机
- spring5源码阅读(五)Post Processors类型及作用详解
- 前端开发:JS中小数点保留两位小数的方法汇总
- PHP折扣,php 团购折扣计算公式_PHP教程
- python用opencv计算汽车间距_计算机视觉:利用OpenCV和Python进行车辆计数详细步调...
- 看完阮一峰老师的博客后,感觉 OAuth2.0认证就这?