require 'ruby_parser'
require 'ruby2ruby'
parser = RubyParser.new
r2r = Ruby2Ruby.new
def recur arr
if arr.is_a? Array
arr.map do |v|
pattern = recognize v
new_v = transform pattern, recur(v)
end
else
arr
end
end
def recognize v
case
when Array === v && v.size == 3 && v[0..1] == [:call, nil] && Symbol === v[2]
:iden
when Array === v && v.size == 2 && v[0] == :lit
:lit
end
end
def transform pattern, v
case pattern
when :iden
[v[0], [:const, :Var], v[2]]
when :lit
[:call, nil, :Lit, v]
else
v
end
end
# 标识符a转换为Var.a
code = recur parser.process('a + b').to_a
p r2r.process Sexp.from_array(code) # => "(Var.a + Var.b)"
# 字面量:a转换为Lit(:a)
code2 = recur parser.process(':a + b * (3 / 0)').to_a
p r2r.process Sexp.from_array(code2) # => "(Lit(:a) + (Var.b * (Lit(3) / Lit(0))))"
# 比较以下两者
p parser.process('a + b').to_a # => [:call, [:call, nil, :a], :+, [:call, nil, :b]]
p parser.process('Var.a + Var.b').to_a # => [:call, [:call, [:const, :Var], :a], :+, [:call, [:const, :Var], :b]]
所谓宏就是代码替换,例如 C 的宏是在字符串的层面上替换,但是这样坑比较多,安全的做法是在抽象语法树上做替换。
s-expression(S 表达式)是一种语法树表示法,同时也是 lisp 代码的形式,所以写 lisp 并不是写字符串,而是在用节点构造一棵语法树。这样的好处是“代码即数据”,也就是对于 lisp 而言,一段代码是像数据一样很容易被编辑的。1 _+ 2 + 3
这样的代码翻译成 lisp 是 (+ (_+ 1 2) 3)
,如果我想把+
方法替换成*
,字符串替换会变成1 _* 2 * 3
,而 lisp 可以明确地代换掉第一个节点的+
。
然而 sexp 并不是 lisp 独有的,因为他只是一种表示法,所以任何语言经过 parse 之后都可以变成 sexp(lisp 是不用 parse 的,可以直接 eval)。西雅图 ruby 有几个 sexp 相关的 gem,RubyParser
可以把 ruby 字符代码转为 sexp,而Ruby2Ruby
可以将 sexp 转为 ruby 代码,利用他们就可以在语法树的层面上编辑 ruby 代码。
这个东西有什么用呢?他可以增强 DSL,例如 ruby 里没办法捕获赋值,没办法捕获[]
和{}
的创建,a = 1
对于 DSL 来说是不可见的,但是我们可以把它转化为assign(Var.a, 1)
,然后用 assign 方法来捕获到 a 的赋值。另外诸如1 / 0
,在 ruby 代码里会立即被求值然后出错,但是经过预处理之后,可以让他正常地被 DSL 处理。