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 处理。