精华内容
下载资源
问答
  • 在小编的上一篇文章里,聊到程序员应该写写技术博客的话题,近日,多次有传智播客成都java培训中心的童鞋与小编互动,他们表示愿意坚持写技术博客,进而与小编讨论起了该如何写好技术博客的话题。以免又有同学追问...
        在小编的上一篇文章里,聊到程序员应该写写技术博客的话题,近日,多次有传智播客成都java培训中心的童鞋与小编互动,他们表示愿意坚持写技术博客,进而与小编讨论起了该如何写好技术博客的话题。以免又有同学追问类似的问题,小编以此文献给成都传智播客的莘莘学子,就当“普度众生”了吧。
        1. 内容要紧扣技术,不要将过多庞杂的东西充斥其中。
        你要明确你写的是技术博客,不是生活日记,所以,在写作过程中,尽量不要过多地掺杂你的生活琐事。例如,你今天和谁一起聚餐、一起唱歌、看了什么电影、参加了什么比赛等等,就算你的生活是多么的丰富多彩,也不一定能引起他人的兴趣。你要清楚,读者关注的是你的技术,而不是你本人。所以,既然打着技术博客的旗号,就不要“挂羊头卖狗肉”了,而要将笔墨重点挥洒在技术上。
        你可以写写你遇到了一个技术难题,是怎样解决它的,把解决的过程、采用的哪种方法写清楚;或者,写一些工作上的经验、总结,谈谈你做这项技术采用的一些小窍门等。
        2. 设定好目标读者,站在读者的角度去写
        我认识的一些程序员,他们的技术博客完全是用一行行代码拼凑出来的,鲜有文字备注或解释这段代码的含义,小编认为,与其说是技术博客,倒不如把它定义为技术笔记。也许,只有作者本人能明白他要表达的思想吧。
        成都传智播客讲师解释到,所谓站在读者的角度,就是首先确定好你写的技术博客是给谁看的,如果是针对跟自己技术差不多的人,那就要注意用一种与读者对话的语言去写,让读者感受到,你文章中体现出来的互动性,让读者产生共鸣,他才会真正爱看你的文章。
        3. 要由浅入深,图文并茂
        写技术博客,肯定不只写一篇文章,对于一个繁琐的知识点,肯定不只一个方面就能交代全面。你在给读者讲一门技术时,不是一下子挖多深,将他们镇住,让他们觉得你有多厉害,而是讲到一个复杂问题的时候,多引用实例,让他们更容易理解,加深记忆,真正学会这项技术。
        传智播客成都java培训中心讲师说,现在论坛里火爆的帖子必然有图,大家都在吼“有图有真相”。给你的技术博客配上恰当的图,使图文相得益彰,更能激发读者的阅读兴趣。
        4. 少转载,多原创
        当你看到一篇很不错的技术文章,也许觉得“复制-粘贴”到你的博客里很方便,但是这么做没有任何价值,在这一过程中,你扮演的角色不过是一名“搬运工”,同样的文字,读者在其他网站上同样可以看到,而且其在搜索引擎中的排名也在你之上,最终,你不费吹灰之力转来的文章,并没有增加你博客的点击率啊。
        传智播客成都java培训中心的讲师建议大家,可以在别人的技术上加入自己的理解,自己的思想表达,没必要完全转帖,发布原创文章不仅可以增加网站的流量,还可以提高网站的权重,对外链的增加也带来一定的好处。
        成都传智播客的学员们,小编只能把你们领进技术博客之门了,剩下的修行还需你们自己来完成,希望大家贵在坚持,写出优秀的技术博客。

     

    展开全文
  • 怎样写一个解释器

    2012-11-05 12:50:00
    这段时间心里有话说,写了不少博文,可是最后发现,真正对人有价值的其实没有几篇。大部分不过是一个技术人员的无病呻吟。...这个社会需要的书和文章,早在一百年前就已经有人写好了,我在这里凑个什么数。技...

      这段时间心里有话说,写了不少博文,可是最后发现,真正对人有价值的其实没有几篇。大部分不过是一个技术人员的无病呻吟。有句话说得好,我曾经为自己没有鞋而苦恼,直到我看见一个没有脚的人。

      早上把《狂人日记》拿出来看了一遍,发现其实国人并不需要我的博文,也不需要先进的技术,因为中国社会实质上正在再次沦为一个人吃人的社会。这个社会需要的书和文章,早在一百年前就已经有人写好了,我在这里凑个什么数。技术再好,有世界上最快的超级计算机,发射了宇宙飞船,有什么用呢?不要忘了,人活着不是为了技术,也不是为了国家的荣耀,人活着是为了自己。现在国人连放心的食物都没得吃,放心的房子都没得住,无论什么先进技术,其实都没什么用。中国需要的不是科学家和工程师,而是文化,法律和正义。一想起这些,就感觉不可承受之重,而这些,我都无能为力。

      我不得不说,博客和微博,是传播愚昧最有效的工具。只看新浪博客和微博那些随处冒出来的低俗广告就知道。说的越多,做的越少,所以我不想写了。写了也是白写,看了也白看。只留下一篇博文《怎样写一个解释器》,是我觉得唯一有价值的。

      文章:怎样写一个解释器 

      卖了好久关子了,说要写一个程序语言理论的入门读物,可是一直没有下笔。终于狠下心来兑现一部分承诺。今天就从解释器讲起吧。

      解释器是比较深入的内容。虽然我试图从最基本的原理讲起,尽量让这篇文章不依赖于其它的知识,但是这篇教程并不是针对函数式编程的入门,所以我假设你已经学会了最基本的 Scheme 和函数式编程。如果你完全不了解这些,可以读一下 SICP 

      的第一,二章。当然你也可以继续读这篇文章,有不懂的地方再去查资料。我在这里也会讲递归和模式匹配的原理。如果你已经了解这些东西,这里的内容也许可以加深你的理解。

      解释器其实不是很难的东西,可是好多人都不会写,因为在他们心目中解释器就像一个 Python 解释器那样复杂。如果你想开头就写一个 Python 解释器,那你多半永远也写不出来。你必须从最简单的语言开始,逐步增加语言的复杂度,才能构造出正确的解释器。这篇文章就是告诉你如何写出一个最简单的语言(lambda calculus) 的解释器,并且带有基本的的算术功能,可以作为一个高级计算器来使用。

      一般的编译器课程往往从语法分析(parsing)开始,折腾 lex 和 yacc 等工具。Parsing 的作用其实只是把字符串解码成程序的语法树(AST)结构。麻烦好久得到了 AST 之后,真正的困难才开始!而很多人在写完 parser 之后就已经倒下了。鉴于这个原因,这里我用“S-expression”来表示程序的语法树(AST)结构。S-expression 让我们可以直接跳过 parse 的步骤,进入关键的主题:语义(semantics)。

      这里用的 Scheme 实现是 Racket。为了让程序简洁,我使用了 Racket 的模式匹配(pattern matching)。如果你用其它的 Scheme 实现的话,恐怕要自己做一些调整。

      解释器是什么

      首先我们来谈一下解释器是什么。说白了解释器跟计算器差不多。它们都接受一个“表达式”,输出一个 “结果”。比如,得到 '(+ 1 2)之后就输出3。不过解释器的表达式要比计算器的表达式复杂一些。解释器接受的表达式叫做“程序”,而不只是简单的算术表达式。从本质上讲,每个程序都是一台机器的“描述”,而解释器就是在“模拟”这台机器的运转,也就是在进行“计算”。所以从某种意义上讲,解释器就是计算的本质。当然,不同的解释器就会带来不同的计算。

      需要注意的是,我们的解释器接受的参数是一个表达式的“数据结构”,而不是一个字符串。这里我们用一种叫“S-expression”的数据结构来表示表达式。比如表达式'(+ 1 2) 里面的内容是三个符号:'+, '1 和 '2,而不是字符串“(+ 12)”。从结构化的数据里面提取信息很方便,而从字符串里提取信息很麻烦,而且容易出错。

      从广义上讲,解释器是一个通用的概念。计算器实际上是解释器的一种形式,只不过它处理的语言比程序的解释器简单很多。也许你会发现,CPU 和人脑,从本质上来讲也是解释器,因为解释器的本质实际上是“任何用于处理语言的机器”。

      递归定义 (recursive definition)

      解释器一般都是“递归程序”。之所以是递归的原因,在于它处理的数据结构(程序)本身是“递归定义”的结构。算术表达式就是一个这样的结构,比如:'(*(+ 1 2) (* (- 9 6)4))。每一个表达式里面可以含有子表达式,子表达式里面还可以有子表达式,如此无穷无尽的嵌套。看似很复杂,其实它的定义不过是:

      “算术表达式”有两种形式:

      1) 一个数

      2) 一个 '(op e1 e2) 这样的结构(其中 e1 和 e2 是两个“算术表达式”)

      看出来哪里在“递归”了吗?我们本来在定义“算术表达式”这个概念,而它的定义里面用到了“算术表达式”这个概念本身!这就构造了一个“回路”,让我们可以生成任意深度的表达式。

      很多其它的数据,包括自然数,都是可以用递归来定义的。比如常见的对自然数的定义是:

      “自然数”有两种形式:

      1) 零

      2) 某个“自然数”的后继

      看到了吗?“自然数”的定义里面出现了它自己!这就是为什么我们有无穷多个自然数。

      所以可以说递归是无所不在的,甚至有人说递归就是自然界的终极原理。递归的数据总是需要递归的程序来处理。虽然递归有时候表现为另外的形式,比如循环(loop),但是“递归”这个概念比“循环”更广泛一些。有很多递归程序不能用循环来表达,比如我们今天要写的解释器就是一个递归程序,它就不能用循环来表达。所以写出正确的递归程序,对于设计任何系统都是至关重要的。其实递归的概念不限于程序设计。在数学证明里面有个概念叫“归纳法”(induction),比如“数学归纳法”(mathematicalinduction)。其实归纳法跟递归完全是一回事。

      我们今天的解释器就是一个递归程序。它接受一个表达式,递归的调用它自己来处理各个子表达式,然后把各个递归的结果组合在一起,形成最后的结果。这有点像二叉树遍历,只不过我们的数据结构(程序)比二叉树复杂一些。

      模式匹配和递归:一个简单的计算器

      既然计算器是一种最简单的解释器,那么我们为何不从计算器开始写?下面就是一个计算器,它可以计算四则运算的表达式。这些表达式可以任意的嵌套,比如'(* (+ 1 2) (+ 3 4))。我想从这个简单的例子来讲一下模式匹配(pattern matching) 和递归(recursion) 的原理。

      下面就是这个计算器的代码。它接受一个表达式,输出一个数字作为结果,正如上一节所示。

      (define calc

      (lambda (exp)

      (matchexp                               ; 匹配表达式的两种情况

      [(? number? x)x]                      ; 是数字,直接返回

      [`(,op ,e1,e2)                        ; 匹配并且提取出操作符 op 和两个操作数 e1, e2

      (let ([v1 (calce1)]                  ; 递归调用 calc 自己,得到 e1 的值

      [v2 (calce2)])                 ; 递归调用 calc 自己,得到 e2 的值

      (matchop                           ; 分支:处理操作符 op 的 4 种情况

      ['+ (+ v1v2)]                    ; 如果是加号,输出结果为 (+ v1 v2)

      ['- (- v1v2)]                    ; 如果是减号,乘号,除号,相似的处理

      ['* (* v1 v2)]

      ['/ (/ v1 v2)]))])))

      这里的 match 语句是一个模式匹配。它的形式是这样:

      (match exp

      [模式结果]

      [模式结果]

      ...  ...

      )

      它根据表达式 exp 的“结构”来进行“分支”操作。每一个分支由两部分组成,左边的是一个“模式”,右边的是一个结果。左边的模式在匹配之后可能会绑定一些变量,它们可以在右边的表达式里面使用。

      一般说来,数据的“定义”有多少种情况,用来处理它的“模式”就有多少情况。比如算术表达式有两种情况,数字或者 (op e1e2)。所以用来处理它的 match 语句就有两种模式。“你所有的情况,我都能处理”,这就是“穷举法”。穷举的思想非常重要,你漏掉的任何一种情况,都非常有可能带来麻烦。所谓的“数学归纳法”,就是这种穷举法在自然数的递归定义上面的表现。因为你穷举了所有的自然数可能被构造的两种形式,所以你能确保定理对“任意自然数”成立。

      那么模式是如何工作的呢?比如 '(,op ,e1 ,e2) 就是一个模式(pattern),它被用来匹配输入的 exp。模式匹配基本的原理就是匹配与它“结构相同”的数据。比如,如果 exp 是 '(+ 1 2),那么 '(,op ,e1 ,e2)就会把 op 绑定到 '+,把 e1 绑定到 '1,把 e2 绑定到 '2。这是因为它们结构相同:

      '(,op ,e1 ,e2)

      '( +  1  2)

      说白了,模式就是一个可以含有“名字”(像 op, e1 和 e2)的“数据结构”,像 '(,op ,e1,e2)。我们拿这个带有名字的结构去“匹配”实际的数据(像 '(+ 12))。当它们一一对应之后,这些名字就自动被绑定到实际数据里相应位置的值。模式里面不但可以含有名字,也可以含有具体的数据。比如你可以构造一个模式'(,op ,e1 42),用来匹配第二个操作数固定为 42 的那些表达式。

      看见左边的模式,你就像直接“看见”了输入数据的形态,然后对里面的元素进行操作。它可以让我们一次性的“拆散”(destruct)数据结构,把各个部件(域)的值绑定到多个变量,而不需要使用多个访问函数。所以模式匹配是非常直观的编程方式,值得每种语言借鉴。很多函数式语言里都有类似的功能,比如 ML 和 Haskell。

      注意这里 e1 和 e2 里面的操作数还不是值,它们是表达式。我们递归的调用 interp1 自己,分别得到 e1 和 e2 的值 v1 和 v2。它们应该是数字。

      你注意到我们在什么地方使用了递归吗?如果你再看一下“算术表达式”的定义:

      “算术表达式”有两种形式:

      1) 一个数

      2) 一个 '(op e1 e2) 这样的结构(其中 e1 和 e2 是两个“算术表达式”)

      你就会发现这个定义里面“递归”的地方就是 e1 和 e2,所以 calc 在 e1 和 e2 上面递归的调用自己。如果你在数据定义的每个递归处都进行递归,那么你的递归程序就会穷举所有的情况。

      之后,我们根据操作符 op 的不同,对这两个值 v1 和 v2 分别进行操作。如果 op 是加号 '+,我们就调用 Scheme 的加法操作,作用于 v1 和 v2,并且返回运算所得的值。如果是减号,乘号,除号,我们也进行相应的操作,返回它们的值。

      所以你就可以得到如下的测试结果:

      (calc '(+ 1 2))

      ;; => 3

      (calc '(* 2 3))

      ;; => 6

      (calc '(* (+ 1 2) (+ 3 4)))

      ;; => 21

      一个计算器就是这么简单。你可以试试这些例子,然后自己再做一些新的例子。

      什么是 lambda calculus?

      现在让我们过渡到一种更强大的语言:lambdacalculus。它虽然名字看起来很吓人,但是其实非常简单。它的三个元素分别是是:变量,函数,调用。用传统的表达法,它们看起来就是:

      变量:x

      函数:λx.t

      调用:t1 t2

      每个程序语言里面都有这三个元素,只不过具体的语法不同,所以你其实每天都在使用 lambda calculus。用 Scheme 作为例子,这三个元素看起来就像:

      变量:x

      函数:(lambda (x) e)

      调用:(e1 e2)

      一般的程序语言还有很多其它的结构,可是这三个元素却是缺一不可的。所以构建解释器的最关键步骤就是把这三个东西搞清楚。构造任何一个语言的解释器一般都是从这三个元素开始,在确保它们完全正确之后才慢慢加入其它的元素。

      有一个很简单的思维方式可以让你直接看到这三元素的本质。记得我说过,每个程序都是一个“机器的描述”吗?所以每个 lambdacalculus 的表达式也是一个机器的描述。这种机器跟电子线路非常相似。lambda calculus 的程序和机器有这样的一一对应关系:一个变量就是一根导线。一个函数就是某种电子器件的“样板”,有它自己的输入和输出端子,自己的逻辑。一个调用都是在设计中插入一个电子器件的“实例”,把它的输入端子连接到某些已有的导线,这些导线被叫做“参数”。所以一个 lambda calculus 的解释器实际上就是一个电子线路的模拟器。所以如果你听说有些芯片公司开始用类似 Haskell 的语言(比如 Bluespec System Verilog)来设计硬件,也就不奇怪了。

      需要注意的是,跟一般语言不同,lambda calculus 的函数只有一个参数。这其实不是一个严重的限制,因为 lambdacalculus 的函数可以被作为值传递 (这叫 first-classfunction),所以你可以用嵌套的函数定义来表示两个以上参数的函数。比如,(lambda (x) (lambda (y) y))就可以表示一个两个参数的函数,它返回第二个参数。不过当它被调用的时候,你需要两层调用,就像这样:

      (((lambda (x) (lambda (y) y)) 1) 2)

      ;; => 2

      虽然看起来丑一点,但是它让我们的解释器达到终极的简单。简单对于设计程序语言的人是至关重要的。一开头就追求复杂的设计,往往导致一堆纠缠不清的问题。

      lambda calculus 不同于普通语言的另外一个特点就是它没有数字等基本的数据类型,所以你不能直接用 lambdacalculus 来计算像 (+ 1 2) 这样的表达式。但是有意思的是,数字却可以被 lambda calculus 的三个基本元素“编码”(encoding)出来。这种编码可以用来表示自然数,布尔类型,pair,list,以至于所有的数据结构。它还可以表示 if 条件语句等复杂的语法结构。常见的一种这样的编码叫做 Church encoding。所以 lambda calculus 其实可以产生出几乎所有程序语言的功能。中国的古话“三生万物”,也许就是这个意思。

      求值顺序,call-by-name, call-by-value

      当解释一个程序的时候,我们可以有好几种不同的“求值顺序”(evaluationorder)。这有点像遍历二叉树有好几种不同的顺序一样(中序,前序,后序)。只不过这里的顺序更加复杂一些。比如下面的程序:

      ((lambda (x) (* x x)) (+ 1 2))

      我们可以先执行最外层的调用,把 (+ 1 2) 传递进入函数,得到 (* (+ 1 2) (+ 12))。所以求值顺序是:

      ((lambda (x) (* x x)) (+ 1 2))

      => (* (+ 1 2) (+ 1 2))

      => (* 3 (+ 1 2))

      => (* 3 3)

      => 9

      但是我们也可以先算出 (+ 1 2) 的结果,然后再把它传进这个函数。所以求值顺序是:

      ((lambda (x) (* x x)) (+ 1 2))

      => ((lambda (x) (* x x)) 3)

      => (* 3 3)

      => 9

      我们把第一种方式叫做 call-by-name (CBN),因为它把参数的“名字”(也就是表达式自己)传进函数。我们把第二种方式叫做 call-by-value (CBV),因为它先把参数的名字进行解释,得到它们的“值”之后,才把它们传进函数。

      这两种解释方式的效率是不一样的。从上面的例子,你可以看出 CBN 比 CBV 多出了一步。为什么呢?因为函数 (lambda (x)(* x x)) 里面有两个 x,所以 (+ 1 2) 被传进函数的时候被复制了一份。之后我们需要对它的每一拷贝都进行一次解释,所以(+ 1 2) 被计算了两次!

      鉴于这个原因,几乎所有的程序语言都采用 CBV,而不是 CBN。CBV 常常被叫做“strict”或者“applicativeorder”。虽然 CBN 效率低下,与它等价的一种顺序 call-by-need 却没有这个问题。call-by-need 的基本原理是对 CBN 中被拷贝的表达式进行“共享”和“记忆”。当一个表达式的一个拷贝被计算过了之后,其它的拷贝自动得到它的值,从而避免重复求值。call-by-need 也叫“lazy evaluation”,它是 Haskell 语言所用的语义。

      求值顺序不只停留于 call-by-name, call-by-value,call-by-need。人们还设计了很多种其它的求值顺序,虽然它们大部分都不能像 call-by-value 和 call-by-need 这么实用。

      完整的 lambda calculus 解释器

      下面是我们今天要完成的解释器,它只有 39 行(不包括空行和注释)。你可以先留意一下各个部分的注释,它们标注各个部件的名称,并且有少许讲解。这个解释器实现的是 CBV 顺序的 lambdacalculus,外加基本的算术。加入基本算术的原因是为了可以让初学者写出比较有趣一点的程序,不至于一开头就被迫去学 Churchencoding。

      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

      ;;; 以下三个定义 env0, ent-env, lookup 是对环境(environment)的基本操作:

      ;; 空环境

      (define env0 '())

      ;; 扩展。对环境 env 进行扩展,把 x 映射到 v,得到一个新的环境

      (define ext-env

      (lambda (x v env)

      (cons `(,x .,v) env)))

      ;; 查找。在环境中 env 中查找 x 的值

      (define lookup

      (lambda (x env)

      (let ([p(assq x env)])

      (cond

      [(not p) x]

      [else (cdr p)]))))

      ;; 闭包的数据结构定义,包含一个函数定义 f 和它定义时所在的环境

      (struct Closure (f env))

      ;; 解释器的递归定义(接受两个参数,表达式 exp 和环境 env)

      ;; 共 5 种情况(变量,函数,调用,数字,算术表达式)

      (define interp1

      (lambda (exp env)

      (match exp                     ; 模式匹配 exp 的以下情况(分支)

      [(? symbol? x) (lookup x env)]           ; 变量

      [(? number? x) x]                 ; 数字

      [`(lambda (,x) ,e)                 ; 函数

      (Closure exp env)]

      [`(,e1 ,e2)                    ; 调用

      (let ([v1 (interp1 e1 env)]

      [v2 (interp1 e2 env)])

      (match v1

      [(Closure `(lambda (,x) ,e) env1)

      (interp1 e (ext-env x v2 env1))]))]

      [`(,op ,e1 ,e2)                  ;算术表达式

      (let ([v1 (interp1 e1 env)]

      [v2 (interp1 e2 env)])

      (match op

      ['+ (+ v1 v2)]

      ['- (- v1 v2)]

      ['* (* v1 v2)]

      ['/ (/ v1 v2)]))])))

      ;; 解释器的“用户界面”函数。它把 interp1 包装起来,掩盖第二个参数,初始值为 env0

      (define interp

      (lambda (exp)

      (interp1 expenv0)))

      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

      测试例子

      这里有一些测试的例子。你最好先玩一下再继续往下看,或者自己写一些新的例子。学习程序的最好办法就是玩弄这个程序,给它一些输入,观察它的行为。有时候这比任何语言的描述都要直观和清晰。

      (interp '(+ 1 2))

      ;; => 3

      (interp '(* 2 3))

      ;; => 6

      (interp '(* 2 (+ 3 4)))

      ;; => 14

      (interp '(* (+ 1 2) (+ 3 4)))

      ;; => 21

      (interp '(((lambda (x) (lambda (y) (* x y))) 2) 3))

      ;; => 6

      (interp '((lambda (x) (* 2 x)) 3))

      ;; => 6

      (interp '((lambda (y) (((lambda (y) (lambda (x) (* y 2))) 3)0)) 4))

      ;; => 6

      ;; (interp '(1 2))

      ;; => match: no matching clause for 1

      在接下来的几节,我们来看看这个解释器里主要的分支(match)表达式的各种情况。

      对基本算术操作的解释

      算术操作在解释器里是最简单也是最“基础”的东西,因为它们不能再被细分为更小的元素了。所以在接触函数,调用等复杂的结构之前,我们来看一看对算术操作的处理。以下就是这个解释器里处理基本算术的部分,它是 interp1 的最后一个分支。

      (matchexp

      ... ...

      [`(,op ,e1 ,e2)

      (let ([v1 (interp1 e1env)]           ; 递归调用 interp1 自己,得到 e1 的值

      [v2 (interp1 e2env)])          ; 递归调用 interp1 自己,得到 e2 的值

      (matchop                           ; 分支:处理操作符 op 的 4 种情况

      ['+ (+ v1v2)]                    ; 如果是加号,输出结果为 (+ v1 v2)

      ['- (- v1v2)]                    ; 如果是减号,乘号,除号,相似的处理

      ['* (* v1 v2)]

      ['/ (/ v1 v2)]))])

      你可以看到它几乎跟刚才写的计算器一模一样,不过现在 interp1 的调用多了一个参数 env 而已。这个 env 是什么,我们下面很快就讲。

      变量和函数

      我想用两个小节来简单介绍一下变量,函数和环境。稍后的几节我们再来看它们是如何实现的。

      变量(variable)的产生是数学史上的最大突破之一。因为变量可以被绑定到不同的值,从而使得函数的实现成为可能。比如数学函数f(x) = x * 2,其中 x 是一个变量,它把输入的值传递到函数的主体“x *2”里面。如果没有变量,函数就不可能实现。

      对变量的最基本的操作是对它的“绑定”(binding)和“取值”(evaluate)。什么是绑定呢?拿上面的函数 f (x)作为例子吧。当 x 等于 1 的时候,f(x) 的值是 2,而当 x 等于 2 的时候,f(x) 的值是 4。在上面的句子里,我们对x 进行了两次绑定。第一次 x 被绑定到了 1,第二次被绑定到了2。你可以把“绑定”理解成这样一个动作,就像当你把插头插进电源插座的那一瞬间。插头的插脚就是 f (x) 里面的那个 x,而 x * 2 里面的x,则是电线的另外一端。所以当你把插头插进插座,电流就通过这根电线到达另外一端。如果电线导电性能良好,两头的电压应该几乎相等。有点跑题了……反正只要记住一点:绑定就是插进插座的那个“动作”。

      那么“取值”呢?再想一下前面的例子,当我们用伏特表测电线另外一端的电压的时候,我们就是在对这个变量进行取值。有时候这种取值的过程不是那么明显,比如电流如果驱动了风扇的电动机。虽然电线的另外一头没有显示电压,其实电流已经作用于电动机的输入端子,进入线圈。所以你也可以说其实是电动机在对变量进行取值。

      环境

      我们的解释器是一个挺笨的程序,它只能一步一步的做事情。比如,当它需要求 f (1) 的值的时候,它做以下两步操作:1) 把 x 绑定到1; 2) 进入 f 的函数体对 x * 2 进行求值。这就像一个人做出这两个动作:1)把插头插进插座,2)走到电线的另外一头测量它的电压,并且把结果乘以 2。在第一步和第二步之间,我们如何记住 x 的值呢?它必须被传递到那个用来处理函数体的递归解释器里面。这就是为什么我们需要“环境”,也就是 interp1 的第二个参数 env。

      环境记录变量的值,并且把它们传递到它们的“可见区域”,用术语说就叫做“作用域”(scope)。通常作用域是整个函数体,但是有一个例外,就是当函数体内有嵌套的函数定义的时候,内部的那个函数如果有同样的参数名,那么外层的参数名就会被“屏蔽”(shadow)掉。这样内部的函数体就看不到外层的参数了,只看到它自己的。比如(lambda (x) (lambda (x) (* x 2))),里面的那个 x 看到的就是内层函数的x,而不是外层的。

      在我们的解释器里,用于处理环境的主要部件如下:

      ;; 空环境

      (define env0 '())

      ;; 对环境 env 进行扩展,把 x 映射到 v

      (define ext-env

      (lambda (x v env)

      (cons `(,x .,v) env)))

      ;; 取值。在环境中 env 中查找 x 的值

      (define lookup

      (lambda (x env)

      (let ([p(assq x env)])

      (cond

      [(not p) x]

      [else (cdr p)]))))

      这里我们用的是 Scheme 的 association list 来表示环境。Association list 看起来像这个样子:((x . 1) (y . 2) (z . 5))。也就是一个两元组(pair)的链表,左边的元素是 key,右边的元素是 value。写的直观一点就是:

      ((x . 1)

      (y . 2)

      (z . 5))

      查表操作就是从头到尾搜索,如果左边的 key 是要找的变量,就返回整个 pair。简单吧?

      ext-env 扩展一个环境。比如,如果原来的环境是 ((y . 2) (z . 5)) 那么 (ext-env x1 ((y . 2) (z .5))),就会得到 ((x . 1) (y . 2) (z . 5))。也就是把 (x . 1)放到最前面去。值得注意的一点是,环境被扩展以后其实是形成了一个新的环境,原来的环境并没有被“改变”。比如上面红色的部分就是原来的数据结构,只不过它被放到另一个更大的结构里面了。这叫做“函数式数据结构”。这个性质在我们的解释器里是至关重要的,因为当我们扩展了一个环境之后,其它部分的代码仍然可以原封不动的访问扩展前的那个旧的环境。当我们讲到调用的时候也许你就会发现这个性质的用处。

      你也可以用另外的,更高效的数据结构(比如 splaytree)来表示环境。你甚至可以用函数来表示环境。唯一的要求就是,它是变量到值的“映射”(map)。你把 x 映射到 1,待会儿查询x 的值,它应该仍然是 1,而不会消失掉或者别的值。也就是说,这几个函数要满足这样的一种“界面约定”:如果 e 是 (ext-env'x 1 env) 返回的环境,那么 (lookup 'x e) 应该返回 1。只要满足这样的界面约定的函数都可以被叫做 ext-env 和 lookup,以至于可以它们用来完全替代这里的函数而不会导致其它代码的修改。这叫做“抽象”,也就是“面向对象语言”的精髓所在。

      对变量的解释

      了解了变量,函数和环境,让我们来看看解释器对变量的操作,也就是 interp1 的 match 的第一种情况。它非常简单,就是在环境中查找变量的值。这里的 (? symbol? x) 是一个特殊的模式,它使用 Scheme 函数 symbol? 来判断输入是否匹配,如果是的就把它绑定到 x,查找它的值,然后返回这个值。

      [(? symbol? x) (lookup x env)]

      注意由于我们的解释器是递归的,所以这个值也许会被返回到更高层的表达式,比如 (* x 2)。

      对数字的解释

      对数字的解释也很简单。由于在 Scheme 里面名字 '2 就是数字 2(我认为这是 Scheme 设计上的一个小错误),所以我们不需要对数字的名字做特殊的处理,把它们原封不动的返回。

      [(? number? x) x]

      对函数的解释

      对函数的解释是一个比较难说清楚的问题。由于函数体内也许会含有外层函数的参数,比如 (lambda (y) (lambda (x) (* y2))) 里面的 y 是外层函数的参数,却出现在内层函数定义中。如果内层函数被作为值返回,那么 (* y 2) 就会跑到y的作用域以外。所以我们必须把函数做成“闭包”(closure)。闭包是一种特殊的数据结构,它由两个元素组成:函数的定义和当前的环境。所以我们对(lambda (x) e) 这样一个函数的解释就是这样:

      [`(lambda (,x) ,e)

      (Closure exp env)]

      注意这里的 exp 就是 `(lambda (,x) ,e)自己。我们只是把它包装了一下,把它与当前的环境一起放到一个数据结构(闭包)里,并不进行任何复杂的运算。这里我们的闭包用的是一个 Racket 的 struct 结构,也就是一个记录类型(record)。你也可以用其它形式来表示闭包,比如有些解释器教程提倡用函数来表示闭包。其实用什么形式都无所谓,只要能存储 exp 和 env 的值。我比较喜欢使用 struct,因为它的界面简单清晰。

      为什么需要保存当前的环境呢?因为当这个函数被作为一个值返回的时候,我们必须记住里面的外层函数的参数的绑定。比如,(lambda (y)(lambda (x) (* y 2)))。当它被作用于 1 之后,我们会得到内层的函数 (lambda (x) (* y 2))。当这个函数被经过一阵周折之后再被调用的时候,y应该等于几呢?正确的做法应该是等于1。这种把外层参数的值记录在内层函数的闭包里的做法,叫做“lexicalscoping”或者“static scoping”。

      如果你不做闭包,而是把函数体直接返回,那么在 (lambda (x) (* y 2)) 被调用的位置,你可能会另外找到一个y,从而使用它的值。在调用的时候“动态”解析变量的做法,叫做“dynamic scoping”。事实证明 dynamicscoping 的做法是严重错误的,它导致了早期语言里面出现的各种很难发现的 bug。很多早期的语言是 dynamicscoping,就是因为它们只保存了函数的代码,而没有保存它定义处的环境。这样要简单一些,但是带来太多的麻烦。早期的 Lisp,现在的 Emacs Lisp 和 TeX 就是使用 dynamic scoping 的语言。

      为了演示 lexical scoping 和 dynamic scoping 的区别。你可以在我们的解释器里执行以下代码:

      (interp '((lambda (y) (((lambda (y)(lambda (x) (* y 2))) 3) 0)) 4))

      其中红色的部分就是上面提到的例子。在这里,(* y 2) 里的 y,其实是最里面的那个 (lambda (y) ...)里的。当红色部分被作用于 3 之后。 (lambda (x) (* y2)) 被作为一个值返回。然后它被作用于 0(x 被绑定到 0,被忽略),所以 (*y 2) 应该等于 6。但是如果我们的解释器是 dynamic scoping,那么最后的结果就会等于 8。这是因为最外层的 y 开头被绑定到了 4,而 dynamic scoping 没有记住内层的 y 的值,所以使用了外层那个 y 的值。

      为什么 Lexical scoping 更好呢?你可以从很简单的直觉来理解。当你构造一个“内部函数”的时候,如果它引用了外面的变量,比如这个例子里的 y,那么从外层的 y 到这个函数的内部,出现了一条“信道”(channel)。你可以把这个内部函数想象成一个电路元件,它的内部有一个节点 y 连接到一根从外部来的电线 y。当这个元件被返回,就像这个元件被挖出来送到别的地方去用。但是在它被使用的地方(调用),这个 y 节点应该从哪里得到输入呢?显然你不应该使用调用处的某个 y,因为这个 y 和之前的那个 y,虽然都叫 y,却不是“同一个y”,也就是同名异义。它们甚至可以代表不同的类型的东西。所以这个 y 应该仍然连接原来的那根 y 电线。当这个内部元件移动的时候,就像这跟电线被无限的延长,但是它始终连接到原来的节点。

      对函数调用的解释

      好,我们终于到了最后的关头,函数调用。函数调用都是 (e1 e2) 这样的形式,所以我们需要先分别求出 e1 和 e2 的值。这跟基本运算的时候需要先求出两个操作数的值相似。

      函数调用就像把一个电器的插头插进插座,使它开始运转。比如,当 (lambda (x) (* x 2)) 被作用于 1 时,我们把 x 绑定到 1,然后解释它的函数体 (* x2)。但是这里有一个问题,如果函数体内有未绑定的变量,它应该取什么值呢?从上面闭包的讨论,你已经知道了,其实操作数 e1 被求值之后应该是一个闭包,所以它的里面应该有未绑定变量的值。所以,我们就把这个闭包中保存的环境(env1)取出来,扩展它,把 x 绑定到 v2,然后用这个扩展后的环境来解释函数体。

      所以函数调用的代码如下:

      [`(,e1,e2)                                            

      (let ([v1 (interp1 e1 env)]

      [v2 (interp1 e2 env)])

      (match v1

      [(Closure `(lambda (,x) ,e) env1)  ;用模式匹配的方式取出闭包里的各个子结构

      (interp1 e (ext-env x v2env1))]  ; 在闭包的环境中把 x 绑定到 v2,解释函数体

      ))]

      你可能会奇怪,那么解释器的环境 env 难道这里就不用了吗?是的。我们通过 env 来计算 e1 和 e2 的值,是因为 e1 和 e2 里面的变量存在于“当前环境”。我们把 e1 里面的环境 env1 取出来用于计算函数体,是因为函数体并不是在当前环境定义的,它的代码在别的地方。如果我们用 env 来解释函数体,那就成了 dynamic scoping。

      实验:你可以把 (interp1 e (ext-env x v2 env1)) 里面的 env1 改成 env,再试试我们之前讨论过的代码,它的输出就会是 8:

      (interp '((lambda (y) (((lambda (y) (lambda (x) (* y2))) 3) 0)) 4))

      另外在这里我们也看到环境用“函数式数据结构”表示的好处。闭包被调用时它的环境被扩展,但是这并不会影响原来的那个环境,我们得到的是一个新的环境。所以当函数调用返回之后,函数的参数绑定就自动“注销”了。如果你用一个非函数式的数据结构,在绑定参数时不生成新的环境,而是对已有环境进行赋值,那么这个赋值操作就会永久性的改变原来环境的内容。所以你在函数返回之后必须删除参数的绑定。这样不但麻烦,而且在复杂的情况下几乎不可能有效的控制。每一次当我使用赋值操作来修改环境,最后都会出现意想不到的麻烦。所以在写解释器,编译器的时候,我都只使用函数式数据结构来表示环境。

      下一步

      在懂得了这里讲述的基本的解释器构造之后,下一步可以做什么呢?其实从这个基本的解释器原型,你可以进一步发展出很多内容,比如:

    • 在这个解释器里加一些构造,比如递归和状态,你就可以得到一个完整的程序语言的解释器,比如 Scheme 或者 Python。
    • 对这个解释器进行“抽象”,你就可以对程序进行类型推导。感兴趣的话可以参考我实现的这个 Hindley-Milner 系统,或者 Python 类型推导
    • 对这个解释器进行一些改变,就可以得到一个非常强大的 online partialevaluator,可以用于编译器优化。

      如果有问题的话,欢迎跟我联系:shredderyin@gmail.com。另外需要指出的是,学会这个解释器并不等于理解了程序语言的理论。所以在学会了这些之后,还是要看一些语义学的书。

    展开全文
  • 日记090531

    2009-05-31 10:13:00
    几天没了,前段日子先是复习Windows,考了,题目和往年有百分之七十以上相同,BS这几天复习数据库,英文版的看得比较慢,不过的确比看中文版的感觉点,有足够的时间看就了,可惜没有今天我的兔子终于有立体...

    好几天没写了,前段日子先是复习Windows,考了,题目和往年有百分之七十以上相同,BS

    这几天复习数据库,英文版的看得比较慢,不过的确比看中文版的感觉好点,有足够的时间看就好了,可惜没有

    今天我的兔子终于有立体感了,哦耶,兴奋中(不然不会跑来写),原来把各个面的法线计算出来,在绘制面是用上就OK了

    嗯,下午继续自习,顺便看有没有美女,哈哈,不过有美女又怎样呢?

    展开全文
  • 2019/1/12 训练日记

    2019-01-13 21:25:28
    我是怀着怎样的心情这篇训练日记的呢, 其实我也不是很清楚,心情还是蛮复杂的, 看了看自己的当初的第一篇博客 从17年的三月份到现在,emmmm 算了,又是新的一年寒假,继续新的征程。   今天先是补了...

    我是怀着怎样的心情写这篇训练日记的呢,

    其实我也不是很清楚,心情还是蛮复杂的,

    看了看自己写的当初的第一篇博客

    从17年的三月份写到现在,emmmm

    算了,又是新的一年寒假,继续新的征程。

     

    今天先是补了补昨天晚上codeforce上的题目,

    然后总结了一部分图论,不是很多也不是很充分,未完待续

    看的博客有好有坏,但都有能够吸取的地方,有些题目什么的都做了记录

    但还没有详细的总结,争取这段时间把它们整合完。

    展开全文
  • 在开始本次的日记之前,首先需要给大家道个歉。因为最近一直忙于新的项目,所以有一个多月都没有继续更新了。 本篇日记我们将详细探讨如何将表现领域的类映射到现有的数据库。现在的经济形势不是太,很多公司都...
  • 无论是的还是坏的,只要你认为你是怎样的,你就是对的。 今天是单老师上课的第二天,渐渐的习惯了单老师的风格,能更的调整自己上课的状态了。感觉自己学到很多,而且学习也不是很辛苦。 1. 复习了mysql,...
  •  其实这是我今天凌晨两点多才看完的,所以日记了今天的日期,心情是越来越了,就快要完成任务。其实很累但也感受到收获的喜悦,是值得的。 我看了类加载器和代理的视频,在这里知道类加载器的作用和怎样去...
  • 应该怎样管理时间

    2012-06-18 00:22:35
    周末很快就过去了,每次周五,都会认为我这个周末要做什么什么的,但真正到周末了,又什么都没做, 昨天下载了一个本电子书《人脉 是设计出来的》感觉得还不错,...准备又开始写日记  2.做人最初阶段还是傻一点
  • 在开始本次的日记之前,首先需要给大家道个歉。因为最近一直忙于新的项目,所以有一个多月都没有继续更新了。 本篇日记我们将详细探讨如何将表现领域的类映射到现有的数据库。现在的经济形势不是太,很多公司都...
  • 你是否能挣到钱,最关键的并不是你有没有点子,也不是你有多聪明,而是你的自信程度。 假如你根本不相信你能做到的话,那么你就根本不会动手做,而假如你不开始去做,那么你就什么也得不到。 你去准备一个本子,...
  • 在我的系统中,使用了六个高度理论作为核心,下面为了让大家更的了解的理解六个高度,引用《小强升职日记》中的相关描述。六个高度理论David Allen 提出我们的工作和人生是可以划分成六个高度去进行检视和规划的,...
  • 第一次我的blog

    2007-02-14 11:08:00
    中学时代,有写日记的习惯,那时候还没有internet,时过境迁,互联网已成为生活的一部分,很难想象,失去互联网,世界将会怎样? 经常登陆CSDN,只是收录“我的网摘”,全是拿来主义。前2天看了郭安定老师的《35 岁...
  • 第一篇博客是怎样

    2010-02-18 23:40:00
    就当作写日记吧 记录发生的事情 有一天 ,木然回首 或许是一笔积累的财富。 记录喜怒哀乐,记录人生的过程     从今天开始吧 开的个好头  今天初5 ,去的叔叔家 。本来是不想去的 结果受到的压迫 。 ╮(╯▽╰)...
  • 导读: 接下来的几天,发生了很多事,小女孩吉娅因此见到了小狗钱钱的原主人,金先生,一位富豪,然后.........吉娅涨红了脸,梦想照片还没找到,梦想储蓄罐也还没准备,昨天的成功日记也没有。 吉娅试图辩解,
  •  做真正 Hacker的乐趣──自己动手去实践 2004年我听编辑说有个年轻人了本《自己动手操作系统》,第一反应是不可能,恐怕是翻译稿,这种书籍是要考作者硬功夫的,不但需要深入掌握操作系统的原理,还需要实际...
  •  做真正 Hacker的乐趣──自己动手去实践 2004年我听编辑说有个年轻人了本《自己动手操作系统》,第一反应是不可能,恐怕是翻译稿,这种书籍是要考作者硬功夫的,不但需要深入掌握操作系统的原理,还需要实际...
  • 一切,还只是开始!

    2013-08-04 21:06:37
    没有写日记的习惯,所以闲时留下点东西,等以后的哪天翻出来看看就能知道当时经历了些什么又有着怎样的心情,不坏。  时间长了脚,越走越快,两年大学生活就这么不明不白的走了过来,这半年,貌似脾气不很友好,也...
  • 程序员快乐的一天!

    2021-04-15 14:45:07
    我叫大明,是一名程序员。 90 后,在一家软件开发公司工作,我喜欢打篮球和游戏,但我更爱学习和工作,虽然学习和工作有时并不爱我… 尽管不想承认,但这就是我。 最近,不知道是因为春天柳絮的飘荡...点什么呢?
  • 心随绪飘

    2006-11-03 21:36:00
    写写日记,但坐在电脑前面看着屏幕听着歌曲,感觉自己。。。唉,怎么说呢?真的不知道该说些什么,只想好好的去感受音乐,感受一颗很伤感的心。。。唉,心里很难受,不知怎样用言语去表达,无语。。。好久没有到...
  • 第40周六

    2014-10-04 19:54:00
    等着做好吃晚饭,此刻感觉跟以前上班回来差不多,虽然十一长假已过去一半多,今天上午主要在家等收快递整理东西加洗衣服,下午睡会儿后去西湖转了一圈,感觉还,晚上打算收拾完吃饭后写日记学习总结下,考虑怎样让...
  • 寸心观天下

    2010-06-03 16:28:00
    我还是比较喜欢没事的时候码码文字的,以前是在日记本上,后来与时俱进,就是敲键盘了。自大学以来,不算我在自己电脑上的一些东西,我在网上换过不少地方,最开始是在搜狐博客,断断续续的了两年多,后来觉得...
  • Git

    2016-08-13 14:11:24
    但我觉得无论怎样都要抽时间来自己的博客,这样可以避免一些问题:比如说很多技术可能在某段时间内记忆很清晰但时间长了容易忘记,还有就是记录一些已掌握的技术日记有利于今后项目的开发效率。这样不仅方便自己,...
  • 草样年华

    2012-02-03 15:01:35
    好久没日志,也好久没写日记了。突然对文字变的很生疏,不知道该怎样表达自己的情绪。最近情绪有点低落,总要逃避和退让。。。即使这样的恐惧来自退让,但是。。。。我想有那么一次任性的冲动。 因为清明节的...
  • 我的$OI$

    2018-05-28 19:52:00
    我只是懒得写日记啦\(\color{pink}{qwq}\) //11月8日 啊……终于要\(NOIp\)了,为此期盼了好久、紧张了好久的我,不知道会迎来怎样的结果。 我只知道这段回忆是值得保留封存的。我每次总是计划的很,但是却总是...
  • 心情

    2010-12-06 20:17:00
    好久没些什么了 不是没有什么可 而是不知该怎样下笔 连日记也中断了 好像习惯了将事沉没心底 或不放于心 或自我隐瞒 。。。。。 我常的一句话是:我不了解自己...
  • (作者语) 自从我们的第一个正式电脑游戏...由于这篇文章是和游戏的制作同步进行的,所以看上去可能会有一种读日记的感觉,不过也许只有这样的感觉,才是最真实的! 了,就让我们一步步的去揭开RPG的神秘面纱吧。
  • 它的最新版本DWR0.6添加许多特性如:支持Dom Trees的自动配置,支持Spring(JavaScript远程调用spring bean),更浏览器支持,还支持一个可选的commons-logging日记操作. AJAX 有哪些优秀开源框架 •DOJO •Dojo是一个用...
  • 4.我的电子日记:能够插入音乐和附件的多媒体日记,让您爱不释手。 5.电子课程表:从中学到大学的课程表,以及教课老师名单,全部保存下来。 6.学生成绩管理:记录从中学到大学的考试成绩,以及全部学生名单,...
  • asp.net知识库

    2015-06-18 08:45:45
    忽略大小Replace效率瓶颈IndexOf 随机排列算法 理解C#中的委托[翻译] 利用委托机制处理.NET中的异常 与正则表达式相关的几个小工具 你真的了解.NET中的String吗? .NET中的方法及其调用(一) 如何判断ArrayList,...

空空如也

空空如也

1 2 3
收藏数 50
精华内容 20
关键字:

怎样写好日记