求值器补充
内置一些基本函数
- 比如cons car cdr 都是二元操作,可以直接嵌入到算术表达式求值里面,也可以提前将它们写入到global environment中去;
- list 因为list不是一个二元操作,因此直接写入环境会是很好的选择
- 我们也可以自己先实现几个小函数,然后作为内置的函数插入到global 环境里面,就可以直接在解释器里面使用了
- !补充:修复之前的一个bug,之前虽然将length 、 car 、cdr 写在了global-env 中,但是由于eval-proc 没有相应的处理代码,求值器会将它作为lambda 函数来处理,导致match错误,因此这里增加了一个(procedure? func ) 的判断,来提取处理这些内置函数!
lambda 函数 处理及函数调用
上篇对于函数的介绍过于简略了,而函数本身在scheme中的意义非凡
先说明一点,这里的函数均为单参数的,因为多参数的函数可以由单参数的lambda函数嵌套得来
考虑:
(lambda (x) (* x x ))
- 如何解析?
- 何时求值?
- x 变量的值应该去哪里寻找?
- 函数能否嵌套?
这里推荐王垠大大的一篇博文,他的文章里面实现了一个异常简洁的求值解释器,值得借鉴和学习;
第一个问题,我们可以利用match 进行匹配:
-
google racket 文档,搜索match即可,文档十分详细美观,还配有示例,推荐有疑问的时候去看看,match 这个语法是从王垠大大的博客中学习而来的,之前在SICP 的mit-scheme中使用的均是最最基础的语法结构。
-
扯点题外话,关于这一点,个人认为,初学的时候SICP的前3章更加何时,几乎没有任何语法糖,只有在迫不得已的时候才不情愿的再给你一个新的语法,十分锻炼对基础语法的掌握。并且习题最好都做一做,很有启发。这里有一份答案,可以参考(这个网页貌似现在被墙了,自己搞定)
第二个问题,其实SICP的求值器也有类似的考量,闭包是函数最为重要的性质,这里的闭包指的是实现过程的技术,和离散数学的闭包还是有所区分的,但其实也能感觉到一种“神似”。简洁一些,就是将lambda函数的函数体和求值环境包装起来。
第三个问题,我们在lambda定义的时候并不能求值,求值的合适时机是函数调用发生的时候,这也就是我们为什么把函数体和环境包装起来的原因。
那么函数求值也就顺理成章了,(func para)是函数调用的写法,如何判断是否是函数调用?
这里采用一种简单的方法,不判断。。其实是将它的判断放到eval的最后,当它不是前面已有的所有类型的时候,如果它还是个pair,那么它就是函数调用。
- 这里的func已经被我们包装成了闭包,我们需要将它的各个组分解析开
如何解析和之前闭包的实现有关,但一般用match都可以将它很好的解析出来 - para 是实际参数,它需要先递归调用eval进行求值
- 有了前两部之后,调用也就水到渠成了
(func-body para (extend-env! var para env))
解析好之后大概是上面那个形式。
第四个问题
出于闭包的函数定于,嵌套是可行的,事实上,函数必须是可嵌套的,不然这里的函数也就过于鸡肋了。
drive-loop
-
在我们写好上面的东西之后,一定早已迫不及待的想要开始试试了!
一次求值一个一定很扫兴。。。 -
每次求值如果要带个前缀才能调用我们的解释器也略显麻烦。
-
因此我们写个drive-loop的死循环来不断的进行读入输入、求值、输出。
-
当然也要做一些输出格式的控制,程序员也是要有美的追求嘛。
另外,我添加了一个对输入的判断,输入exit即执行racket内置的exit指令,退出我们的循环,这样就不用每次强行叉掉它了。
tips:Drracket可以导出可执行程序,选择“可在其他计算机使用”的话它还会自动将需要的dll动态链接文件一起打包起来,很方便~
More
- tips:在Drracket内部输入括号十分有趣,按下[这个键后,就可以输入();想要输入大括号,自然是shift+[ ; 想要输入[ 本身的话,使用alt+[就可以了,小括号用的多,这样还是比较好用的~
上面只是最最简单的一个内核,我们可以在此基础上添加一些好玩的东西。
- 感谢王垠的博客,在写的过程中他的很多总结的句子和词语真的是一语点醒梦中人的感觉。
- 感谢SICP,展示了一个不同的神奇世界。