Emacs之魂(九):读取器宏

Emacs之魂(一):开篇
Emacs之魂(二):一分钟学会人界用法
Emacs之魂(三):列表,引用和求值策略
Emacs之魂(四):标识符,符号和变量
Emacs之魂(五):变量的“指针”语义
Emacs之魂(六):宏与元编程
Emacs之魂(七):变量捕获与卫生宏
Emacs之魂(八):反引用与嵌套反引用
Emacs之魂(九):读取器宏


1. 编译器宏

Lisp源代码文本,首先经过读取器,得到了一系列语法对象,
这些语法对象,在宏展开阶段进行变换,最终由编译器/解释器继续处理。

以下我们使用defmacro定义了一个宏inc

(defmacro inc (var)
    `(setq ,var (1+ ,var)))

它可以将(inc x)展开为(setq x (1+ x))

inc宏可以看做对编译器/解释器进行“编程”,它影响了最终被编译/解释的程序。
因此,类似inc这样的宏,称为编译器宏(compiler macro)。

此外,还有一种宏,称为读取器宏(reader macro),
它在源代码的读取阶段,以自定义的方式,将文本转换为语法对象。

引用(quote)“'”,就是一个读取器宏,
它将源代码文本'(1 2)转换成(quote (1 2))

2. 用户定义的读取器宏

虽然,引用“'”是一个读取器宏,但它却不是由用户定义的,
支持用户自定义的读取器宏,是一个很强大的语言特性,
它可以让我们摆脱语法的束缚,创建自己的语言。

2.1 Common Lisp

(1)set-macro-character
在Common Lisp中,我们可以使用set-macro-character,来模拟引用“'”的定义,

(set-macro-character #\'
    #'(lambda (stream char)
        (list (quote quote) (read stream t nil t))))

当读取器遇到'a的时候,会返回(quote a)
其中read函数可以参考:read

(2)set-dispatch-macro-character
我们还可以自定义捕获字符(dispatch macro character),
例如,我们定义#?来捕获后面的文本,

(set-dispatch-macro-character #\# #\?
    #'(lambda (stream char1 char2)
        (list 'quote
            (let ((lst nil))
                (dotimes (i (+ (read stream t nil t) 1))
                    (push i lst))
                (nreverse lst)))))

读取器会将#?7转换成(0 1 2 3 4 5 6 7)

(3)get-macro-character
我们还可以自定义分隔符,例如,以下我们定义了#{ ... }分隔符,

(set-macro-character #\}
    (get-macro-character #\)))

(set-dispatch-macro-character #\# #\{
    #'(lambda (stream char1 char2)
        (let ((accum nil)
              (pair (read-delimited-list #\} stream t)))
            (do ((i (car pair) (+ i 1)))
                ((> i (cadr pair))
                (list 'quote (nreverse accum)))
              (push i accum)))))

读取器会将#{2 7}转换成(2 3 4 5 6 7)
其中,get-macro-character可以参考:GET-MACRO-CHARACTER

2.2 Racket

在Racket中,我们可以通过创建自定义的读取器,得到一门新语言,
例如,下面两个文件language.rktmain.rkt

(1)language.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)))))

(2)main.rkt模块,就可以用新语法进行编写了,

#lang reader "language.rkt"
Hello World!

然后,我们载入main.rkt,查看该模块导出的data变量,

> (require (file "~/Test/main.rkt"))
> data
"\nHello World!"

main.rkt中,
我们通过#lang reader "language.rkt",载入了一个自定义的读取器模块,
该模块必须导出readread-syntax两个函数。

这里,read-syntax只是简单的获取源代码,导出到data变量中,
最终返回了一个用于模块定义的语法对象(module ...)

在本例中,它把"Hello World!"转换成了一个模块定义表达式,

(module anything racket
    (provide data)
    (define data "Hello World!"))

其中,anything是模块名,racket是该模块的依赖。
所以,当载入main.rkt后,我们就可以获取data的值了。

在实际应用中,我们还可以对源代码进行任意解析,创建自己的语言。

2.3 Emacs Lisp

Emacs Lisp内置的读取器,并不支持自定义的读取器宏,
为了实现读取器宏,我们需要重写Emacs内置的read函数,
例如,elisp-reader

Emacs在启动时,会自动载入~/.emacs.d/init.el文件,然后执行其中的配置脚本,
因此,我们可以在init.el中调用elisp-reader

(1)创建~/.emacs.d/init.el文件,

(add-to-list 'load-path "~/.emacs.d/package/elisp-reader/")
(require 'elisp-reader)

(2)使用git克隆elisp-reader仓库到~/.emacas.d/package文件夹,

git clone https://github.com/mishoo/elisp-reader.el.git ~/.emacs.d/package/elisp-reader

(3)打开Emacs,自动执行init.el中的配置,

(4)在Emacs中定义一个读取器宏,然后求值整个Buffer,(M-x ev-b

(require 'cl-macs)

(def-reader-syntax ?{
    (lambda (in ch)
      (let ((list (er-read-list in ?} t)))
        `(list ,@(cl-loop for (key val) on list by #'cddr
                          collect `(cons ,key ,val))))))

(5)测试read函数的执行结果,(C-x C-e

(read "{ :foo 1 :bar \"string\" :baz (+ 2 3) }")
> (list (cons :foo 1) (cons :bar "string") (cons :baz (+ 2 3)))

(car { :foo 1 :bar "string" :baz (+ 2 3) })
> (:foo . 1)

源代码{ :foo 1 :bar "string" :baz (+ 2 3) }被直接读取成了一个列表对象,

((:foo . 1) (:bar "string") (:baz (+ 2 3)))

car函数而言,它看到的是列表对象,并不知道具体的语法是什么。

3. 总结

本文介绍了读取器宏的概念,Lisp各方言中会对读取器宏有不同程度的支持,
我们分析了Common Lisp,Racket以及Emacs Lisp的做法。

读取器宏直接作用到源代码文本上,用户定义的读取器宏可以对读取器进行“编程”,
借此可以支持自由灵活的语法,它是设计和使用DSL的神兵利器。

参考

Common Lisp the Language, 2nd Edition: 8.4 Compiler Macros
ANSI Common Lisp: 14.3 Read-Macros
Let Over Lambda: 4. Read Macros
The Racket Reference: 17.3.2 Using #lang reader
Github: elisp-reader

时间: 2024-08-24 05:49:24

Emacs之魂(九):读取器宏的相关文章

Emacs之魂(六):宏与元编程

Emacs之魂(一):开篇Emacs之魂(二):一分钟学会人界用法Emacs之魂(三):列表,引用和求值策略Emacs之魂(四):标识符,符号和变量Emacs之魂(五):变量的"指针"语义Emacs之魂(六):宏与元编程Emacs之魂(七):变量捕获与卫生宏Emacs之魂(八):反引用与嵌套反引用Emacs之魂(九):读取器宏 数据和代码 如果说Lisp语言有一个特性最能使人津津乐道的话,我想应该是它的宏系统(macro system)了吧, 在Lisp语言中,程序和代码的表现形式(t

Emacs之魂(七):变量捕获与卫生宏

Emacs之魂(一):开篇Emacs之魂(二):一分钟学会人界用法Emacs之魂(三):列表,引用和求值策略Emacs之魂(四):标识符,符号和变量Emacs之魂(五):变量的"指针"语义Emacs之魂(六):宏与元编程Emacs之魂(七):变量捕获与卫生宏Emacs之魂(八):反引用与嵌套反引用Emacs之魂(九):读取器宏 回顾 上文我们介绍了宏,它与函数是不同的,函数调用发生在程序执行期间,函数在调用之前,会先对它所有的实参进行求值,然后将形参绑定到这些实参的求值结果上,函数的返

Emacs之魂(二):一分钟学会人界用法

Emacs之魂(一):开篇Emacs之魂(二):一分钟学会人界用法Emacs之魂(三):列表,引用和求值策略Emacs之魂(四):标识符,符号和变量Emacs之魂(五):变量的"指针"语义Emacs之魂(六):宏与元编程Emacs之魂(七):变量捕获与卫生宏Emacs之魂(八):反引用与嵌套反引用Emacs之魂(九):读取器宏 上文提到了编辑器之战, 据江湖传说,Emacs被称为"神的编辑器", Emacs有着无与伦比的可扩展性和可定制性,简直变成了一个"

Emacs之魂(一):开篇

Emacs之魂(一):开篇Emacs之魂(二):一分钟学会人界用法Emacs之魂(三):列表,引用和求值策略Emacs之魂(四):标识符,符号和变量Emacs之魂(五):变量的"指针"语义Emacs之魂(六):宏与元编程Emacs之魂(七):变量捕获与卫生宏Emacs之魂(八):反引用与嵌套反引用Emacs之魂(九):读取器宏 程序员大部分的时间都是在和代码打交道,因此,对于文本编辑器一定不会陌生了. 编辑器是处理文本的工具. 就像趁手的兵器对武林高手的辅助作用一样, 强大的编辑器也会

Emacs之魂(四):标识符,符号和变量

Emacs之魂(一):开篇Emacs之魂(二):一分钟学会人界用法Emacs之魂(三):列表,引用和求值策略Emacs之魂(四):标识符,符号和变量Emacs之魂(五):变量的"指针"语义Emacs之魂(六):宏与元编程Emacs之魂(七):变量捕获与卫生宏Emacs之魂(八):反引用与嵌套反引用Emacs之魂(九):读取器宏 1. 符号 上文我们提到了Emacs Lisp是一种Lisp-2, 即同一个符号(symbol)在不同的上下文中,可以分别表示两种不同的值(value): 变量

Emacs之魂(五):变量的“指针”语义

Emacs之魂(一):开篇Emacs之魂(二):一分钟学会人界用法Emacs之魂(三):列表,引用和求值策略Emacs之魂(四):标识符,符号和变量Emacs之魂(五):变量的"指针"语义Emacs之魂(六):宏与元编程Emacs之魂(七):变量捕获与卫生宏Emacs之魂(八):反引用与嵌套反引用Emacs之魂(九):读取器宏 1. 语义学 在计算理论中,形式语义学是关注计算模式和程序设计语言含义的严格的数学研究领域. 语言的形式语义是用数学模型去表达该语言描述的可能计算来给出的. 提

Emacs之魂(八):反引用与嵌套反引用

Emacs之魂(一):开篇Emacs之魂(二):一分钟学会人界用法Emacs之魂(三):列表,引用和求值策略Emacs之魂(四):标识符,符号和变量Emacs之魂(五):变量的"指针"语义Emacs之魂(六):宏与元编程Emacs之魂(七):变量捕获与卫生宏Emacs之魂(八):反引用与嵌套反引用Emacs之魂(九):读取器宏 1. 反引用 上文我们介绍了如何使用defmacro定义宏, (defmacro inc (var) (list 'setq var (list '1+ var

XmlReader 读取器读取内存流 MemoryStream 的注意事项

stream|xml MemoryStream对象提供了无需进行IO就可以创建Stream的方法,XmlTextWriter和XmlReader提供快速书写和读取XML内容的方法,结合MemoryStream,就可以直接在内存中构造XmlTextWriter,并用XmlReader进行读取. 使用MemoryStream和XmlTextWriter进行书写XML,需要注意两点:XmlTextWriter.Flush操作和重设MemoryStream.Position = 0. C# <%@ Pa

日本Systems Support公司推出UHF手持读取器,读取距离长达20m

日本东北Systems Support公司(总部:宫城县仙台市)将推出与IC标签的通信距离长达20米以上的手持型UHF频带RFID读写器"Swing–U".该产品将从2016年6月开始受理订单. 该产品可读取距离20米以上的IC标签,读取范围超过110度,1秒钟可读取200个标签.利用该产品,无需打开距离较远的瓦楞纸箱即可盘货,进行室内的备件管理时也可以不用考虑IC标签的安装位置. 机身配备1.3英寸显示屏,可确认已读取的IC标签数量.电池剩余电量以及信号强弱等.另外还可用相机螺丝安装