Racket编程指南——17 创造语言

17 创造语言

前一章中定义的宏(macro)工具让一个程序员定义语言的语法扩展,但一个宏在以下两个方面是有限的:

  • 一个宏不能限制上下文中可用的语法或改变包围的表的意义;

  • 一个宏仅在语言词汇约定的参数范围内能够扩展语言中的语法,如用括号对带子表的宏名分组和用标识符、关键字和原意的核心语法。

读取器层和扩展器层之间的区别在《Lists and Racket Syntax》中介绍。

也就是说,一个宏只能扩展一种语言,它只能在 扩展器(expander)层起作用。Racket为定义扩展器(expander)层的起始点提供额外的工具,以便扩展读取器(reader)层,以便定义读取器(reader)层的起始点,也以便封装一个读取器(reader)扩展器(expander) 起始点为一个方便命名的语言。

    17.1 模块(module)语言

      17.1.1 隐式绑定

      17.1.2 使用#lang s-exp

    17.2 读取器扩展

      17.2.1 源位置

      17.2.2 readtable(读取表格)

    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)语言

当使用手写module表来书写模块,模块路径在新模块名提供模块最初导入后指定。由于初始导入模块决定了模块主体中可用的最基本绑定,就像require,最初的导入可以被称为一个模块语言(module language)

最常见的模块语言(module languages)racketracket/base,但你可以通过定义一个合适的模块定义你自己的模块语言(module languages)。例如,使用像all-from-outexcept-outrename-out那样的provide子表,你可以添加、删除或重命名绑定 从racket创作一个模块语言(module languages),这个语言是racket}的一个变体:

《The module Form介绍了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 隐式绑定

如果你试图从定义你自己的模块语言(module language)中的racket里删除太多 ,那么生成的模块将不会作为一个模块语言(module language)正确工作:

> (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 language)使用:

> (module just-lambda racket
    (provide lambda #%module-begin))
> (module identity 'just-lambda
    (lambda (x) x))
> (require 'identity)

#

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 language)可能限制函数为一个单一的参数,限制函数调用提供一个单一的参数,限制模块主体到一个单一的表达式,不允许字面语法,以及处理非绑定标识符作为无解释的符号:

> (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和反引用(Quasiquoting)样板文件,就像在"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"模块语言(module language),一个简单的网页可以不需要明确的定义或导出页(page)而被描述,并且以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: " "2018-03-05"))

17.1.2 使用#lang s-exp

#lang级别上实现一个语言比声明一个单一的模块要复杂得多,因为#lang允许程序员控制语言的几个不同方面。然而,s-exp语言,为了使用带#lang简写的模块语言(module language)扮演了一种元语言:

#lang s-exp module-name
form ...

和以下内容是一样的

(module name module-name
  form ...)

这里name来自包含#lang程序的源文件。这个名称s-exp是S-expression的简写形式,这是一个读取器(reader)层的传统的名称的 词法约定:括号、标识符、数字、带反斜杠转义的双引号字符串等等。

使用#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 (part ("(lib scribblings/reference/reference.scrbl)" "top")) provides more on 读取器扩展.

Racket语言读取器(reader)层可以通过#reader表扩展。一个读取器扩展作为一个模块实现,该模块在#reader之后命名。模块导出的函数将原始字符解析成一个被扩展器(expander)层接受的表。

#reader的语法是

#reader module-path reader-specific

module-path命名一个模块,它提供readread-syntax函数。reader-specific部分是一个字符序列,它被解析为确定——被来自module-pathreadread-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"readread-syntax函数既从输入流中读取五个字符又把它们放入一个字符串进而成为一个列表。来自"five.rkt"的读取器函数没有义务遵循Racket词法约定并作为一个单一的数处理连续序列234567。由于只有23456部分被readread-syntax所接受,7仍然需要用通常的Racket方式进行分析。同样,来自"five.rkt"的读取器函数没有义务忽略空白,以及

#lang racket/base
 
'(1 #reader"five.rkt" 234567 8)

相当于

#lang racket/base
 
'(1 (" 2345") 67 8)

由于"five.rkt"后的第一个字符立即是一个空格。

一个#reader表也能够在REPL中使用:

> '#reader"five.rkt"abcde

'("abcde")

17.2.1 源位置

readread-syntax的区别在于read是用于数据,而 read-syntax是用来解析程序的。更确切地说,当通过Racket的read解析封闭流时,将使用read函数,当封闭流由Racket的read-syntax函数解析时,使用read-syntax。没有什么需要readread-syntax用同样的方法解析输入,但是使它们不同会混淆程序员和工具。

read-syntax函数可以返回与read相同的值,但它通常应该返回一个语法对象(syntax object),它将解析表达式与源位置连接起来。与"five.rkt"的例子不同的是,read-syntax函数通常是直接实现生成语法对象(syntax object),然后read可以使用read-syntax和揭开语法对象(syntax object)封装器以产生一个原生的结果。

下面的"arith.rkt"模块实现了一个读取器以解析简单的中缀算术表达式为Racket表。例如,1*2+3解析成Racket表(+ (* 1 2) 3)。支持的运算符是+-*/,而操作数可以是无符号整数或单字母变量。该实现使用port-next-location获取当前源位置,并使用 datum->syntax将原始值转换为语法对象(syntax object)

"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
     ; operatoroperand 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 "bad arithmetic syntax"
                      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 readtable(读取表格)

一个读取器扩展对以任意方式解析输入字符的能力可以是强大的,但很多情况下,词汇扩展需要一个不那么一般但更可组合的方法。同样,Racket语法的扩展器(expander)等级可以通过宏(macros)来扩展,Racket语法的reader等级可以通过一个readtable来复合扩展。

Racket读取器是一个递归下降解析器,以及readtable映射特征到解析处理器。例如,默认readtable映射(到一个处理器,它递归解析子表直到找到一个)current-readtable参数(parameter)决定readtablereadread-syntax使用。而不是直接解析原始特征,一个读取器扩展可以安装一个扩展的readtable然后链接readread-syntax

参见《Dynamic Binding: parameterize》——对《parameters》的介绍。

make-readtable函数构建一个新的readtable作为一个现有的一个扩展。它根据一个字符、一个字符映射类型和(对于的某些映射的类型)一个解析程序接受一系列规范。例如,扩展readtable使$可以用来开始和结束中缀表达式,实现read-dollar函数并使用:

(make-readtable (current-readtable)
                #\$ 'terminating-macro read-dollar)

read-dollar协议要求函数接受不同参数的数值,这取决于它是否被用于readread-syntax模式。在read模式中,解析器函数给出两个参数:触发解析器函数的字符和正在读取的输入端口。在read-syntax模式中,函数必须接受提供字符源位置的四个附加参数。

下面的"dollar.rkt"模块根据被"arith.rkt"提供的readread-syntax函数定义了一个read-dollar函数,并将read-dollar连同新的readread-syntax函数放在一起,它安装readtable并链接Racket的readread-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决定了模块的其余部分在读取器(reader)级别上被解析的方式。读取器(reader)级别解析必须生成一个module(模块)表作为一个语法对象(syntax object)。与以往一样,module(模块)后面的第二个子表指定了模块语言(module language)来控制模块主体表的含义。因此,一种language(语言)被指定在#lang控制读取器(reader)级和扩展器(expander)级的一个模块的解析之后。

    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),这样的名字像racketracket/baseslideshowscribble/manual可以用于作为#lang语言和模块路径。

同时,language的语法的限制比模块路径远远多,因为只a-zA-Z0-9/(不在开始或结束)、_-+在一个language(语言)名称中被允许。这些限制使#lang语法尽量简单。保持#lang简单,相应地,语法又很重要,因为语法是固有不变和非可扩展的;#lang协议允许语言精炼,在几乎无约束的方式定义的语法,但#lang协议本身必须保持固定,使各种不同的工具可以“引导(boot)”到扩展的世界。

幸运的是,这#lang协议提供了一个参考其它比刚性语言语法方面语言的自然方式:通过定义一种language,实现自己的嵌套协议。我们已经看到一个例子(在使用#lang s-exp里):该s-explanguage允许程序员使用通用模块路径(module path)语法指定一个模块语言(module language)。同时,s-exp照顾了一个#lang语言读取器(reader)级的职责。

不同于rackets-exp不能作为一个带require的模块路径。虽然#lang为重叠模块路径语法的language的语法,一个language是不能直接作为一个模块的路径。相反,一个language在两个地点获得模块路径:第一,它寻找一个language读取器(reader)的主模块的子模块。如果这不是一个有效的模块路径,那么language/lang/reader添加后缀。(如果不是一个有效的模块路径,引发错误。)作为结果的模块提供readread-syntax函数使用一个协议,它类似于#reader的一个。

《读取器扩展》介绍了#reader.

一个#langlanguage变成了一个模块路径的方式的结果是,语言必须被安装在一个集合(collection)里,类似于"racket""slideshow"的方式是用Racket分发的集合。然而,还有一种方法可以避免这种限制:reader(读取器)语言允许使用通用模块路径(module path)指定语言的读取器(reader)级实现。

17.3.2 使用#lang reader

对于#langreader语言类似于s-exp,在它里边扮演一种元语言。而s-exp让一个程序员在解析的扩展器(expander)层指定一个模块语言(module language)reader可以让一个程序员在读取器(reader)层指定一个语言。

一个#lang reader必须跟着一个模块路径,并指定模块必须提供两个函数:readread-syntax。该协议是一个和#reader实现相同的,但对于#langreadread-syntax函数必须产生一个module表,它基于对模块的输入文件的其余部分。

下面的"literal.rkt"模块实现了一种语言,它把整个主体作为字面文本并且导出文本作为数据(data)字符串:

"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模块语言(module language)抽象语言实现的公共部分,以简化新语言的创建。在其最基础的表里,用syntax/module-reader实现的语言简单地指定用于该语言的模块语言(module language),在这种情况下,语言的读取器(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)

实现和导出identity函数,因为"raquet-mlang.rkt"导出lambda函数。

syntax/module-reader语言接受许多可选规范来调整语言的其它特性。例如,另一个 解析语言的readread-syntax可以用#:read#:read-syntax分别指定。下面的"dollar-racket.rkt"语言运用"dollar.rkt"(见readtable(读取表格))建立一个像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集合(collection)(参见Adding Collections)。具体而言,将"literal.rkt"移入"literal/main.rkt"的一个读取器(reader)子模块给任何目录名"literal",像这样:

"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"))以在使用宏(raco)时得到更多信息。

你也可以让你的语言通过使用Racket安装包管理器提供给他人去安装(参见(part ("(lib pkg/scribblings/pkg.scrbl)" "top")))。你创造了一个"literal"包并用Racket包目录登记后(见(part ("(lib pkg/scribblings/pkg.scrbl)" "concept:catalog"))),其他人就可以使用raco pkg安装它:

  raco pkg install literal

一旦安装,其他人可以用同样的方式调用该语言:通过在一个源文件的顶部使用#langliteral

如果你使用一个公共的源库(例如,GitHub),你可以将你的包链接到这个源。当你升级这个包,其他人可以使用raco pkg更新他们的版本:

  raco pkg update literal

参见(part ("(lib pkg/scribblings/pkg.scrbl)" "top"))以了解有关Racket包管理器的更多信息。

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的定义区域(definitions area)并点击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请求调用函数;结果应该是一个在输入流上实现语法着色解析的函数。对于literalsyntax-color/default-lexer模块提供了一个适用于纯文本的default-lexer语法着色解析器,所以literal加载和返回在对一个'color-lexer请求的响应里的解析器。

编程工具用于查询的符号集完全位于选择与之合作的工具和语言之间。例如,除了'color-lexer之外,DrRacket采用一种'drracket:toolbar-buttons查询以确定哪个按钮在工具栏上应该可用于在使用这个语言的模块上操作。

syntax/module-reader语言可以让你指定get-info通过一个#: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"所需求,而不是用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)的一部分。

与一个语言的语法着色属性不同(如源处理配置中所描述的),运行时配置本身是一个模块(module)的一个属性,而不是表示模块的源文本(source text)的属性。出于这个原因,即使模块编译成字节码形式且源不可用,模块的运行时配置也需要可用。因此,运行时配置不能通过我们从语言解析器模块导出的get-info函数来处理。

相反,它将由一个新的configure-runtime(配置运行时)子模块处理,我们会在里面添加解析module(模块)表。当一个模块直接用racket运行,racket查找configure-runtime(配置运行时)子模块。如果它存在,racket就运行它。但如果这个模块被导入到另一个模块,这个 'configure-runtime(配置运行时)子模块被忽略。(如果'configure-runtime(配置运行时)子模块不存在,racket只是像通常一样对模块求值。)那意味着'configure-runtime(配置运行时)子模块可用于任何特殊设置任务,它在模块直接运行时需要出现。

回到literal(字面)语言(参见源处理配置),我们可以调整语言,这样直接运行一个literal(文字)模块造成它打印出它的字符串,而在一个较大的程序中使用一个literal(文字)模块只提供数据(data)而不打印。为了使这项工作,我们将需要一个额外的模块。(为了清楚起见,我们将把这个模块作为一个单独的文件来实现。但它同样可以是一个已存在的文件的一个模块。)

.... (the main installation or the user’s space)
|- "literal"
   |- "main.rkt"            (with reader submodule)
   |- "show.rkt"            (new)
  • "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


你可能感兴趣的:(Lisp,Racket编程指南(中文译),Racket)