racket reader介绍
DSL(Domain-specific language)是解决软件复杂性一个非常重要的手段。但是不是说所有语言都可以方便简单的定义DSL。
lisp/scheme作为meta programming language天生擅长定义DSL。
分层的软件设计是大家常用的设计方法,下层为上层提供接口功能。
但在lisp/scheme中,每层都可以认为是一个新的language,每层在下一层的基础上定义出新的language。我们可以方便的定义DSL。
racket提供了定义和扩展reader layer的方法。
reader layer通过parse源代码,生成#'(module module-name lanuage …)的语法数据。
expand layer: scheme的macro作用的阶段,它的signature为输入syntax object,输出一个syntax object。简单来理解可以认为是一个source-to-source的过程。 scheme的macro的强大威力主要体现在:scheme的宏提供了对语言的语法扩展(syntactic extensions)能力,但是宏有如下限制:
宏只能在expander layer对语言进行扩展。
结合reader layer和expand layer的扩展能力,我们可以方便的定义出任何新的语言。
;; module module-name-blabla> (module module-name-blabla racket (provide (except-out (all-from-out racket) lambda) (rename-out [lambda function])));; module test> (module test 'module-name-blabla (map (function (points) (case points [(0) "love"] [(1) "fifteen"] [(2) "thirty"] [(3) "forty"])) (list 0 2)))> (require 'score)'("love" "thirty")
module 语法:module-name-blabla表达定义的module的名字,之后的 racket 表示一个module-path,他提供了这个module最初的imports,我们叫他 initial-import module. 他决定了这个module(module-name-blabla)最初有哪些名字绑定,譬如require, except等。这就好像module-path定义了我们 这个module所使用的语言一样。
Implicit Form Binding: 我们可以以racket这个module为基础,定义自己的module language:> (module just-lambda racket (provide lambda))> (module identity 'just-lambda (lambda (x) y))eval:2:0: module: no #%module-begin binding in the module'slanguage in: (module identity (quote just-lambda) (lambda(x) x))
#%module-begin是一个隐式规则,用来包裹住module的body.譬如上面的例子,展开的样子应该是:
#'(#%module-begin (lambda (x) y))
类似 #%module-begin这样的隐式规则的还有 #%top:表示free-variable。 #%app:表示函数调用。
> (module just-lambda racket (provide lambda #%module-begin #%app #%datum))> (module bla 'just-lambda ((lambda (x) y) 10))
这种隐式规则有什么用呢?我们可以重新定义这些隐式规则,这样我们可以修改他们隐式的作用。
譬如我们想定义一种只允许一个参数的lambda定义(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)> (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)Using #lang s-exp: 直接使用 #lang language 比较麻烦,因为#lang语法可以让我们以不同的方式定义新的语言。而 s-exp的方式则仅允许我们以 module language的方式进行meta-language。
#lang s-exp module-name
form …
这实际上等价于:
(module name module-name form …)
For example:
#lang racket/base ;;five.rkt(provide read read-syntax)(define (read in) (list (read-string 5 in)))(define (read-syntax src in) (list (read-string 5 in)))
然后我们的程序可以这样使用上面的module:
'(1 #reader"five.rkt"234567 8) ;;结果为'(1 ("23456") 7 8)
read 和 read-syntax 有什么区别:
字面来理解:read用来parse data,read-syntax用来parse program。准确的说read将被ra cket的 read 函数所调用, read-syntax将被racket的 read-syntax 调用。没有强制要求read和read-syntax行为必 须一样,但最好是一样,以免造成 混淆。
思考,如果一个模块这样写:
(module bla racket #reader"yourreader.rkt" body ...);yourreader.rkt返回#‘(#'body ...) 这样就可以定义这个模块新的语法。Readtables:
如果从头开始写一个parser,相信不会有人愿意。但是如果我们可以基于原来的reader,定制 一些我们希望覆盖的规则呢?譬如在原来的symbol-expresstion的基础上增加一个对$…$的解析呢?
(+
这时我只希望写如何parse $1*3+2$这一小段字符串,其他的还是交给racket去parse。readtable可以 做到这一点!
Racket reader是一个递归下降的parser,readtable将字符映射到parsing handler。比如说: 默认的readtable将 ( 映射到一个handler,这个handler 会一直parse知道找到对应的 ).
make-readtable 函数构造出新的readtable作为对已经存在的readtable的扩展。
(make-readtable (current-readtable) #\$ 'terminating-macro read-dollar)
parameter current-readtable 决定了哪个readtable被使用。 make-readtable 第一个参数表示新构造出来的readtable在哪个readtable基础上构造出来的, 之后是一系列字符到handler的参数,上面例子只有一对:#\s read-dollar。
#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"dollar.rkt" (let ([a $1*2+3$] [b $5/6$]) $a+b$)35/6
#lang language ;; equal to#reader language/lang/reader
那么language/lang/reader.rkt需要provide read和read-syntax。
read-syntax需要返回一个(module name module-path…) form的syntax,这个syntax在expander 的时候又会使用module-path作为module-language。
这时最复杂的定义language的方式,因为我们同时需要关心reader和module language。
#lang reader "reader.rkt"
等价于:
#reader "reader.rkt"
使用这种语法,我们可以不用使用#reader也不用使用#lang这种比较复杂的定义language的方式。
#lang exp moduleA
等价于
(module name moduleA …)
通过#lang exp我们可以方便的定义module language。
racket提供了简化我们在reader layer和expand layer层扩展工作的工具包,他们统一称作: Syntax: Meta-Programming Helpers。但是这个包个人感觉非常复杂,很难理解和使用。
Author: ZhangPing
p.zhang.9.25@gmail.com