很久之前看到王垠的怎样写一个解释器,感觉还是蛮简单的。
昨天看了SICP的视频,元循环求值器,就是在讲如果用scheme实现scheme解释器。那个老师戴上了巫师帽,披上魔法袍,在黑板上完成了这些代码,非常的有趣。
但是用语言A实现语言A,总是有种怪怪的感觉,因此决定用python重新实现一个基本的scheme解释器。
parser(expt)
选择scheme来实现解释器的唯一理由是lisp代码本身就是一颗抽象语法树,免去了parser的麻烦。用python解析scheme代码只需要简单的正则替换为一个嵌套的list就可以了,这里顺便将float和int的字符串做了转化。
eval和apply
正如SICP封面上的这个类似地球仪的球显示的一样(可能是个水晶球),解释器的核心就是eval和apply的相互递归调用。对于一个计算函数,eval对函数名和参数递归的调用自身进行求值。再调用apply将 参数-参数名 绑定到函数内部的环境中,然后对函数的body依次求值。
对于特殊的语法规则,如定义(define)、匿名函数(lambda)、条件语句(cond)等,无法用一个简单的函数规则去描述他们,因此在eval中进行了专门的处理。
对于数字(字符串也一样,这里未实现)的求值,它本身是自求值的,因此eval直接返回了它本身。
对于函数,分为了两类:基本函数和用户自定义函数,理论上我们只需要+、-、*、/等有限的基本函数就可以了。由于函数本身是不进行任何运算的(除非显式的调用它),对于函数,eval只返回了一个标记。
由于现代的scheme都是静态作用域,在函数定义的时候需要保存函数定义时环境的拷贝(函数的子环境),也就是上下文,并且将父环境也绑定到这个子环境上。此外也保存了用户自定义函数的参数表args及函数体body。
在apply的时候,对于基本函数调用python本身的功能进行求值。对于用户自定义函数,首先将 形参-实参 这样的key-value绑定到函数的子环境中,再对body进行求值。
对于变量,首先会在当前的环境中查找对应的值,如果没有的话会递归调用eval在当前的父环境上。为了避免死循环需要判断父环境是否为空,并且初始的全局环境是没有父环境的。
全部代码:
import re
def parse(expt):
expt = re.sub("(", " ( ", expt)
expt = re.sub(")", " ) ", expt)
expt = re.sub("s+", " ", expt)
expt = expt.strip().split(" ")
res = []
for i in expt:
if i == "(":
res.append(i)
elif i == ")":
temp = []
while res[-1] != "(":
temp.append(res.pop())
res.pop()
res.append(list(reversed(temp)))
else:
if re.match("^[0-9]+$", i):
res.append(int(i))
elif re.match("^[0-9][0-9.]*$", i):
res.append(float(i))
else:
res.append(i)
return res
def eval(expt, envs):
# 特殊语法及自定义函数(lambda 和cond的行为与普通的函数不一样,所以特殊处理)
if type(expt) == list:
if expt[0] == "define":
envs[expt[1]] = eval(expt[2], envs)
elif expt[0] == "lambda":
new_envs = envs.copy()
new_envs["PAR"] = envs
return ("lambda", expt[1], expt[2:], new_envs)
elif expt[0] == "cond":
for p, t in expt[1:]:
if p == "else" or eval(p, envs):
return eval(t, envs)
else:
expt = [eval(i, envs) for i in expt]
return apply(expt[0], expt[1:])
# 数字
elif type(expt) == float or type(expt) == int:
return expt
# 基本过程
elif expt == "+":
return ("proc", "+")
elif expt == "-":
return ("proc", "-")
elif expt == "*":
return ("proc", "*")
elif expt == "/":
return ("proc", "/")
elif expt == "=":
return ("proc", "=")
# 变量
elif expt in envs:
return envs[expt]
elif len(envs["PAR"]) > 0:
return eval(expt, envs["PAR"])
def apply(proc, args):
if proc[0] == "proc":
if proc[1] == "+":
return sum(args)
elif proc[1] == "-":
return args[0] - args[1]
elif proc[1] == "*":
return args[0] * args[1]
elif proc[1] == "/":
return args[0] / args[1]
elif proc[1] == "=":
return args[0] == args[1]
elif proc[0] == "lambda":
args_t, body, envs = proc[1:]
for k, v in zip(args_t, args):
envs[k] = v
for i in body:
res = eval(i, envs)
return res
def run(code):
print("run start!!!")
ast = parse(code)
env_golbal = {}
for i in ast:
res = eval(i, env_golbal)
print(res)
if __name__ == '__main__':
# fib(20) = 10946
code = """
(define fib
(lambda (n)
(define iter
(lambda (a b i)
(cond ((= i n) a)
(else (iter b (+ a b) (+ i 1))))))
(iter 1 1 0)))
(fib 20)
"""
code2 = """
(((lambda (F)
((lambda (f)
(lambda (n)
((F (f f)) n)))
(lambda (f)
(lambda (n)
((F (f f)) n)))))
(lambda (fib)
(lambda (n)
(cond ((= n 0) 1)
((= n 1) 1)
(else (+ (fib (- n 1))
(fib (- n 2))))))))
20)
"""
run(code)
run(code2)
这里的两个case分别是迭代版和递归版的斐波那契数列,其中递归版使用了Y算子。个人感觉Y算子是非常有意思的,可以在完全不使用函数定义的情况下实现递归调用(纯函数式不需要定义)。Y算子的前提是函数f本身是收敛的(有结束的,也就是函数不动点存在),然后利用F(f) = f,无限递归找到函数的不动点。
这里是python版本的Y算子
(
# Y算子 Y(F) = F(f(f))
lambda F: (
(
lambda f: (
lambda n:
F(f(f))(n)
)
)(
lambda f: (
lambda n:
F(f(f))(n)
)
)
)
)(
# F(fib) = fib = lambda n
lambda fib: (
lambda n: 1 if n <= 1 else fib(n - 1) + fib(n - 2)
)
)(20)
迭代版的是用尾递归实现的,(然而并没有尾递归优化)。