├── README.md ├── extra └── lambda-notes.txt ├── program ├── prog-02.hs ├── prog-03.hs ├── prog-04.hs ├── prog-05.hs ├── prog-06.hs ├── prog-07.hs └── prog-08.hs └── tutorial └── zh_cn ├── 00-preface.md ├── 01-lambda-expression.md ├── 02-data-type.md ├── 03-induction.md ├── 04-parser.md ├── 05-parser-combinator.md ├── 06-lambda-parser.md ├── 07-alpha-conversion.md ├── 08-beta-reduction.md ├── Makefile └── tutorial.css /README.md: -------------------------------------------------------------------------------- 1 | lambda-viewer 2 | ============= 3 | 4 | "Learn You a Lambda, a Haskell Tutorial" is a series that shows you how to build an online [Lambda Viewer], step by step. It is meant to be a supplement material to beginners who are already learning Haskell and/or Lambda Calculus from other sources. 5 | 6 | Each chapter will be written in both Chinese and English, completed with exercises and sample programs. 7 | 8 | The Chinese chapters have been posted to [HaskellCN Forum] as they are written, and discussions usually go there. 9 | 10 | English translation is planned, but not yet written. 11 | 12 | **手把手教你做λ** 13 | 14 | * [引言](http://a.haskellcn.org/topic/4f9b9d42edefd68d37008f34) 15 | * [(一)关于λ表达式的一切](http://a.haskellcn.org/topic/4f9e4037edefd68d37010c8b) 16 | * [(二)数据类型的结构与解构](http://a.haskellcn.org/topic/4fa39c63edefd68d3701fc0e) 17 | * [(三)漂亮打印与数学归纳法](http://a.haskellcn.org/topic/4fae1f29c34eab3e0101b497) 18 | * [(四)语法识别器、高阶函数和列表推导](http://a.haskellcn.org/topic/501db01e68995699130648d5) 19 | * [(五)语法识别器的拼接与组合](http://a.haskellcn.org/topic/504793ecc19c6ba851069186) 20 | * [(六)如何正确识别λ表达式](http://a.haskellcn.org/topic/51d4a2a198942416ea00000a) 21 | * [(七)从形式语义到等价关系](http://a.haskellcn.org/topic/55ca88129894244992000009) 22 | * [(八)替换与归约](http://a.haskellcn.org/topic/5854e3a59894241c11000000) 23 | 24 | [Lambda Viewer]: http://projectultimatum.org/cgi-bin/lambda 25 | [HaskellCN Forum]: http://a.haskellcn.org 26 | 27 | -------------------------------------------------------------------------------- /extra/lambda-notes.txt: -------------------------------------------------------------------------------- 1 | Notes on Lambda Calculus 2 | ======== 3 | 4 | (collected from Weibo posts by @九瓜) 5 | 6 | 我们在刚接触陌生的知识时,尤其是抽象点的,往往有畏难的情绪。这就对了,因为抽象思考从来都是不容易的,所以我们还需要有动力来推动自己。现在我告诉你,λ演算很有趣。如果仅此一条就能够成为你的动力,那么欢迎来到#λ讲堂#!无需任何编程经验,甚至数学也只要一点点。 7 | 8 | #λ讲堂# (1) λ演算简单讲就是关于λ表达式 (λ expression) 的一套演算规则。λ表达式经常也被叫做λ项 (λ term),只有三种形态,变元/抽象/应用。而演算规则也不多,主要就两条。我们只要进一步理解这些定义和规则,很快就能体会到三生万物的玄妙了。 9 | 10 | 因为是一些抽象的东西,我们不妨先把遇到的名词从约定俗成的概念里面剥出来,仅仅把它们当作是单个的词,然后赋予相应的定义。但凡没有给出定义的,一律不要被动接受,要来质问我! 11 | 12 | #λ讲堂# (2) 先讲什么是λ项(λ term)。1. 变元是λ项,通常用小写字母写出来,比如 x,y,z 等; 2. 如果 M 是λ项,那么 λx.M 也是;3. 如果 M 和 N 都是λ项,那么 M N 也是。 13 | 14 | 变元 (variable) 形态的λ项很简单,就是 x 或者 y 或者 z 或者什么别的,我们通常用单个小写字母,有时候也加上数字脚标,比如 x0, x1, x2 等,以示区分。 15 | 16 | 抽象 (abstraction) 形态的λ项,写出来就是先用λ这个符号本身作开头,后面跟一个变元,然后一个小点,然后跟一个任意的λ项。例如 λx.x 或 λx.y 或 λx.x x 等。 17 | 18 | 应用 (application) 形态的λ项,就是两个λ项写在一起,中间仅留一个空格做分隔。例如 f x 或者 x x。写在前面的λ项通常叫函数(function),后面的叫参数(argument)。比如在 x x 这个表达式里,就是把 x 这个函数作用于(apply to)它自己。 19 | 20 | #λ讲堂# (3) 在实际书写抽象和应用两种λ项时,如果没有一定的标识,往往会产生歧义。所以通常是用括号来把一个λ项和它前后的符号区分开,比如 (λx.x) y 这个表达式,就是说函数 λx.x 作用在参数 y 上。我们通常不认为括号本身是λ项的一部分,使用它们纯粹是为了书写。 21 | 22 | 括号的使用有时候也可以略去。约定俗成的习惯是在表达抽象时,小点后面整个式子(除非是遇到反括号)都是与小点前面的变元对应的λ项。比如 λx.(λy.(x y)) 就可以简写为 λx.λy.x y;在表达应用时则向左结合,比如 f x y 应该被理解为 (f x) y 而不是 f (x y)。 23 | 24 | #λ讲堂# (4) 以下哪些是正确的λ项?[1] a b c d e f [2] (g h) (i j) [3] λx.(x+1) [4] λx.f (λy.f y) [5] λ(x y).y [6] λ(λx.x).y [7] (λf.f x y) (λx.λy.x) [8] λx.y.x [9] f g λx.y z 25 | 26 | #λ讲堂# (5) 可能如果我们不谈到函数和参数这种广义名称,会更容易理解一些。所谓变元(variable),无非就是一些彼此不一样的个体,是最基本的无结构的λ项。而抽象(abstraction)和应用(application)则给出了两种不同形态的结构,可以通过简单的λ项来组建复杂的λ项。 27 | 28 | #λ讲堂# (6) 把以下的λ项在恰当的地方加上括号来帮助理清它们的结构(不要加太多)。比如我们可以把 λx.λy.x y 写成 λx.(λy.(x y)),两者结构一样,后者看起来更清晰。[1] a b c d e f [2] λx.λy.λz.z y x [3] f g λx.y z [4] λx.x λy.y λz.z 29 | 30 | #λ讲堂# (7) 对λ項的结构还有疑问的,可以去 http://sinaurl.cn/hbrYEG 看看,可以把任何λ項转成一个树状图片显示出里面的结构。 31 | 32 | #λ讲堂# (8) 题外话。我们中学都学过自然归纳法,就是先证明n=0的时候成立,然后假设n=k的时候成立去求证当n=k+1的情况。这和λ项的定义有什么相似之处吗? 33 | 34 | #λ讲堂# (9) 现在我们已经知道λ项的定义了,很形而下。但是仅仅知道了定义的我们显然不能够满意,我们还要问:λ项到底表示的是个什么意思?在解答这个问题之前,我们不妨先思考一下,什么是“意思”?这个形而上的问题,请大家自由讨论! 35 | 36 | When we ask what is the meaning of something, what do we really mean by "meaning"? 37 | 38 | 穷爸爸富爸爸大家都看过吧?里面有一句话 intelligence is the ability to make finer distinctions. 这种能力是否就是智力姑且不论,但是当我们说 A 和 B 不一样的时候,至少代表我们对它们所代表的东西有所理解,比一概不知要进一步。 39 | 40 | #λ讲堂# (10) 回到λ项上面来,尽管我们还无从理解它的意思,但至少可以试着判断一下两个λ项是否相等。一个最便宜的办法就是,如果写出来一样那就相等;如果写出来不一样那就不相等,也即 lexical equivalence。但是作为人类,我们是否应该有更高的追求... 41 | 42 | #λ讲堂# (11) 这里就要讲到两个基本的公理(axiom),它们是判定λ项是否相等的依据。只有准确无误地理解这两个公理,才能够深入体会λ项的那魔法般的表达能力。第一个公理叫α变换,是针对抽象结构(abstraction)的,不过我们要先学习一下什么叫绑定和自由变元。 43 | 44 | #λ讲堂# (12) 在 λx.M 中,紧跟λ符号之后的x,叫形式参数(formal parameter)简称形参(parameter)。所有出现在M里的x,都被当前这个λ的形参x所绑定,除非还有内层的λ抽象也绑定了x。举例:λx.(x (λy.(x (λx.x)))) 里面,只有第2和第3个x是被第1个x(形参)绑定。 45 | 46 | 绑定的概念也可以换一种说法:一个变元只被当前或外层离它最近的同名形参所绑定。所以正确理解λ项的层次结构是重要的基础功课,参见#λ讲堂#第3到第7课。 47 | 48 | #λ讲堂# (13) 判断以下λ项中,从左往右第一个抽象里的形参都绑定了哪些变元?请用引号标出,比如 (λx.y "x") x。[1] (λf.λx.f x) f x [2] (λx.x) λy.x λx.y x [3] λx.x λx.(λx.x) x [4] (λx.y z λz.y x z) x y z 49 | 50 | #λ讲堂# (14) 一个λ项中没有被任何形参绑定的变元就是自由(free)的。判断以下λ项中,哪些变元是自由的,用引号标出,比如 (λx."y" x) "x"。[1] (λf.λx.f x) f x [2] (λx.x) λy.x λx.y x [3] λx.x λx.(λx.x) x [4] (λx.y z λz.y x z) x y z 51 | 52 | #λ讲堂# (15) α变换(α-conversion)是对λ项进行如下操作:把某个形参以及所有它绑定的变元全都替换成一个新的变元。比如 (λx.y x) x 就可以通过α变换得到 (λz.y z) x,这个变换的过程简写为 (λx.y x) => (λz. y z) x,其中 => 代表做一次α变换。 53 | 54 | 如果我们把λ项和数学函数非正式地联系一下,λ的抽象相当于构建了一个函数,比如 λx.x 相当于定义了 f(x)=x 这个等值函数,而α变换则可以把前述 f 的定义改写成 f(y)=y 55 | 56 | λ项不仅可以用来构建函数(function introduction),还可以用来解构函数(function elimination),接下来我们就会讲到。 57 | 58 | 广义的λ项允许加入数学运算符,即便如此,也并非所有数学函数都可以用这种方式表达出来。这涉及到哥德尔不完备定理,以后会讲到。 59 | 60 | #λ讲堂# (16) 公理一:α变换不改变λ项代表的意思,这也叫做α等价(α-equivalence)。判断以下的λ项是否为α等价:[1] λx.λx.x 和 λy.λx.y [2] λf.λx.f (λy.f x) 和 λf.λy.f (λy.f x) [2] λx.λy.x y 和 λy.λx.y x [3] λx.x λx.(λx.x) x 和 λx.x λy.(λx.x) y 61 | 62 | 此题换一句话说,就是是否存在一系列的α变换的操作,能够把前一个λ项改写成后一个。不需要严格的证明。 63 | 64 | 公理一还可以引申为一个略为直观的概念:λ项中的形参的命名不重要,重要的是它的绑定关系。当然这不是一个严格的叙述,只是帮助理解。 65 | 66 | #λ讲堂# (17) 在上述α变换的定义中,我们注意到有一个条件就是要替换为“新”(也即原λ项中没有出现过)的变元,实际上这个条件其实过于严格(充分非必要),什么才是一个恰到好处的(充要)条件呢?请大家自由发言! 67 | 68 | 这是我的解答,假设我们用 y 替换 λx.M 中形参 x 以及被 x 绑定的变元,首先 y 不能是M中已有的自由变元,其次,用于替换的 y 69 | 不能被M内部的形参绑定。 70 | 71 | #λ讲堂# (18) 如果我们通过使用零次或多次α变换,从一个λ项 M 得到另一个λ项 N(简写为 M =>* N,这里用*表示零次或多次),我们可以反过来从 N 通过α变换得到 M 吗?(也即 N =>* M 成立吗?) 72 | 73 | 正如我们目光如炬的TA在多达两天以前指出,所谓等价关系(用=表示),就是满足三个条件,自反reflexive(M=M),对称symmetric(可以由M=N推导出N=M),传递transitive(可以由L=M和M=N推导出L=N)。这里的问题就是问如何证明 =>* 的对称性。 74 | 75 | 这里我们再次面对什么是公理。如果我们承认关于等价关系的定义为公理,那么“α变换描述的是一个等价关系”这个命题是可以被证明的,则不再需要被划定为一个公理。 76 | 77 | #λ讲堂# (19) β归约(β-reduction)是把结构为 (λx.M) N 的λ项(应用中的函数一方正好是一个抽象结构)化简为 M,并将其中被形参 x 绑定的变元都替换为 N。我们把β归约也用 => 表示,比如 (λf.f x) g => g x 78 | 79 | 如果我们再次把λ项和数学函数非正式地联系一下,β归约有点类似于函数的求值,而替换变元的过程,类似于把变量的值代入到数学表达式里面去。 80 | 81 | 往后如无特殊说明,=>就代表β归约。之所以用同一个 => 符号,主要是因为纯粹的α变换比较少用到,而且β归约过程中也要用到α变换。 82 | 83 | #λ讲堂# (20) 在不改变原有绑定关系的原则下, 前述β归约的步骤中,正确的替换规则是什么?或者先问一个简单一点的问题:有什么情况是要避免发生的吗? 84 | 85 | 我们还可以注意到,α变换仅仅是把抽象中的的形参和它绑定的变元换成全新的变元,而抽象结构本身并未改变。β归约则不同,归约的结果是把原λ项中的一个抽象结构和一个应用结构消除了。凡是满足β归约条件的地方,我们把它们叫 86 | 87 | #λ讲堂# (21) 一个λ项中符合 (λx.M) N 这种结构的地方,叫做β可规约式(β-redex)。以下λ项各有多少个 β-redex? [1] f (λx.x y) [2] λy.(λf.x x) y [3] (λx.x x)(λx.x x) [4] (λx.(λy.λz.z y) x) p (λx.x) [5] (λf.λx.f (f x)) (λf.λx.f (f x)) f x 88 | 89 | #λ讲堂# (22) 对一个λ项 M 进行零或多次β归约得到 N,可以简写为 M =>* N。如果 N 不含任何 β-redex,我们成之为β范式(β-normal form)。前一课里面的λ项 http://t.sina.com.cn/1684815495/wr4kqew5UA 能规约成范式吗?如果能,请写出规约过程。 90 | 91 | #λ讲堂# (23) 公理二:如果 M =>* L 且 N =>* L,那么 M 和 N 等价,也叫做β等价(β-equivalence)。注意这个等价关系并非是说 =>* 本身,因为 M =>* N 的反方向(也即 N =>* M )往往是不成立的。 92 | 93 | 公理二满足传递律(transitivity)吗?也即:是否能够通过M和N满足β等价,N和L满足β等价,证得M和L满足β等价。 94 | 95 | 更直白的说法:能否通过条件(1)存在一个A,M =>* A, N =>* A 和条件(2)存在一个B,N =>* B , L =>* B,来证明结论:存在一个C,M =>* C 且 L =>* C。 96 | 97 | #λ讲堂# (24) 在前面做过的习题里面,β归约往往能够化繁为简达到范式。不过我们也见识过β归约停不下来的例子,比如 (λx.x x)(λx.x x)。其实它还不错哦,至少可以保持身材不变。请举一反三,试着给出一个越减越肥的例子吧! 98 | 99 | 在一个λ项中可能有多个β-redex,你要先归约哪一个呢?在这个多层的盗梦空间里,是否必须要选对了才能够停下来(达到范式),而选错了就得随着那个陀螺转下去(一直停不下来)? 100 | 101 | #λ讲堂# (25) 简单回顾一下,我们从λ项的结构与写法开始,到现在一共学习对它的两种操作(α变换和β归约),以及两种等价关系。基础知识差不多就讲完了!恭喜你坚持到了这里!给自己[鼓掌]!有坚实理论做基础,有趣的事情马上登场了! 102 | 103 | #λ讲堂# (26) 有一个叫丘齐的人设计了一个游戏,他一口气写了一串λ项(我们不妨称之为 N0, N1, N2, ...),超牛!因为 N0 f x =>* x,N1 f x =>* f x,N2 f x =>* f (f x),N3 f x =>* f (f (f x))!!你知道它们都是什么吗? 104 | 105 | #λ讲堂# (27) 好吧,能够设计出 N0,N1,N2 这些还不是很牛,那么丘长老接下来又说了:“谁说自然数要写成 0 1 2 3,那些都是死符号,我的这些λ项才是自然数!因为它们是活的!” 言下之意是归约 N f x 可以变出来 N 个 f 作用在 x 上。 106 | 107 | #λ讲堂# (28) 先把死活的问题放一边,我们假设自然数是丘长老发明的,那么给定一个 N,怎么表示 N+1 呢?也即,如何设计一个λ项 SUCC,使得归约 (SUCC N) f x 得到的范式里的 f 比归约 N f x 得到要多一个。 108 | 109 | #λ讲堂# (29) 对λ項的β归约有疑问的,可以去 http://sinaurl.cn/hbrYEG 看看,不但可以把任何λ項转成一个树状图片显示结构,还可以用鼠标点蓝字,选择不同的redex进行β归约,而且在β归约前也会作适当的α变换。赶快把 (λf.λx.f (f x)) (λf.λx.f (f x)) f x 输进去看看! 110 | 111 | #λ讲堂# (30) OK,加一的问题似乎也不是很牛,两岁的娃娃都会做。那么怎么设计一个更灵活的加法呢?也即,如何设计一个λ项 PLUS,使得归约 (PLUS M N) f x 得到的范式里面 f 的数量是归约 M f x 和 N f x 所能得到的总和!哇卡卡!! 112 | 113 | #λ讲堂# (31) 如果你没有偷偷去查维基百科就设计出来了PLUS,那你牛的!不过丘长老还是更牛一点,因为他还能做乘法!设计一个λ项 MULT,使归约 (MULT M N) f x 得到的范式里 f 的数量是归约 M f x 和 N f x 所得 f 数量的乘积。 114 | 115 | #λ讲堂# (32) 把 (λm.λn.m n) 作用于任意两个用上述λ项表达的自然数 M 和 N,能得到什么?能解释一下为什么吗? 116 | 117 | #λ讲堂# (33) 减一的问题:设计一个λ项 PRED,使得 (PRED N) f x 归约得到的 f 比 N f x 要少一个。维基百科给的解答是 λn.λf.λx.n (λg.λh.h (g f)) (λu.x) (λu.u),建议动手试试看哦。谁来猜猜它的设计思想? 118 | 119 | 既然@兔山白 已经提前警告大家减法超级难,我就直接给答案好了。 120 | 121 | #λ讲堂# (34) 减法难就难在我们无法把一个已知的应用结构拆开来,只能通过构造函数的方法来做。所以说丘长老的牛还不是我们可以比的,因为就是他,Alonzo Church,发明了λ演算。前述表达自然数的方法,就叫作邱奇数(church numerals)。 122 | 123 | #λ讲堂# (35) 到这里,我们的邱奇数之旅就告一段落了。在数学上,如果一套系统能够表达自然数及其运算,基本上就可以表达大部分运算了。最后再来一题,(λf.λx.(λg.λx.f (g g x)) (λg.λx.f (g g x)) x) 表达的是 church numeral 里面哪个数? 124 | 125 | #λ讲堂# (36) 在第 23 课里我们曾经试图证明β等价满足传递律,@浮生十慵 石破天惊地提出了一个假设“换言之,如果N=>*A, N=>*B,那么存在一个C, A=>*C, B=>*C”,这个假设岂止是成立,简直是非常成立!它还有个名字,叫 Church-Rosser 定理。 126 | 127 | 具体证明方法就比较复杂,也不是掌握λ演算的重点。Rosser 是 Church 的学生,所以我估计脏活是他干的。顺带再说一句,Turing 也是 Church 的学生,在老师给他审稿(就是图灵机那篇)的时候,证明了图灵机和λ演算可以相互表达。 128 | 129 | 我们又一次面对什么是公理的问题。如果我们承认关于等价关系的定义(满足自反/对称/传递)为公理,那么公理一和公理二都可以被证明满足这个等价关系,从而只是定理。 130 | 131 | #λ讲堂# (37) 从 Church-Rosser 定理还可以推出一个结论,那就是只要一个λ項存在范式的话,无论你先归约它的哪一个 redex,最后总有一条路能够到达范式。在小径分叉的花园,选错一次不要紧,重要的是不要每次都选错。 132 | 133 | #λ讲堂# (38) 说到这个小径分叉的花园,当然还有一个大前提,就是要确认存在范式先!不然只有杯具没有钟点... 那么,怎么判断一个λ項最终可以归约到范式呢?请自由讨论! 134 | 135 | #λ讲堂# (39) 明察秋毫的@兔山白 多次迫切地要求使用“λx.f x => f”,这也有个名字,叫做η变换(η-conversion,η音eta)。它来自一个观察:对于任意λ项 M,N,若x不是M的自由变元,则 (λx.M x) N => M N。那么问题是 (λx.M x) 和 M 等价吗? 136 | 137 | #λ讲堂# (40) 外延性 (extensionality) 等价的定义:当且仅当 f x = g x 对所有的 x 都成立时,f 和 g 等价。所以η变换属于外延性等价,这个和我们通常的等价定义不一样。只有当我们承认外延性等价这个前提时,η变换才是等价的。 138 | 139 | #λ讲堂# (41) 回来我们的花园:有没有一种归约顺序,只要存在范式,就总是可以到达范式呢?答案是:每次选择最左边最外层的 redex 就行!这种方法叫做正序(normal order)归约。假如 M =>* N,且 N 是范式,那么存在一个正序归约的步骤把 M 归约到 N。这也叫 Church-Rosser 第二定理。 140 | 141 | #λ讲堂# (42) 正序归约表面上看是个很奇怪的顺序,因为通常在数学计算里面,我们总是先把函数的参数算出来,才代入函数式进行计算。这样的次序也有个名字,叫做应序(applicative order)归约,也即每次选择最左最内的 redex 进行归约。 142 | 143 | #λ讲堂# (43) 以下λ项是否存在范式?如果存在,正序和应序各需要几次β归约(α和η变换不计在内)? [1] (λx.λy.y) ((λx.x x) (λx.x x)) [2] (λf.f (f x)) ((λx.λy.x) (λx.y)) [3] (λf.λx.f (f x)) (λf.λx.f (f x)) (λx.x) (λx.x) 144 | 145 | #λ讲堂# (44) 在小径分叉的花园里,假设存在终点(也即范式),要怎么选择才是最优的呢(也即β归约的次数最少)?是正序吗?是应序吗?举几个例子吧! 146 | 147 | #λ讲堂# (45) 经过长达两天的休整,我们迎来了新的一课!在不偷看维基百科和狗狗的前提下,请设计以下四个λ项,T,F,AND,OR,从而使得 AND T T =>* T,AND T F =>* F, AND F T =>* F, AND F F => F, OR T T => T, OR T F =>* T, OR F T =>* T, OR F F =>* F! 148 | 149 | #λ讲堂# (46) 偷偷告诉大家一个我的最新成果。我设计了一个λ项,因为太复杂,姑且叫它 NORMORNOT,可以用来判断任意一个λ项是否有范式,当且仅当 X 能够归约到范式时,NORMORNOT X =>* T;否则 NORMORNOT X =>* F。这可能是公元纪年以来 4 月 1 日这一天最伟大的发现哦! 150 | 151 | #λ讲堂# (47) 当当当,万众期待的不动点(fixed point)今日登场!在数学上如果 x = f(x),那么 x 就是函数 f 的不动点。提问 [1] 数学函数 f(x)=x * x 的不动点是什么?[2] 给定一个λ项 λf.(λx.f (x x)) (λx.f (x x)),通常简称 Y,那么对任意一个λ项 F,归约 Y F 会得到什么? 152 | 153 | #λ讲堂# (48) 更新了一下 Lambda Viewer http://sinaurl.cn/hbrYEG,现在可以显示 redex 的层次。有兴趣的同学可以重新试一下第 43 和 44 课,记住别把α变换算在内就好了。 154 | 155 | #λ讲堂# (49) 假设我们采用@兔山白 对45课的解答,定义 T = λf.λx.x 和 F = λf.λx.f x,那么在46课的基础上我又设计了一个 FAN = λm.(NORMORNOT m) (λx.y) ((λx.x x) (λx.x x)),意思就是如果 M 有范式,那 FAN M 就没有;如果 M 没有,FAN M 就有。请问是这样的吗? 156 | 157 | #λ讲堂# (50) 在上一课之后,我突然发现有一个问题很纠结。根据第47课对 Y 的定义,归约 Y FAN 会得到范式吗?有木有很纠结?有木有?有木有!!! 158 | 159 | #λ讲堂# (51) 第五十课其实就是歌德尔不完备定理(Gödel's incompleteness theorem)的一个例子。如果我们认为λ演算可以表达所有的函数(包括 NORMORNOT),那么就会产生悖论;如果我们不允许悖论存在,那么就有一些函数是无法写成λ项的,比如 NORMORNOT。 160 | 161 | #λ讲堂# (52) 现在我们知道,虽然λ演算很强大,但是有些事情它还是无法做到的,比如用来判断一个λ项是否存在范式。λ演算到底有多强大呢?用它可以完成所有的被称之为计算或着算法的事情,就像一个理想(无限内存)的计算机。这就是 Church-Turing Thesis。 162 | 163 | #λ讲堂# (53) Church-Turing Thesis 之所以叫做 thesis(命题),不叫 theorem(定理),是因为它是无法被证明的,它是个循环定义(tautology):但凡能够用λ演算或者图灵机(理想计算机)计算或者表达出来的,就是可以计算(computable)或者能够称之为算法的。 164 | 165 | #λ讲堂# (54) 课程到这里暂停一下吧,在进入下一阶段 type theory 之前,我们回顾一下前面的未完成的课程:35课还没有结论;38课已经被证否了;44课大家都没做对;45课还没有给出常规答案;47课还有个奇特处没被发现。我都追加了注释,欢迎大家温故而知新! 166 | 167 | 168 | 烂苹果乐园 by @九瓜 169 | 170 | 愚人节你别徘徊/快到#λ讲堂#来/欢迎织博的小孩/不要只是点转发/要咆哮体呼喊/向枯燥理论说拜拜/抽象!应用!/样样都简单/α!β!/奇妙的魔法/../告诉What's Your Term/接受这归约吧/Normalize You/走进不停的循环/Choose Your Way/范式就在前方/小新啊/字数能多一点嘛! 171 | 172 | 烂苹果与八卦 by @兔山白 173 | 174 | 第一日 天上像是出了两个太阳。熙凤对湘云说,真是热死人了。湘云说,你说这是不是传说中的热寂啊。熙凤笑了,你又发痴了,是热坏了脑袋还是看BBT(Big Bang)看多了?又说,镇里来了个姓邱的老道,卖烂苹果,不怕315啊?哦对了,明天我要出差了。湘云自言自语的说,那一定是个不平凡的人。 175 | 176 | 第二日 地倾东南,人心惶惶。狗头儿撞倒了白兔山。邱七创作了烂苹果之歌。故人来访,邱七相约同往。故人笑曰:关于那个.,我已经找到了。临别赠一物,又说,等你参透了,我就同你唱这首歌。 177 | 178 | 第三日 惊蛰。来了十个不平凡的少年,号称拯救地球大作战使命团。停电了,邱七拿出一面八卦镜,移烛相照,从乾位移到了坤位。贾君鹏大惊:乾坤大挪移?邱七不答,又从坤位挪到了震位。湘云也报了名。回来的路上,看到了一条蛇。大家开始训练,师傅说,所谓抽象,就是在25g的情况下的灵魂出窍。悟空问?25克?无限说,gravity,stupid。牛三金是南方人,被25g累得气喘吁吁,为什么这么烂(难)。 179 | 180 | 第四日 风向东南,辐射无忧。贾君鹏回家吃饭。牛三金回家过年。。。。剩下邱七,悟空,无限(AKA小林,麒麟,kylin),湘云四人。挂帆东南,目标白兔山。 181 | 182 | 第五日 海水是那么的平静,那么的蓝。 183 | 184 | 第六日 白兔山到了,无限喊道。众人一看,根本没有什么山,只有一幡,上书白兔山三个大字,中国移动我也不动的垂在那里。悟空丧气的说:这就是传说中的白兔山,不给力啊,师傅。半晌,无限又喊,动了,动了,幡动了。邱七呵斥道:什么动不动,你不要动不动就说什么动不动。湘云寻思:难道这就是不动点。一行无话,踏上返程。 185 | 186 | 第七日 着火了。又是无限。众人惊惶,师傅在甲板上快烤成了Bak Guah。悟空说,师傅快走。邱七说:我命犯火,尤忌七数。末了又说:如如不动,急急如律 湘云陷入了沉思,她穿过时间的迷宫,看到了生命的终点,抽丝剥茧,眼看找到了答案,却不巧在语言这根筋上断了线。 187 | 188 | 第八日 醒醒,你醒醒 熙凤摇着湘云。二人打开房门,海水已经到了第八楼,地球已成泽国。熙凤说:等等,我去捉条鱼,给你做条正宗的糖醋鱼。湘云远眺,海天一线。不禁发出这样的天问,宇宙是无限的么,生命是有尽吗?熙凤说,那还用问,这个当然。可是那条蛇,湘云说,咬住了自己的尾巴。 189 | 190 | 191 | 解读:邱七/Church,狗头儿/Gödel,无限/Turing,白兔山/悖论,烂苹果/lambda,故人/?,乾/FFF,坤/TTT,震/TTF,蛇/循环(Y),25g/25课,幡/FAN,如如不动/α变换,急急如律/β归约 192 | 193 | 关于熙凤,应该是瓶子,因为没有入梦(上课)。而且曾经取笑过兔山白的糖醋排骨,所以最后安排她做“正宗”的糖醋鱼,哈哈! 194 | 195 | 关于湘云,应该是十慵。因为曾经预见了 Church-Rosser 定理,然后所做歌词里有“语言”二字。 196 | 197 | 关于故人,一说是因为不动点所以应该是 Banach 或者什么其他人;二说因为不完备定理所以应该是 Gödel。历史上的确 Gödel 是先去普林(Church 所在地)给过 talk,Church 和 Turing 的成果都是两年之后。 198 | 199 | 致敬:明明/结构+火,鸟山明/悟空+师傅+25g,黄耀明/十个少年+天问,贝克特/?,红楼梦/熙凤+湘云+楼+梦,博尔赫斯/迷宫,金庸/乾坤大挪移,石康/?,欧阳峰张国荣王家卫/节气天象+明天出差+不动幡 200 | 201 | 关于贝可特我就知道等待戈多,不够了解。还有,为什么是石康?难道不是刘震云或者冯小刚? 202 | -------------------------------------------------------------------------------- /program/prog-02.hs: -------------------------------------------------------------------------------- 1 | -- Program that goes with "Learn You a Lambda, a Haskell Tutorial", Chapter 2. 2 | 3 | -- Datatype definition for lambda expression. 4 | data Term = Var Var | Lam Var Term | App Term Term 5 | data Var = V String 6 | 7 | 8 | -------------------------------------------------------------------------------- /program/prog-03.hs: -------------------------------------------------------------------------------- 1 | -- Program that goes with "Learn You a Lambda, a Haskell Tutorial", Chapter 3. 2 | 3 | -- Datatype definition for lambda expression. 4 | data Term = Var Var | Lam Var Term | App Term Term 5 | data Var = V String 6 | 7 | -- Show instances for Var and Term. 8 | instance Show Var where 9 | show (V s) = s 10 | 11 | instance Show Term where 12 | show (Var v) = show v 13 | show (Lam x e) = "(λ" ++ show x ++ "." ++ show e ++ ")" 14 | show (App f e) = "(" ++ show f ++ " " ++ show e ++ ")" 15 | -------------------------------------------------------------------------------- /program/prog-04.hs: -------------------------------------------------------------------------------- 1 | -- Program that goes with "Learn You a Lambda, a Haskell Tutorial", Chapter 4. 2 | 3 | import Data.Char 4 | 5 | -- Datatype definition for lambda expression. 6 | data Term = Var Var | Lam Var Term | App Term Term 7 | data Var = V String 8 | 9 | -- Show instances for Var and Term. 10 | instance Show Var where 11 | show (V s) = s 12 | 13 | instance Show Term where 14 | show (Var v) = show v 15 | show (Lam x e) = "(λ" ++ show x ++ "." ++ show e ++ ")" 16 | show (App f e) = "(" ++ show f ++ " " ++ show e ++ ")" 17 | 18 | -- Read instance for Var. 19 | instance Read Var where 20 | readsPrec _ = variable 21 | 22 | -- Parser for single character variable. 23 | variable s = map f (char isAlpha s) 24 | where f (c, s1) = (V [c], s1) 25 | 26 | char :: (Char -> Bool) -> ReadS Char 27 | char f (c:s) | f c = [(c, s)] 28 | char f _ = [] 29 | 30 | variable2 s = [ (V [c1,c2], s2) 31 | | (c1, s1) <- char isAlpha s 32 | , (c2, s2) <- char isAlpha s1 ] 33 | -------------------------------------------------------------------------------- /program/prog-05.hs: -------------------------------------------------------------------------------- 1 | -- Program that goes with "Learn You a Lambda, a Haskell Tutorial", Chapter 5. 2 | 3 | import Data.Char 4 | 5 | -- Datatype definition for lambda expression. 6 | data Term = Var Var | Lam Var Term | App Term Term 7 | data Var = V String 8 | 9 | -- Show instances for Var and Term. 10 | instance Show Var where 11 | show (V s) = s 12 | 13 | instance Show Term where 14 | show (Var v) = show v 15 | show (Lam x e) = "(λ" ++ show x ++ "." ++ show e ++ ")" 16 | show (App f e) = "(" ++ show f ++ " " ++ show e ++ ")" 17 | 18 | -- Read instance for Var. 19 | instance Read Var where 20 | readsPrec _ = variable 21 | 22 | -- Parser for variables that start with an alphabet, followed by a number. 23 | variable = mapP f (alpha &&& digits) 24 | where f (c, d) = V (c : d) 25 | alpha = char isAlpha 26 | digits = many1 (char isDigit) 27 | 28 | -- Parser for single character variables. 29 | variable1 = mapP (\c -> V [c]) (char isAlpha) 30 | 31 | -- Basic parser combinators. 32 | nil :: ReadS [a] 33 | nil s = [([], s)] 34 | 35 | char :: (Char -> Bool) -> ReadS Char 36 | char f (c:s) | f c = [(c, s)] 37 | char f _ = [] 38 | 39 | mapP :: (a -> b) -> ReadS a -> ReadS b 40 | mapP f g = map (\ (c, s) -> (f c, s)) . g 41 | 42 | (&&&) :: ReadS a -> ReadS b -> ReadS (a, b) 43 | f &&& g = \s -> [ ((x, y), s2) 44 | | (x, s1) <- f s, 45 | (y, s2) <- g s1 ] 46 | 47 | -- Left biased choice (which is actually not appropriate) 48 | (|||) :: ReadS a -> ReadS b -> ReadS (Either a b) 49 | f ||| g = \s -> case f s of 50 | [] -> map right (g s) 51 | xs -> map left xs 52 | where left (x, s) = (Left x, s) 53 | right (y, s) = (Right y, s) 54 | 55 | (<|>) :: ReadS a -> ReadS a -> ReadS a 56 | f <|> g = mapP select (f ||| g) 57 | where select (Left x) = x 58 | select (Right y) = y 59 | 60 | many, many1 :: ReadS a -> ReadS [a] 61 | many r = many1 r <|> nil 62 | many1 r = mapP cons (r &&& many r) 63 | where cons (x, xs) = x : xs 64 | 65 | 66 | -------------------------------------------------------------------------------- /program/prog-06.hs: -------------------------------------------------------------------------------- 1 | -- Program that goes with "Learn You a Lambda, a Haskell Tutorial", Chapter 6. 2 | -- 3 | -- We use the QuickCheck library to help check program correctness. 4 | -- E.g. "quickCheck propTerm" will show that all tests pass for the 5 | -- lambda parser given at the end of this file. 6 | -- 7 | -- You can use "quickCheck propSpaced" to test solutions to problem 1, 8 | -- or "quickCheck propPretty" to test solutions to problem 2. 9 | 10 | import Data.Char 11 | import Test.QuickCheck 12 | import Control.Applicative hiding (many, (<|>)) 13 | 14 | -- Datatype definition for lambda expression. 15 | data Term = Var Var | Lam Var Term | App Term Term deriving Eq 16 | data Var = V String deriving Eq 17 | 18 | -- Show instances for Var and Term. 19 | instance Show Var where 20 | show (V s) = s 21 | 22 | instance Show Term where 23 | show (Var v) = show v 24 | show (Lam x e) = "(λ" ++ show x ++ "." ++ show e ++ ")" 25 | show (App f e) = "(" ++ show f ++ " " ++ show e ++ ")" 26 | 27 | -- Pretty-print that minimizes the number of parentheses. 28 | -- (solution to problem 2 in tutorial 3) 29 | pretty = snd . fold i g h 30 | where 31 | i (V v) = (either id id, v) 32 | g (V v) (_,e) = (either pr pr, "λ" ++ v ++ "." ++ e) 33 | h (b,f) (d,e) = (either id pr, b (Left f) ++ " " ++ d (Right e)) 34 | pr s = "(" ++ s ++ ")" 35 | 36 | -- Generic fold on Term, used by pretty. 37 | fold :: (Var -> a) -> (Var -> a -> a) -> (a -> a -> a) -> Term -> a 38 | fold i g h = fold' 39 | where 40 | fold' (Var v) = i v 41 | fold' (Lam v e) = g v (fold' e) 42 | fold' (App f e) = h (fold' f) (fold' e) 43 | 44 | -- Basic parser combinators. 45 | nil :: ReadS [a] 46 | nil s = [([], s)] 47 | 48 | char :: (Char -> Bool) -> ReadS Char 49 | char f (c:s) | f c = [(c, s)] 50 | char f _ = [] 51 | 52 | mapP :: (a -> b) -> ReadS a -> ReadS b 53 | mapP f g = map (\ (c, s) -> (f c, s)) . g 54 | 55 | (&&&) :: ReadS a -> ReadS b -> ReadS (a, b) 56 | f &&& g = \s -> [ ((x, y), s2) 57 | | (x, s1) <- f s, 58 | (y, s2) <- g s1 ] 59 | 60 | (|||) :: ReadS a -> ReadS b -> ReadS (Either a b) 61 | f ||| g = \s -> map left (f s) ++ map right (g s) 62 | where left (x, s) = (Left x, s) 63 | right (y, s) = (Right y, s) 64 | 65 | (<|>) :: ReadS a -> ReadS a -> ReadS a 66 | f <|> g = mapP select (f ||| g) 67 | where select (Left x) = x 68 | select (Right y) = y 69 | 70 | many, many1 :: ReadS a -> ReadS [a] 71 | many r = many1 r <|> nil 72 | many1 r = mapP cons (r &&& many r) 73 | where cons (x, xs) = x : xs 74 | 75 | paren p = mapP f (sym '(' &&& p &&& sym ')') 76 | where f ((_, x), _) = x 77 | 78 | -- Read instance for Var and Term. 79 | instance Read Var where 80 | readsPrec _ = variable 81 | 82 | instance Read Term where 83 | readsPrec _ = term 84 | 85 | -- Randomly generate Var and Term for QuickCheck. 86 | instance Arbitrary Var where 87 | arbitrary = do 88 | f <- choose ('a', 'z') 89 | c <- arbitrary 90 | d <- choose (0::Int, 100) 91 | return $ V $ if c then [f] else f : show d 92 | 93 | instance Arbitrary Term where 94 | arbitrary = sized term 95 | where 96 | term 0 = Var <$> arbitrary 97 | term n = oneof [ Var <$> arbitrary, 98 | Lam <$> arbitrary <*> term (n-1), 99 | App <$> term m <*> term m ] 100 | where m = n `div` 2 101 | 102 | 103 | -- Show, Read, Arbitrary instances for Pretty 104 | data Pretty = Pretty Term deriving Eq 105 | 106 | instance Show Pretty where 107 | show (Pretty x) = pretty x 108 | 109 | instance Read Pretty where 110 | readsPrec = mapP Pretty . readsPrec 111 | 112 | instance Arbitrary Pretty where 113 | arbitrary = fmap Pretty arbitrary 114 | 115 | -- Randomly insert spaces into a string. 116 | data Spaced a = Spaced a [Bool] 117 | 118 | instance Eq a => Eq (Spaced a) where 119 | Spaced x _ == Spaced y _ = x == y 120 | 121 | instance Show a => Show (Spaced a) where 122 | show (Spaced x l) = spaced (show x) l 123 | where spaced (x:xs) (b:bs) | b && notVar x xs = x : spaced (' ' : xs) bs 124 | | otherwise = x : spaced xs bs 125 | spaced xs [] = xs 126 | spaced [] _ = [] 127 | notVar x [] = True 128 | notVar x (y:_) = not (isAlphaNum x && isDigit y) 129 | 130 | instance Read a => Read (Spaced a) where 131 | readsPrec = mapP f . readsPrec 132 | where f x = Spaced x [] 133 | 134 | instance Arbitrary a => Arbitrary (Spaced a) where 135 | arbitrary = Spaced <$> arbitrary <*> arbitrary 136 | 137 | -- To check if the parser can parse all non-ambiguously printed terms. 138 | propTerm :: Term -> Bool 139 | propTerm t = read (show t) == t 140 | 141 | -- To check if the parser can parse all pretty printed terms. 142 | propPretty :: Pretty -> Bool 143 | propPretty t = read (show t) == t 144 | 145 | -- To check if the parser can handle arbitrarily spaced terms. 146 | propSpaced :: Spaced Term -> Bool 147 | propSpaced t = read (show t) == t 148 | 149 | -- To check if the parser can handle arbitrarily spaced pretty terms. 150 | propSpacedPretty :: Spaced Pretty -> Bool 151 | propSpacedPretty t = read (show t) == t 152 | 153 | -- =========================================================== 154 | -- You may modify the parser below to complete the assignment. 155 | -- =========================================================== 156 | 157 | -- Parser for variables that start with lowercase letter, 158 | -- and optionally followed by a number. 159 | variable = mapP f (alpha &&& (digits <|> nil)) 160 | where f (c, d) = V (c : d) 161 | alpha = char (\c -> c >= 'a' && c <= 'z') 162 | digits = many1 (char isDigit) 163 | 164 | -- Parser for lambda terms that is not ambiguous, and not left-recursive. 165 | term, term', atom :: ReadS Term 166 | term = mapP (foldl1 App) (many1 term') 167 | term' = mapP fst (atom &&& (space ||| nil)) 168 | atom = lam <|> var <|> paren term 169 | 170 | var = mapP Var variable 171 | 172 | lam = mapP f (lbd &&& variable &&& sym '.' &&& term) 173 | where f (((_, v), _), e) = Lam v e 174 | 175 | lbd = (sym '\\' <|> sym 'λ') 176 | 177 | sym = char . (==) 178 | 179 | space = many1 (sym ' ') 180 | 181 | -------------------------------------------------------------------------------- /program/prog-07.hs: -------------------------------------------------------------------------------- 1 | -- Program that goes with "Learn You a Lambda, a Haskell Tutorial", Chapter 7. 2 | -- 3 | -- You can base your solution either on this file, or on your previous solution 4 | -- for Chapter 6, assuming you have an improved parser for lambda terms. 5 | 6 | import Data.Char 7 | import Control.Applicative hiding (many, (<|>)) 8 | import Test.QuickCheck 9 | import Data.List (intersect) 10 | 11 | -- Datatype definition for lambda expression. 12 | data Term = Var Var | Lam Var Term | App Term Term deriving Eq 13 | data Var = V String deriving Eq 14 | 15 | -- Show instances for Var and Term. 16 | instance Show Var where 17 | show (V s) = s 18 | 19 | instance Show Term where 20 | show (Var v) = show v 21 | show (Lam x e) = "(λ" ++ show x ++ "." ++ show e ++ ")" 22 | show (App f e) = "(" ++ show f ++ " " ++ show e ++ ")" 23 | 24 | -- Pretty-print that minimizes the number of parentheses. 25 | -- (solution to problem 2 in tutorial 3) 26 | pretty = snd . fold i g h 27 | where 28 | i (V v) = (either id id, v) 29 | g (V v) (_,e) = (either pr pr, "λ" ++ v ++ "." ++ e) 30 | h (b,f) (d,e) = (either id pr, b (Left f) ++ " " ++ d (Right e)) 31 | pr s = "(" ++ s ++ ")" 32 | 33 | -- Generic fold on Term, used by pretty. 34 | fold :: (Var -> a) -> (Var -> a -> a) -> (a -> a -> a) -> Term -> a 35 | fold i g h = fold' 36 | where 37 | fold' (Var v) = i v 38 | fold' (Lam v e) = g v (fold' e) 39 | fold' (App f e) = h (fold' f) (fold' e) 40 | 41 | -- Basic parser combinators. 42 | nil :: ReadS [a] 43 | nil s = [([], s)] 44 | 45 | char :: (Char -> Bool) -> ReadS Char 46 | char f (c:s) | f c = [(c, s)] 47 | char f _ = [] 48 | 49 | mapP :: (a -> b) -> ReadS a -> ReadS b 50 | mapP f g = map (\ (c, s) -> (f c, s)) . g 51 | 52 | (&&&) :: ReadS a -> ReadS b -> ReadS (a, b) 53 | f &&& g = \s -> [ ((x, y), s2) 54 | | (x, s1) <- f s, 55 | (y, s2) <- g s1 ] 56 | 57 | (|||) :: ReadS a -> ReadS b -> ReadS (Either a b) 58 | f ||| g = \s -> map left (f s) ++ map right (g s) 59 | where left (x, s) = (Left x, s) 60 | right (y, s) = (Right y, s) 61 | 62 | (<|>) :: ReadS a -> ReadS a -> ReadS a 63 | f <|> g = mapP select (f ||| g) 64 | where select (Left x) = x 65 | select (Right y) = y 66 | 67 | many, many1 :: ReadS a -> ReadS [a] 68 | many r = many1 r <|> nil 69 | many1 r = mapP cons (r &&& many r) 70 | where cons (x, xs) = x : xs 71 | 72 | paren p = mapP f (sym '(' &&& p &&& sym ')') 73 | where f ((_, x), _) = x 74 | 75 | -- Read instance for Var and Term. 76 | instance Read Var where 77 | readsPrec _ = variable 78 | 79 | instance Read Term where 80 | readsPrec _ = term 81 | 82 | -- Parser for variables that start with lowercase letter, 83 | -- and optionally followed by a number. 84 | variable = mapP f (alpha &&& (digits <|> nil)) 85 | where f (c, d) = V (c : d) 86 | alpha = char (\c -> c >= 'a' && c <= 'z') 87 | digits = many1 (char isDigit) 88 | 89 | -- Parser for lambda terms that is not ambiguous, and not left-recursive. 90 | term, term', atom :: ReadS Term 91 | term = mapP (foldl1 App) (many1 term') 92 | term' = mapP fst (atom &&& (space ||| nil)) 93 | atom = lam <|> var <|> paren term 94 | 95 | var = mapP Var variable 96 | 97 | lam = mapP f (lbd &&& variable &&& sym '.' &&& term) 98 | where f (((_, v), _), e) = Lam v e 99 | 100 | lbd = (sym '\\' <|> sym 'λ') 101 | 102 | sym = char . (==) 103 | 104 | space = many1 (sym ' ') 105 | 106 | -- Randomly generate Var and Term for QuickCheck. 107 | instance Arbitrary Var where 108 | -- use a limited range to increase chances for variable re-use 109 | arbitrary = fmap (V . (:[])) $ choose ('a', 'z') 110 | 111 | instance Arbitrary Term where 112 | arbitrary = sized term 113 | where 114 | term 0 = Var <$> arbitrary 115 | term n = oneof [ Var <$> arbitrary, 116 | Lam <$> arbitrary <*> term (n-1), 117 | App <$> term m <*> term m ] 118 | where m = n `div` 2 119 | 120 | -- Testing for alphaEq's behavior 121 | propAlpha :: Int -> Int -> Term -> Bool 122 | propAlpha i j e = nlam == 0 || (testEq e e1 && testEq e1 e) && 123 | (null fvs || testNEq e1 e2 && testNEq e2 e1 && 124 | testNEq e1 e3 && testNEq e3 e1) && 125 | (null fvs' || testNEq e4 e4' && testNEq e4' e4 && 126 | testEq e5 e5' && testEq e5' e5) 127 | where 128 | testEq e e' = alphaEq e e' || error ("Expect True, but alphaEq (" ++ show e ++ ") (" ++ show e' ++ ") is False") 129 | testNEq e e' = not (alphaEq e e') || error ("Expect False, but alphaEq (" ++ show e ++ ") (" ++ show e' ++ ") is True") 130 | -- count the number of lambdas 131 | nlam = fold (\_ -> 0) (\_ _ -> 1) (+) e 132 | n = abs i `mod` nlam 133 | navigate i f e@(Lam v e') = if i == n then Left (e, f) else navigate (i + 1) (f . Lam v) e' 134 | navigate i f e@(App e1 e2) = 135 | case navigate i (f . flip App e2) e1 of 136 | Right i' -> navigate i' (f . App e1) e2 137 | r -> r 138 | navigate i f _ = Right i 139 | Left (Lam v e', ctx) = navigate 0 id e 140 | bvs = boundVars e' 141 | fvs = filter (/=v) $ freeVars e' 142 | -- a case that is always valid 143 | u1 = head [ u | u <- allVars, u `notElem` bvs, u `notElem` fvs ] 144 | e1 = ctx $ Lam u1 (subV v u1 e') 145 | -- a case that is always invalid 146 | u2 = fvs !! (abs j `mod` length fvs) 147 | e2 = ctx $ Lam u2 (subV v u2 e') 148 | -- a case that is always invalid 149 | u3 = fvs !! (abs j `mod` length fvs) 150 | e3 = ctx $ Lam v (subV u3 v e') 151 | -- a case that is always invalid 152 | fvs' = fvs `intersect` freeVars e 153 | u4 = fvs' !! (abs j `mod` length fvs') 154 | e4 = Lam u4 e 155 | e4' = Lam v $ ctx $ Lam v (subV u4 v e') 156 | -- a case that is always valid 157 | (u5:u6:_) = [ u | u <- allVars, u `notElem` freeVars e, u `notElem` boundVars e, u `notElem` fvs, v `notElem` fvs ] 158 | e5 = Lam u4 e 159 | e5' = Lam u5 $ subV u4 u5 $ ctx $ Lam u6 (subV v u6 e') 160 | 161 | -- An infinite supply of variable names 162 | atoz = ['a' .. 'z'] 163 | allVars = map V (map (:[]) atoz ++ [ v : show m | v <- atoz, m <- [0..]]) 164 | 165 | -- Bounded variable set 166 | boundVars :: Term -> [Var] 167 | boundVars = fold (\_ -> []) (:) (++) 168 | 169 | -- Free variable set 170 | freeVars :: Term -> [Var] 171 | freeVars = aux [] 172 | where 173 | aux env (Var v) | elem v env = [] 174 | | otherwise = [v] 175 | aux env (Lam v e) = aux (v:env) e 176 | aux env (App f e) = aux env f ++ aux env e 177 | 178 | 179 | -- Substitute a free varible by another 180 | subV :: Var -> Var -> Term -> Term 181 | subV u w (Var v) | v == u = Var w 182 | | otherwise = Var v 183 | subV u w (Lam v e) | v == u = Lam v e 184 | | otherwise = Lam v (subV u w e) 185 | subV u w (App f e) = App (subV u w f) 186 | (subV u w e) 187 | 188 | 189 | -- =========================================================== 190 | -- You may modify the function below to complete the assignment. 191 | -- =========================================================== 192 | 193 | alphaEq :: Term -> Term -> Bool 194 | alphaEq e e' = True 195 | 196 | -- You may test the correctness of your implementation with the following: 197 | -- main = quickCheck propAlpha 198 | -------------------------------------------------------------------------------- /program/prog-08.hs: -------------------------------------------------------------------------------- 1 | -- Program that goes with "Learn You a Lambda, a Haskell Tutorial", Chapter 8. 2 | -- 3 | -- You can base your solution either on this file, or on your previous solution 4 | -- for Chapter 7, assuming you have an improved parser for lambda terms. 5 | 6 | import Data.Char 7 | import Control.Applicative hiding (many, (<|>)) 8 | import Test.QuickCheck 9 | import Data.List (intersect) 10 | 11 | -- Datatype definition for lambda expression. 12 | data Term = Var Var | Lam Var Term | App Term Term deriving Eq 13 | data Var = V String deriving Eq 14 | 15 | -- Show instances for Var and Term. 16 | instance Show Var where 17 | show (V s) = s 18 | 19 | instance Show Term where 20 | show (Var v) = show v 21 | show (Lam x e) = "(λ" ++ show x ++ "." ++ show e ++ ")" 22 | show (App f e) = "(" ++ show f ++ " " ++ show e ++ ")" 23 | 24 | -- Pretty-print that minimizes the number of parentheses. 25 | -- (solution to problem 2 in tutorial 3) 26 | pretty = snd . fold i g h 27 | where 28 | i (V v) = (either id id, v) 29 | g (V v) (_,e) = (either pr pr, "λ" ++ v ++ "." ++ e) 30 | h (b,f) (d,e) = (either id pr, b (Left f) ++ " " ++ d (Right e)) 31 | pr s = "(" ++ s ++ ")" 32 | 33 | -- Generic fold on Term, used by pretty. 34 | fold :: (Var -> a) -> (Var -> a -> a) -> (a -> a -> a) -> Term -> a 35 | fold i g h = fold' 36 | where 37 | fold' (Var v) = i v 38 | fold' (Lam v e) = g v (fold' e) 39 | fold' (App f e) = h (fold' f) (fold' e) 40 | 41 | -- Basic parser combinators. 42 | nil :: ReadS [a] 43 | nil s = [([], s)] 44 | 45 | char :: (Char -> Bool) -> ReadS Char 46 | char f (c:s) | f c = [(c, s)] 47 | char f _ = [] 48 | 49 | mapP :: (a -> b) -> ReadS a -> ReadS b 50 | mapP f g = map (\ (c, s) -> (f c, s)) . g 51 | 52 | (&&&) :: ReadS a -> ReadS b -> ReadS (a, b) 53 | f &&& g = \s -> [ ((x, y), s2) 54 | | (x, s1) <- f s, 55 | (y, s2) <- g s1 ] 56 | 57 | (|||) :: ReadS a -> ReadS b -> ReadS (Either a b) 58 | f ||| g = \s -> map left (f s) ++ map right (g s) 59 | where left (x, s) = (Left x, s) 60 | right (y, s) = (Right y, s) 61 | 62 | (<|>) :: ReadS a -> ReadS a -> ReadS a 63 | f <|> g = mapP select (f ||| g) 64 | where select (Left x) = x 65 | select (Right y) = y 66 | 67 | many, many1 :: ReadS a -> ReadS [a] 68 | many r = many1 r <|> nil 69 | many1 r = mapP cons (r &&& many r) 70 | where cons (x, xs) = x : xs 71 | 72 | paren p = mapP f (sym '(' &&& p &&& sym ')') 73 | where f ((_, x), _) = x 74 | 75 | -- Read instance for Var and Term. 76 | instance Read Var where 77 | readsPrec _ = variable 78 | 79 | instance Read Term where 80 | readsPrec _ = term 81 | 82 | -- Parser for variables that start with lowercase letter, 83 | -- and optionally followed by a number. 84 | variable = mapP f (alpha &&& (digits <|> nil)) 85 | where f (c, d) = V (c : d) 86 | alpha = char (\c -> c >= 'a' && c <= 'z') 87 | digits = many1 (char isDigit) 88 | 89 | -- Parser for lambda terms that is not ambiguous, and not left-recursive. 90 | term, term', atom :: ReadS Term 91 | term = mapP (foldl1 App) (many1 term') 92 | term' = mapP fst (atom &&& (space ||| nil)) 93 | atom = lam <|> var <|> paren term 94 | 95 | var = mapP Var variable 96 | 97 | lam = mapP f (lbd &&& variable &&& sym '.' &&& term) 98 | where f (((_, v), _), e) = Lam v e 99 | 100 | lbd = (sym '\\' <|> sym 'λ') 101 | 102 | sym = char . (==) 103 | 104 | space = many1 (sym ' ') 105 | 106 | -- Randomly generate Var and Term for QuickCheck. 107 | instance Arbitrary Var where 108 | -- use a limited range to increase chances for variable re-use 109 | arbitrary = fmap (V . (:[])) $ choose ('a', 'z') 110 | 111 | instance Arbitrary Term where 112 | arbitrary = sized term 113 | where 114 | term 0 = Var <$> arbitrary 115 | term n = oneof [ Var <$> arbitrary, 116 | Lam <$> arbitrary <*> term (n-1), 117 | App <$> term m <*> term m ] 118 | where m = n `div` 2 119 | 120 | -- Free variable set 121 | freeVars :: Term -> [Var] 122 | freeVars = aux [] 123 | where 124 | aux env (Var v) | elem v env = [] 125 | | otherwise = [v] 126 | aux env (Lam v e) = aux (v:env) e 127 | aux env (App f e) = aux env f ++ aux env e 128 | 129 | 130 | -- Substitute a free varible by another 131 | subV :: Var -> Var -> Term -> Term 132 | subV u w (Var v) | v == u = Var w 133 | | otherwise = Var v 134 | subV u w (Lam v e) | v == u = Lam v e 135 | | otherwise = Lam v (subV u w e) 136 | subV u w (App f e) = App (subV u w f) 137 | (subV u w e) 138 | 139 | -- Substitute a free variable by a term 140 | subT :: Var -> Term -> Term -> Term 141 | subT u w (Var v) | v == u = w 142 | | otherwise = Var v 143 | subT u w (App f e) = App (subT u w f) 144 | (subT u w e) 145 | subT u w (Lam v e) 146 | | v == u = Lam v e 147 | | v `notElem` fvsW = Lam v (subT u w e) 148 | | otherwise = Lam x (subT u w (subV v x e)) 149 | where 150 | bvsE = boundVars e 151 | fvsE = freeVars e 152 | fvsW = freeVars w 153 | x = freshVar (bvsE ++ fvsE ++ fvsW) 154 | 155 | beta :: Term -> Term 156 | beta (App (Lam v e) e') = subT v e' e 157 | beta e = error ("Term " ++ show e ++ " is not a beta redex") 158 | 159 | -- =========================================================== 160 | -- You may modify functions below to complete the assignment. 161 | -- 162 | -- You may also use alphaEq from prog-07.hs to help check the 163 | -- correctness of subT. 164 | -- =========================================================== 165 | 166 | -- Bounded variable set 167 | boundVars :: Term -> [Var] 168 | boundVars e = undefined 169 | 170 | freshVar :: [Var] -> Var 171 | freshVar fvs = undefined 172 | 173 | redexes :: Term -> [Term] 174 | redexes e = undefined 175 | 176 | reduce :: Term -> Term 177 | reduce e = undefined 178 | 179 | -------------------------------------------------------------------------------- /tutorial/zh_cn/00-preface.md: -------------------------------------------------------------------------------- 1 | #手把手教你做λ - 引言 2 | 3 | 《手把手教你做λ》是一个 tutorial 系列,面向正在学习 Haskell 编程的程序员。如果你缺乏对 Haskell 的最基本概念,我建议还是从正式全面的教材开始,比如 [Learn You a Haskell for Great Good!] 4 | 5 | 换句话说,这个系列不会教你如何使用正确的语法编写 Haskell 程序,而是如何提高 Haskell 编程的水平。在开始的时候,你可以没有学过这门语言,甚至是编程新手。但是随着我们内容的展开,你应该能够独立从 Haskell 的书本上学习到相关基础知识,并参与到本教程中来。 6 | 7 | 这个系列之所以叫做《手把手教你做λ》,是因为我们的目的是最终用 Haskell 实现一个 Lambda 表达式的计算器,作为 CGI 程序运行在网站上,最后的效果参见 [Lambda Viewer]。如果你还不知道什么是 Lambda Calculus,那我们正好一起把它也给学了,因为它是所有函数语言的理论基础与核心。 8 | 9 | 本系列希望涵盖的内容有:algebraic datatype,recursion,higher-order function,polymorphism,type class,data traversal,monad,functor,applicative,monad transformer,generics,以及 Parsec 等常见库的使用,DSL (domain specific language) 的实现等等。 10 | 11 | 每一课的内容不多,只会涉及一个知识点甚至更少。会介绍一定的背景知识,并包含少量习题。这样大部分人每次用少量时间就可以跟上进度,想多学一点的则可以同时精读正规教材。 12 | 13 | 主要是希望大家能够积极参与讨论和动手实践,越多的互动越好。毕竟学习一门新的编程语言不仅仅是学习它语法规则和库的使用而已,还要能够用新的方式来思考,来抽象,来表达,来抓住问题的核心并解决它,从而完成一次自我的提升。这是每一个优秀的程序员的基本素养。 14 | 15 | 16 | [Lambda Viewer]: http://projectultimatum.org/cgi-bin/lambda 17 | [Learn You a Haskell for Great Good!]: http://learnyouahaskell.com 18 | -------------------------------------------------------------------------------- /tutorial/zh_cn/01-lambda-expression.md: -------------------------------------------------------------------------------- 1 | #手把手教你做λ(一)关于λ表达式的一切 2 | 3 | (前λ讲堂的学员可以跳过此节) 4 | 5 | 函数语言追根溯源的话,都是来自于 Alonzo Church 和他的学生 Stephen Kleene 在 1932 年的一篇论文引入的 λ-Calculus (λ演算) 的概念。1936 年 Alan Turing 发表论文引入了 Turing Machine (图灵机) 的概念,奠定了他当代计算机科学之父的地位。与此同时,他也成为 Church 的学生,在普林斯顿攻读博士学位。后来 Kleene 证明了λ演算、图灵机和早先 Kurt Gödel 引入的 general recursion (递归) 函数是等价的,并提出了一个伟大的命题,也就是后来人们说的 Church-Turing Thesis (丘齐图灵命题):任何可计算的东西都能够被这三种方式表达。 6 | 7 | 当然,可计算 (computable) 并不是一个有着精确定义的概念,这也是为什么丘齐图灵命题是一个无法被证明的 hypothesis (假说)。实际上,Church-Turing Thesis 往往被看作是对“什么是可计算的”一个定义。这个命题的优美之处,还在于它成功地在具象的机器和抽象的代数之间划了一个等号。前者着重执行指令,后者着重于对表达式求值。这也是的命令式语言 (Imperative Language),和函数式语言 (Functional Language) 的区别。 8 | 9 | 在一个只学习过命令式语言的程序员看来,Haskell 这类的函数语言实属异类。看似没有清晰的运行流程和分枝结构,既不实用又让人难以理解。其实作为 Haskell 的核心,λ表达式是异常简洁的。下面是关于λ表达式 (λ Expression),也叫λ项 (λ Term),的定义: 10 | 11 | 1. 变元是λ项,通常用小写字母写出来,比如 x,y,z 等; 12 | 2. 如果 M 是λ项,那么 λx.M 也是; 13 | 3. 如果 M 和 N 都是λ项,那么 M N 也是。 14 | 15 | 变元 (variable) 形态的λ项很简单,就是 x 或者 y 或者 z 或者什么别的,我们通常用单个小写字母,有 16 | 时候也加上数字脚标,比如 x0, x1, x2 等,以示区分。 17 | 18 | 抽象 (abstraction) 形态的λ项,写出来就是先用λ这个符号本身作开头,后面跟一个变元,然后一个小点,然后跟一个任意的λ项。例如 λx.x 或 λx.y 或 λx.x x 等。 19 | 20 | 应用 (application) 形态的λ项,就是两个λ项写在一起,中间仅留一个空格做分隔。例如 f x 或者 x x。 写在前面的λ项通常叫函数(function),后面的叫参数(argument)。比如在 x x 这个表达式里,就是把 x 这个函数作用于(apply to)它自己。 21 | 22 | 在实际书写抽象和应用两种λ项时,如果没有一定的标识,往往会产生歧义。所以通常是用括号来把一个λ项和它前后的符号区分开,比如 (λx.x) y 这个表达式,就是说函数 λx.x 作用在参数 y 上。 23 | 我们通常不认为括号本身是λ项的一部分,使用它们纯粹是为了书写。 24 | 25 | 括号的使用有时候也可以略去。约定俗成的习惯是在表达抽象时,小点后面整个式子(除非是遇到反括号) 26 | 都是与小点前面的变元对应的λ项。比如 λx.(λy.(x y)) 就可以简写为 λx.λy.x y;在表达应用时则向左结 27 | 合,比如 f x y 应该被理解为 (f x) y 而不是 f (x y)。 28 | 29 | #习题 30 | 31 | 以下哪些是正确的λ项? 32 | 33 | 1. a b c d e f 34 | 2. (g h) (i j) 35 | 3. λx.(x+1) 36 | 4. λx.f (λy.f y) 37 | 5. λ(x y).y 38 | 6. λ(λx.x).y 39 | 7. (λf.f x y) (λx.λy.x) 40 | 8. λx.y.x 41 | 9. f g λx.y z 42 | -------------------------------------------------------------------------------- /tutorial/zh_cn/02-data-type.md: -------------------------------------------------------------------------------- 1 | #手把手教你做λ(二)数据类型的结构与解构 2 | 3 | 4 | 如果你熟悉[BNF]语法,把λ表达式的结构写出来就是这样的: 5 | 6 | ::= | "λ" "." | 7 | " " 8 | ::= "a" | "b" | ... | "y" | "z" | ... 9 | 10 | 前面说过,我们的目标是实现一个[λ表达式的计算器](http://projectultimatum.org/cgi-bin/lambda)。那么在 Haskell 里面如何表示一个λ项呢?我们可以定义下面的数据类型(data type): 11 | 12 | data Term = Var Var | Lam Var Term | App Term Term 13 | data Var = V String 14 | 15 | 其中紧跟在关键字 `data` 后的 `Term` 和 `Var` 是我们定义的类型名字,等式右边 `Var`,`Lam`,`App` 是 `Term` 类型的 constructor (构造子),`V` 是 `Var` 类型的构造子。在 Haskell 里,构造子和类型都要用大写字母开头,而且不禁止构造子和类型名称重复,所以需要根据上下文来判断一下。构造子属于 value 层面,类型属于 type 层面,略为注意就不会混淆。例如第一行的 `Var Var` 里面,前者是构造子,后者是类型;而 `Lam Var Term` 这里,第一个 `Lam` 是构造子,后面的 `Var` 和 `Term` 都是类型。 16 | 17 | 不难发现,在 Haskell 里定义λ项的数据类型和它的 BNF 语法有些类似。不同之处是前者需要对每种λ项的形态提供一个构造子。这样做的目的是严格区分不同的类型,或者同一个类型的不同部分。这在构造类型的值,或者进行 pattern matching (模式匹配)时都是必要的。 18 | 19 | 有了以上的定义,我们可以在 Haskell 里面表达任意的λ项了。比如: 20 | 21 | - `Var (V "x")` 代表变元形态的λ项 x 22 | - `Lam (V "f") (Lam (V "x") (App (Var (V "f")) (Var (V "x"))))` 代表 λf.(λx.(f x)),或者简写为 λf.λx.f x 23 | 24 | 这样构建出来的λ项,也叫做 `Term` 类型的值。我们不妨把类型看作是一个集合 (set),里面包括各种可以用它的构造子们合法构建的值。比如 `Integer` 类型的值可以是任意整数,构造子为 `0|1|2|...`;而 `String` 类型的值可以是任意长度的字符串。 25 | 26 | 在 Haskell 里用 `data` 关键字定义的数据类型,也叫做代数数据类型 (Algebraic Data Type, 或简称 ADT),可以很方便地对各种抽象结构进行表述。定义和运用 ADT 也是高级编程语言的一个基本功能。 27 | 28 | 数据类型就是数据类型好了,为什么叫 algebraic?一方面是因为可以用自定义的类型名字,例如上面的 `Term` 和 `Var`,来指代它们所定义的类型,以构建更多的类型。例如 `Var` 类型就被用来定义 `Term`,而 `Term` 的定义里还用到了它自己,构成一个递归结构。 29 | 30 | 那么代数数据类型的基本结构是什么?从上面的例子,我们至少可以观察到以下两种: 31 | 32 | 1. Product (积)。如果 A、B 指代两种不同的类型,那么 A×B 表示两者的积类型,它的值里面,同时要有一个 A 类型的值与一个 B 类型的值。通常所说的 tuple 类型就是这个意思。 33 | 34 | 2. Sum (和)。如果 A、B 指代两种不同的类型,那么 A+B 表示两者的和类型,它的值可以是 A 类型的值,也可以是 B 类型的值。 35 | 36 | 如果我们用逻辑关系做比方,product 可以看作是“与”,sum 可以看作是“或”。 37 | 38 | 如果抛开构造子,`Term` 其实就是 `Var + Var × Term + Term × Term`。换句话说,`Term` 类型是三种类型的和:它可以是变元、抽象、或应用,三者之一。其中变元等同与 `Var` 类型,抽象等同于一个 `Var` 和一个 `Term` 的积,而应用等同于一个 `Term` 和另一个 `Term` 的积。 39 | 40 | 实际上在 C 语言里我们也有 struct 和 union,对定的就是 product 和 sum。这么重要的基础概念,反而在多数脚本语言里得不到支持 (尤其是sum),实在是遗憾。 41 | 42 | 除开 product 和 sum,ADT 还有其它几种基本结构:0 (此类型不包含任何值),1 (此类型只有一个值),和 → (函数结构,有时也叫做幂)。这样一来称之为 algebraic data type 总算名副其实了吧?这些理论源于 category theory,限于篇幅不再一一讨论。 43 | 44 | #习题 45 | 46 | 一、根据上述 `Term` 和 `Var` 类型的 Haskell 定义,给出以下λ项所对应的 Haskell 表达式: 47 | 48 | 1. a b c d e f 49 | 1. (g h) (i j) 50 | 1. λx.f (λy.f y) 51 | 1. (λf.f x y) (λx.λy.x) 52 | 1. f g λx.y z 53 | 54 | 二、Haskell 常用的列表类型 `[a]` 用 product 和 sum 的方式该如何定义? 55 | 56 | [BNF]: http://en.wikipedia.org/wiki/Backus–Naur_Form 57 | 58 | -------------------------------------------------------------------------------- /tutorial/zh_cn/03-induction.md: -------------------------------------------------------------------------------- 1 | #手把手教你做λ(三)漂亮打印与数学归纳法 2 | 3 | 在上一节,我们定义了以下的数据类型,用来在 Haskell 程序里表达一个λ项: 4 | 5 | data Term = Var Var | Lam Var Term | App Term Term 6 | data Var = V String 7 | 8 | 虽然这种内部表达形式和λ项的结构一致,但它并不利于阅读和书写,而且关于 `Term` 的表达式只能写在程序里面。所以接下来我们要解决的问题,就是如何在内部数据类型和书面的字串之间转换,也即关于λ项的输入和输出的问题。 9 | 10 | 先讲输出。从数据结构转换成字串输出的过程,有个好听的名字叫 Pretty Print。在 Haskell 里,我们通常可以用 show 函数来做这件事情。例如:`show 123` 的结果是字串 `"123"`,而 `show pi` 的结果是 `"3.141592653589793"`。我们可以对 `Term` 和 `Var` 也自行定义 `show` 函数来做 Pretty Print,比如: 11 | 12 | instance Show Var where 13 | show (V s) = s 14 | 15 | 关于 `Var` 类型的 `show` 函数非常简单,直接使用在 `Var` 类型的定义里代表变元名字的那个字串就可以了。而对 `Term` 定义 `show` 函数就略为复杂一点: 16 | 17 | instance Show Term where 18 | show (Var v) = show v 19 | show (Lam x e) = "λ" ++ show x ++ "." ++ show e 20 | show (App f e) = show f ++ " " ++ show e 21 | 22 | 这里我们针对 `Term` 类型的每种构造形式分别进行处理: 23 | 24 | - 对变元结构直接用 `show v`,因为这里 `v` 是 `Var` 类型,而我们已经定义了 `Var` 类型的 `show` 函数。 25 | - 对函数结构则用 "λ" 和 "." 符号把型参变元 `x` 和函数体 `e` 连接起来,其中对于 `v` 和 `e` 都使用 `show` 来把它们分别转换成字串; 26 | - 对应用结构则用空格把函数部分 f 和参数部分 e 隔开就好了。 27 | 28 | 在前一节,我们已经注意到定义 `Term` 类型后两种结构时,也同样用到了 `Term` 类型本身。这种递归结构几乎是以同样的形态出现在 `show` 函数的定义里:例如,为了得到 `Lam x e` 里面子结构 `e` 的字串,我们直接使用 `show e`,用 `show` 函数本身来定义 `show` 就是递归。 29 | 30 | 我们在中学数学里学习过自然归纳法,它把关于自然数 n 的证明,归结成两种情况: 31 | 32 | 1. 当 n = 1 时证明命题成立。 33 | 2. 假设 n = k 时命题成立,证明 n = k + 1 时命题同样成立。 34 | 35 | 实际上,自然归纳法借助了自然数的内部结构,也即 36 | 37 | - Base case (基础):1 是自然数。 38 | - Inductive case (归纳):假如已知 k 为自然数,那么 k + 1 也是自然数。 39 | 40 | 同样在 `Term` 的定义里,我们也能够观察到类似的情况: 41 | 42 | 1. `Var Var` 是基础: 如果 `v :: Var` 那么 `Var v :: Term`。这里尽管我们有一个 `v` 属于 `Var` 类型的前提,但它不是递归。 43 | 2. `Lam Var Term` 是归纳:如果 `v :: Var`,`e :: Term`,那么 `Lam v e :: Term`。这里对于 `e` 的类型所作的前提假设是递归。 44 | 3. `App Term Term` 也是归纳:如果 `f, e :: Term`,那么 `App f e :: Term`。这里对于 `f` 和 `e` 的类型所作的前提假设是递归。 45 | 46 | 对数据类型采用 base case 和 inductive case 的定义方式叫做归纳定义 (inductive definition)。我们回过头看关于 `Term` 的 `show` 函数,它的定义同样也是归纳定义: 47 | 48 | 1. `show (Var v)` 是基础:已知 `show v`,将变元结构的 `Term` 转换为字串。 49 | 2. `show (Lam v e)` 是归纳:已知 `show v` 和 `show e`,将函数结构的 `Term` 转换为字串。 50 | 3. `show (App f e)` 也是归纳:已知 `show f` 和 `show e`,将应用结构的 `Term` 转换为字串。 51 | 52 | 作为程序员,我们可能更熟悉遍历 (traversal) 这个概念,把 `show` 函数看作是对一个有着递归结构的数据进行遍历操作,在每个节点计算它所对应的字串。有趣的是,在这里我们又一次成功地把具象的操作和抽象的数学归纳法对应起来了。 53 | 54 | 而作为细心的程序员的你,一定也早就发现上面定义的 `show` 函数有个 bug:它没有考虑到用字串书写λ项可能会产生的歧义。比如以下两个表达式的计算结果都是字串 `"λx.x x"`: 55 | 56 | - show (Lam (V "x") (App (Var (V "x")) (Var (V "x")))) 57 | - show (App (Lam (V "x") (Var (V "x"))) (Var (V "x"))) 58 | 59 | 而它们完全是两种结构。为了消除歧义,我们可以在 Pretty Print 时加入括号。更正后的 `show` 函数如下: 60 | 61 | instance Show Term where 62 | show (Var v) = show v 63 | show (Lam x e) = "(λ"++ show x ++"."++ show e ++")" 64 | show (App f e) = "(" ++ show f ++" "++ show e ++")" 65 | 66 | 这样一来,上面两个例子的结果分别是 `"(λx.(x x))"` 和 `"((λx.x) x)"`,就不会产生歧义了。 67 | 68 | #习题 69 | 70 | 一、如果把上面的 `show` 函数看作是对 `Term` 的遍历操作,那它是先序 (pre-order) 还是后序 (post-order)?你能用另一种方法实现 show 函数吗? 71 | 72 | 二、上面的 `show` 函数作为 Pretty Print 其实还不够漂亮,因为括号用的太多了。比如 (λx.(x x)),考虑到 . 的作用范围是到最右边,可以简写为 λx.x x;而如果是 ((f x) y),考虑到应用结构是左结合 (left associative),可以化简为 f x y。你能实现一个新的 `show` 函数,让它的结果包含最少的括号吗? 73 | 74 | -------------------------------------------------------------------------------- /tutorial/zh_cn/04-parser.md: -------------------------------------------------------------------------------- 1 | #手把手教你做λ(四)语法识别器、高阶函数和列表推导 2 | 3 | 上一节里我们实现了把 `Term` 类型转换为字串的 `show` 函数,解决了输出问题。接下来,我们要反过来,考虑该如何把一个代表λ项的字串转换为 `Term` 类型的数据结构,这也叫做语法识别器 (parser)。在 Haskell 的标准库里,有一个跟 `Show` 对应的类型类 (type class),叫做 `Read`: 4 | 5 | type ReadS a = String -> [(a, String)] 6 | class Read a where 7 | readsPrec :: Int -> ReadS a 8 | 9 | 类型 `ReadS a` 所代表的就是对类型 `a` 的识别器,它是这样一个函数:对给定的字串参数从左到右进行识别 (parse),如果成功,则返回所识别的值 (属于 `a` 类型) 和剩下的字串;如果不成功,则返回空列表。通常成功时只返回一个值,但是考虑到有时候需要允许歧义,用列表也可以返回多个值,以代表所有的可能性。 10 | 11 | 而定义类型类 `Read` 的实例 (instance) 则需要定义一个 `readsPrec` 函数,它的参数代表优先级,返回的则是 `ReadS a` 所代表的识别器。我们这里用不到优先级,可以忽略掉。主要任务是实现类型 `Var` 和类型 `Term` 的识别器。 12 | 13 | 假设λ表达式里所有的变元都是单个字母,那么很容易就可以写出如下的识别器: 14 | 15 | import Data.Char 16 | 17 | variable :: ReadS Var 18 | variable (c:s) | isAlpha c = [(V [c], s)] 19 | variable _ = [] 20 | 21 | instance Read Var where 22 | readsPrec _ = variable 23 | 24 | 这里我们用到了模式匹配来判断字串的第一个字符是否是字母(`isAlpha` 函数出自 Data.Char 模块),如果是,则成功返回符合 `Var` 类型的值 `V [c]` 和余下的字串 `s`。否则返回空列表 `[]`,代表识别失败。其中 `| isAlpha c = ...` 的写法叫做 `guard`,意思是满足条件 `isAlpha c == True` 的情况下才计算右边。这是一种语法糖 (syntactic sugar),意即非关键的语法性能,它可以完全转成 case 的写法。 25 | 26 | 实际上,我们会经常用到单个字符的识别器,所以这个功能可以单独被提取出来,成为下面的函数: 27 | 28 | char :: (Char -> Bool) -> ReadS Char 29 | char f (c:s) | f c = [(c, s)] 30 | char f _ = [] 31 | 32 | 这样一来,用 `char isAlpha` 就可以识别所有的字母。细心的读者会发现,`char isAlpha` 是 `ReadS Char` 类型,那怎么把它变成我们需要的 `ReadS Var` 类型呢?根据 `ReadS` 的定义,其返回的值是一个列表,那么事情就简单了: 33 | 34 | variable s = map f (char isAlpha s) 35 | where f (c, s1) = (V [c], s1) 36 | 37 | 注意到 `char isAlpha s` 返回的是 `[(Char,String)]` 类型,我们要把它变成 `variable s` 所需要返回的 `[(Var, String)]` 类型,而这可以用 `map :: (a -> b) -> [a] -> [b]` 来做到。如果 `f` 是从类型 `a` 到类型 `b` 的映射,那么 `map f` 则是从类型 `[a]` 到类型 `[b]`。换句话说,`map` 是一个高阶函数 (higher-order function),它能够把一个 `a -> b` 类型的函数,转换 `[a] -> [b]` 类型的函数,用于处理列表。 38 | 39 | 关于列表的运算,除了使用 `map` 这样的函数,在 Haskell 里还可以用推导式 (comprehension) 来表示,比如 `variable` 还可以写成: 40 | 41 | variable s = [(V [c], s1) | (v, s1) <- char isAlpha s] 42 | 43 | 列表推导式也是一种语法糖,上述两种 `variable` 函数的写法完全等价,而使用推导式有时能够帮助理解复杂的表达式。这很类似于数学里关于集合运算的写法,比如上面这个定义就可以理解为:对于列表 `char isAlpha s` 里的每一个元素 `(v, s1)`,生成新的元素 `(V [c], s1)` 并组成新的列表返回。 44 | 45 | 假设我们规定变元必须是两个字母,那么该如何实现这样的识别器?这就需要用到复杂一点的列表推导式: 46 | 47 | variable2 s = [ (V [c1,c2], s2) 48 | | (c1, s1) <- char isAlpha s 49 | , (c2, s2) <- char isAlpha s1 ] 50 | 51 | 这可以简单理解为,在 `s` 字串中识别一个字母 `c1`,在剩下的 `s1` 字串中识别第二个字母 `c2`,将两个字母拼成一个字串 `[c1,c2]` 成为识别结果。 52 | 53 | # 习题 54 | 55 | 一、在不使用 GHC 或 GHCi 的情况下,(心算)给出下列表达式的值: 56 | 57 | 1. variable "" 58 | 1. variable "a" 59 | 1. variable "123" 60 | 1. variable "abc" 61 | 1. variable2 "" 62 | 1. variable2 "a" 63 | 1. variable2 "abc" 64 | 65 | 二、把 variable2 用普通方法定义,不要使用列表推导。如果要更挑战一点,可以考虑仅用 `map` 和 `concat`。 66 | 67 | 三、定义一个函数 `variableN :: Int -> ReadS Var`,根据参数 n 生成长度为 n 的变元识别器。仅考虑 n >= 0 的情况。 68 | 69 | -------------------------------------------------------------------------------- /tutorial/zh_cn/05-parser-combinator.md: -------------------------------------------------------------------------------- 1 | #手把手教你做λ(五)语法识别器的拼接与组合 2 | 3 | 上一节里我们实现了针对单个字母的变元识别器 (parser),它是这样的: 4 | 5 | type ReadS a = String -> [(a, String)] 6 | 7 | char :: (Char -> Bool) -> ReadS Char 8 | char f (c:s) | f c = [(c, s)] 9 | char f _ = [] 10 | 11 | variable :: ReadS Var 12 | variable s = map f (char isAlpha s) 13 | where f (c, s1) = (V [c], s1) 14 | 15 | 类型 `ReadS a` 所代表的就是对类型 `a` 的识别器,`char` 用于识别单个字符,`variable` 则通过 `map f` 将 `char` 所识别的单个字符转为我们所需要的 `Var` 类型。这种从 `ReadS a` 到 `ReadS b` 的 map 转换方式会经常用到,让我们把它提出来单独定义为: 16 | 17 | mapP :: (a -> b) -> ReadS a -> ReadS b 18 | mapP f = map (\ (c, s) -> (f c, s)) 19 | 20 | 那么 `variable` 可以写成: 21 | 22 | variable = mapP (\c -> V [c]) (char isAlpha) 23 | 24 | 注意这里连参数 `s` 被省略掉了,那是因为 `char isAlpha` 的类型就是 `ReadS Char`,而匿名函数 `\c -> V [c]` 的类型是 `Char -> Var`,所以通过 `mapP` 我们正好得到了 `variable` 函数所需的类型 `ReadS Var`。 25 | 26 | 用 `char` 做出来的识别器仅能识别单个字符,在本节我们要考虑如何将它扩展成可以识别多个字符 (比如开头用字母,后面用数字) 的变元识别器。 27 | 28 | 一种常见的解决方法是考虑如何根据给定的条件切割字串, 这用 Prelude 里面的 `takeWhile`, `dropWhile` 或者 `span` 函数就可以做到。但是在这里,我们不妨换个角度,先来考虑一下是否有更普适 (general) 的方案。 29 | 30 | 既然我们已经可以做出单个字符的识别器,是否可以仅用它来搭建更复杂的识别器呢?而识别器是否能够互相拼接起来,又有哪几种拼接方式?最基础的识别器都有哪些? 31 | 32 | 先来回答最后一个问题。既然我们考虑的识别器都是以字串 (字符的列表) 为输入类型,那么显而易见,必要的一个基础识别器是识别单个字符,这个就是我们已经实现了的 `char` 函数。然后相应地,识别空字串也应该是一个基础识别器: 33 | 34 | nil :: ReadS [a] 35 | nil s = [([], s)] 36 | 37 | 对于任何输入,`nil` 都可以成功识别,并且返回识别值 (就是空表),以及剩余的字串 (就是原字串)。由于空表可以代表任意类型的列表,所以 `nil` 代表的是对任意列表类型 `[a]` 的空表识别器。 38 | 39 | 现在我们有两个基础识别器了,再来看看它们的拼接组合方式有哪些。根据上下文无关文法 (Context Free Grammar,缩写为 CFG) 的结构,应该至少有两种方式:序列和分支。 40 | 41 | 序列是假设有两个识别器 `f :: ReadS a` 和 `g :: ReadS b`,先用 `f` 识别,然后用 `g` 识别剩下的字串,这样我们得到一个 `a` 类型和一个 `b` 类型的值,放在一起就是类型为 `(a, b)` 的双元组 (也叫 tuple)。借助上一章学习的列表推导式,序列方式的组合可以写成下面的函数: 42 | 43 | (&&&) :: ReadS a -> ReadS b -> ReadS (a, b) 44 | f &&& g = \s -> [ ((x, y), s2) 45 | | (x, s1) <- f s, 46 | (y, s2) <- g s1 ] 47 | 48 | 这里我们定义了一个中缀 (infix) 操作符函数 `&&&` 来拼接 `f` 和 `g`。因为 Haskell 对操作符默认是中缀的方式,所以在等式左侧写成 `f &&& g` 实际上表示的是对函数 `&&&` 的定义,而不是对 `f` 的定义。这里 `f` 和 `g` 则是此函数的参数。中缀操作符如果用括号括起来,则可以像其它前缀函数一样自由使用,例如,用于声明它的类型。 49 | 50 | 在等式右边则使用了列表推导,先用 `f` 在 `s` 中识别出 `x :: a`,再用 `g` 在剩余字串 `s1` 中识别出 `y :: b`,然后将 `(x, y)` 组合成列表返回得到 `ReadS (a, b)` 类型。注意等式右边首先是一个以字串为参数的匿名函数,因为 `f &&& g` 的类型是 `ReadS (a, b)`,展开即为函数类型 `String -> [(a,b), String]`。 51 | 52 | 接下来考虑分支组合。我们可以将 `f :: ReadS a` 和 `g :: ReadS b` 组合成 `ReadS (Either a b)`。这个思路非常自然,因为前面章节里,我们提到过数据类型的两个基本结构是 product 和 sum,类型 `(a, b)` 表达的是前者,而后者则可以用类型 `Either a b` 表达,它在 Prelude 里面定义如下: 53 | 54 | data Either a b = Left a | Right b 55 | 56 | 亦即“左”分支表示 `a` 类型,“右”分支表示 `b` 类型,两者选其一。 57 | 58 | 语法识别器无可避免地要在程序中构建数据类型,甚至可以看作是从字串起始来构建数据类型,所以它们的组合方式类似于 product 和 sum 也就不奇怪了。但在分支组合时,如果 `f` 和 `g` 都能够成功识别那该怎么办?而 `Either` 似乎不能够同时代表个值,那么简单的办法是优先选择其中一个。假设我们选择倾向于 `f`,写成函数如下: 59 | 60 | (|||) :: ReadS a -> ReadS b -> ReadS (Either a b) 61 | f ||| g = \s -> case f s of 62 | [] -> map right (g s) 63 | xs -> map left xs 64 | where left (x, s) = (Left x, s) 65 | right (y, s) = (Right y, s) 66 | 67 | 这里 `|||` 也是我们定义的中缀操作符函数,当 `f s` 成功识别时 (返还值不为空表),我们用 `map left` 将 `xs :: [(a, String)]` 作为左分支转为 `[(Either a b, String)]` 类型;否则我们则用 `map right` 将 `g s` 的识别值 (当 `g` 识别失败时,返回的空表不影响 `map` 操作) 作为右分支也转为同样的 `Either` 类型。 68 | 69 | 考虑到在进行分支识别时,我们识别出来的往往是同类型的值,也就是说 `a` 和 `b` 是相同类型的时候,这时只需要返回一种类型即可: 70 | 71 | (<|>) :: ReadS a -> ReadS a -> ReadS a 72 | f <|> g = mapR select (f ||| g) 73 | where select (Left x) = x 74 | select (Right y) = y 75 | 76 | 这里,`mapR select` 将 `f ||| g` 返还的 `ReadS (Either a a)` 转换为 `ReadS a` 类型。函数 `select` 也可以直接用 Prelude 中 `either` 函数来定义 `select = either id id`。 77 | 78 | 有了序列和分支这两种基本组合方式,我们可以定义更多样的组合。比如: 79 | 80 | many, many1 :: ReadS a -> ReadS [a] 81 | many r = many1 r <|> nil 82 | many1 r = mapP cons (r &&& many r) 83 | where cons (x, xs) = x : xs 84 | 85 | 函数 `many1` 可以用参数所给的识别器 `r :: ReadS a`,来连续从输入字串中识别出一个或多个 `a` 类型的值。`many` 则可以识别零个或多个。值得注意的是,两个定义是相互递归 (mutural recursion),其中 `many1` 是用 `r &&& many r` 实现,代表用 `r` 和 `many r` 顺序识别,这样就是一个以上。而 `many` 则是 `many1 r <|> nil`,代表一个以上或者没有。因为我们定义了 `<|>` 的操作是左边优先,所以 `nil` 不能放在左边,不然因为它总是成功,会造成返回值恒为空。 86 | 87 | 有了这样的武器,识别多个字符 (第一个字符为字母,后面可以跟数字) 的变元就简单了: 88 | 89 | variable = mapP f (alpha &&& digits) 90 | where f (c, d) = V (c : d) 91 | alpha = char isAlpha 92 | digits = many1 (char isDigit) 93 | 94 | 回顾本节开头,正是因为我们没有用 Prelude 中现成的表处理函数,而是去探寻更普适的解决方案,才得以总结出了两种基础识别器,和识别器的两种基本组合方式,加上递归的运用,就能够构建在 CFG 范围内任意复杂的识别器。这种对事物本身规律的探寻和概括,提升抽象 (abstraction) 层面的做法,是编程最的核心能力,在 Haskell 里面的运用尤其突出。 95 | 96 | # 习题 97 | 98 | 一、用组合的方式设计一个识别器 `num :: ReadS Int`,从字串中识别十进制的自然数,正则表达式为 `[0-9]+`。换句话说,是要实现一个和 Prelude 里定义的 `reads :: ReadS Int` 等价的函数。不可以用 `read`,但是可以用 `fromEnum` 来把 `Char` 转为 `Int`。 99 | 100 | 二、用组合的方式设计一个识别器 `readR :: ReadS Rational`,识别正则表达式 `[+-][0-9]+(%[0-9]+)?` 表示的有理数。换句话说,是要实现一个和 Prelude 里定义的 `reads :: ReadS Rational` 等价的函数。注意 Haskell 里有理数的分子分母之间是用 `%` 符号隔开。 101 | 102 | 三、上一节讲到 `ReadS a` 的类型可以表达多个识别值,但以上定义的 `<|>` 和 `|||` 的组合方式只能返回其中一个分支的可能性。应该如何定义 `<|>` 或者 `|||` 让它们能够返回两个分支的所有可能性呢? 103 | 104 | -------------------------------------------------------------------------------- /tutorial/zh_cn/06-lambda-parser.md: -------------------------------------------------------------------------------- 1 | #手把手教你做λ(六)如何正确识别λ表达式 2 | 3 | 在上一节,我们归纳了基础的语法识别器 `char` (识别单个字符)和 `nil`(识别空字串),还有识别器的两种组合方式 `&&&` (序列组合)和 `|||` (分支组合),以及一个灵活转换识别器类型的操作 `mapP`: 4 | 5 | mapP :: (a -> b) -> ReadS a -> ReadS b 6 | 7 | 在这个基础上,我们还定义了 `many` 和 `many1` ,并且实现了多字符的变元识别器 `variable`。现在,我们可以着手写一个λ项的识别器了。回顾一下λ项的语法和数据结构: 8 | 9 | ::= | "λ" "." | 10 | " " 11 | 12 | data Term = Var Var | Lam Var Term | App Term Term 13 | 14 | 因为 Haskell 本身就支持定义之间相互引用,所以我们把语法和数据结果对应起来就得到如下的程序: 15 | 16 | term :: ReadS Term 17 | term = var <|> lam <|> app 18 | lam = mapP f (lbd &&& variable &&& sym '.' &&& term) 19 | where f (((_,v),_),e) = Lam v e 20 | app = mapP f (term &&& space &&& term) 21 | where f ((f, _), e) = App f e 22 | lbd = sym '\\' <|> sym 'λ' 23 | space = many1 (sym ' ') 24 | sym c = char (==c) 25 | 26 | 这里,我们定义了 `sym` 函数来辅助识别给定的字符,其中 `(==c)` 的写法叫“section”,因为 `(==)` 需要两个参数才能返回 `Bool`,给它一个参数 `c`,那么就变成还需要一个参数就返回 `Bool`。所以 `(==c)` 表示的是一个用于判断参数是否等于 `c` 的函数。 27 | 28 | 以上的定义看似合理,但它并不是正确的程序。首先,上节末的习题(三)提到,分支组合 `|||` 应该返回两个分支的所有可能性,而不是仅选择左边(或右边)的识别器。这是为什么呢?我们不妨看看在 GHCi 中求值的例子: 29 | 30 | > term "a a" 31 | [(a," a")] 32 | 33 | 原本 `a a` 应该是表示一个应用项,但是 `term` 函数在对它进行分支匹配时,直接选取了 `var` 分支,忽略掉其它的,造成最终结果不正确,而且还有剩余字串未能识别成功。那么把 `app` 放在 `var` 前面是否就正确了呢?也不是。实际上,语法识别是一个不断试探、不断回溯 (backtracking) 的过程,成功匹配了当前的一个分支,并不意味着之后对剩余的字串就能够匹配成功,否则应该选择另外的分支重试。这时候如果把其它分支完全忽略掉,就会错过正确的识别结果。所以正确的 `|||` 定义应该把两个分支的匹配结果都保留下来: 34 | 35 | f ||| g = \s -> map left (f s) ++ map right (g s) 36 | where left (x, s) = (Left x, s) 37 | right (y, s) = (Right y, s) 38 | 39 | 在修改了 `|||` 的定义后,我们会发现 `term "a a"` 依然无法成功识别,在得到头两个匹配结果后,它会进入一个无限循环,最终抛出栈溢出的异常: 40 | 41 | > term "a a" 42 | [(a," a"),((a a),"")*** Exception: stack overflow 43 | 44 | 这又是为什么呢?更简单一点,直接对 `term ""` 求值就会引发栈溢出。如果我们试着分析这其中的求值过程,会发现当 `term` 面对空串时,`var` 和 `lam` 都匹配不成功,然后进行 `app` 的匹配,而 `app` 又首先使用 `term` 去匹配空串,这就造成了无限的递归循环,最终引发栈溢出。这是一个经典的问题,也即递归下降 (recursive descent) 类型的语法识别器无法处理“左递归语法”识别空串的情况。 45 | 46 | 不仅如此,当我们回头检视λ项的语法时,会发现 `app` 的写法还有另一个问题。例如 x y z 到底代表了 (x y) z 还是 x (y z)?在本教程第一节里,我们谈到过,解决歧义的方法是默认是向左结合,需要右结合时则使用括号。这时就需要定义个 `paren` 函数来辅助识别左右括号了: 47 | 48 | paren :: ReadS b -> ReadS b 49 | paren p = mapP f (sym '(' &&& p &&& sym ')') 50 | where f ((_, x), _) = x 51 | 52 | 这里,`paren p` 和识别器 `p` 返回一样的值,但包括识别它左右的括号。引入括号之后右结合就不是问题了,那么在处理多项并列时,如何解决默认左结合的问题呢?这时我们可以把用空格分开的λ表达式当成多项式,用 `many1` 来处理: 53 | 54 | term, term', atom :: ReadS Term 55 | term = mapP (f id) (many1 term') 56 | where f g [x] = x 57 | f g (x:xs) = f (App (g x)) xs 58 | term' = mapP fst (atom &&& (space ||| nil)) 59 | atom = lam <|> var <|> paren term 60 | 61 | 在 `term` 的定义中,我们用 `many1 term'` 来识别多个 `Term`, 而其中辅助函数 `f` 则是把 `[Term]` 以左结合的方式转变为 `Term`。而且因为 `many1` 至少返回一个识别值,`f` 无须处理空表的情况。在 `f` 的定义里,它的参数 `g` 是用来实现左结合的函数,而 `App (g x)` 的写法可以理解为:对下一个 `x`,先进行左结合 `(g x)`,然后用 `App` 生成新的结合函数,传递下去让 `f` 处理剩下的 `xs`。 62 | 63 | 如果我们使用 Prelude 里面给出的 `foldl1` 函数,可以更简洁地定义 `term` 如下: 64 | 65 | term = mapP (foldl1 App) (many1 term') 66 | 67 | 这里,`term'` 是为了处理λ项之间的空格,而 `atom` 则是代表对单项式的识别,包括 `lam`、`var` 以及放在括号里的 `term`。这时我们发现,无论是 `term` 还是 `atom` 都不再是左递归,也就回避了左递归语法引发的问题。 68 | 69 | 有了正确的语法识别器,我们不难给出对于 `Term` 的 `Read` 类型类的声明(和对 `Var` 的声明类似,我们忽略掉对优先级的处理): 70 | 71 | instance Read Term where 72 | readsPrec _ = term 73 | 74 | 至此,我们完成了一个λ表达式的识别器,解决了初步的程序输入和输出的问题。 75 | 76 | *附注:* 因为 `'λ'` 字符其实是个字母,也就是说 `isAlpha 'λ'` 为 `True`,所以建议程序中还是用 `'\\'` 代替,或者把变元的识别从 `isAlpha` 改为 `\x -> x >= 'a' && x <= 'z'`。 77 | 78 | #习题 79 | 80 | 一、我们定义了 `term'` 来处理空格,但这种方法不是特别全面,比如括号后面的空格,或者符号 `.` 前后的空格,都会造成识别失败。能否有一个更好的处理空格的方式?需要怎样修改程序? 81 | 82 | 二、尽管我们避免了识别应用形态λ项的歧义,但是在使用λ抽象时,依然会有产生歧义的情况。比如 `term "λx.x x"` 会得到三个结果: 83 | 84 | [((λx.(x x)),""),(((λx.x) x),""),((λx.x)," x")] 85 | 86 | 前两个都完整地匹配了输入字串,这造成 `read "λx.x x" :: Term` 会得到错误 Exception: Prelude.read: ambiguous parse。假设我们定义λ的作用域是延展到最右边,那么应该如何修改语法,让 `read "λx.x x"` 正确读出唯一的结果 `(λx.(x x))`? 87 | 88 | -------------------------------------------------------------------------------- /tutorial/zh_cn/07-alpha-conversion.md: -------------------------------------------------------------------------------- 1 | #手把手教你做λ(七)从形式语义到等价关系 2 | 3 | 之前我们只是从语法角度解决了如何打印与识别λ表达式,现在我们可以进一步学习它所表达的含义,也即λ表达式的语义。所谓语义(semantics),简单讲就是如何来理解一件事情。而我们这里说的“事情”,就是λ表达式,在研究语义的时候,我们称之为目标语言(object language)。 4 | 5 | 形式化语义(formal semantics)就是通过形式化的方法(formal methods)来明确和制定一个目标语言所表达的语义。常用的方法有三种: 6 | 7 | 1. 操作语义(operational semantics):通过将目标语言分解成一系列的操作步骤来理解它是做什么的。这里隐含的一个条件是,先要假定有一个抽象机(abstract machine),以及与之伴随的一系列操作指令,这样才能够通过理解目标语言所对映的操作来理解它在计算什么。 8 | 9 | 2. 指称语义(denotational semantics):将目标语言翻译成元语言(meta language),通常这个元语言是我们用的数学语言,比如函数啊集合啊之类的。假定我们已经理解了元语言,那么通过指称语义就可以理解目标语言所表达的是什么了。这里重要的是弄清楚所指称的到底是什么。 10 | 11 | 3. 公理语义(axiomatic semantics):我们先假定针对目标语言有几条恒真的公理,然后通过数理逻辑来进一步理解这个目标语言里面都能够证明些什么。公理能够给出的一种最基本判断,就是等价关系(equivalence)。以公理和衍生的定理为基础,我们就可以进一步判断两种在字面上不同的表达是否等价,从而理解在这个目标语言里什么是相同的什么是不同的。 12 | 13 | 通常而言,从操作到指称到公理,理解或者定义一个目标语言所需要的辅助工具越来越少,也意味着语义越来越抽象。最后在公理语义里,只剩下基于公理的命题判断,而将计算的细节(比如时间空间复杂度)全部忽略掉了。从语义学的角度,越是能够发现不同之处,就越具体(concrete),而反之,则越抽象(abstract)。 14 | 15 | 举一个极端 concrete 的例子,我们可以说两个表达式看起来字面上(lexical)不一样,那它们就不一样。但这样一来,所有的理解就都停留在字面上,没有价值(trivial)。反方向极端 abstract 例子,则是所有表达式都等价,都是一个意思。那这样的话,这个语言存在大量冗余(redundant),只留下一个符号不就好了吗?所以,对其中不同分寸的把握,也促成了不同的语义定义。而通过采用不同的形式化方法,则能够从多个层面来解释我们的目标语言,帮助理解。 16 | 17 | 回到λ表达式,在正式介绍它的公理语义之前,我们首先引入两个概念,用于进一步分析它的结构。第一个概念是关于变元的绑定(binding)。在 `λx.M` 中,紧跟λ符号之后的 `x`,叫形式参数(formal parameter)简称形参。所有出现在 `M` 里的 `x`,都被当前这个λ的形参 `x` 所绑定,除非还有内层的λ抽象也绑定了 `x`。例如在 `λx.(x (λy.(x (λx.x))))` 里面,只有第二和第三个 `x` 是被第一个 `x`(形参)绑定。 18 | 19 | 绑定的概念也可以换一种说法:一个变元只被当前或外层离它最近的同名形参所绑定。被绑定有时候也叫做被捕捉(captured)。与绑定相对应的是自由变元(free variable):一个λ项中没有被任何形参绑定的变元就是自由的。我们可以定义一个函数,来找出一个λ项中所有的自由变元: 20 | 21 | freeVars :: Term -> [Var] 22 | freeVars = aux [] 23 | where 24 | aux env (Var v) | elem v env = [] 25 | | otherwise = [v] 26 | aux env (Lam v e) = aux (v:env) e 27 | aux env (App f e) = aux env f ++ aux env e 28 | 29 | 在上面这个程序里,我们定义了一个辅助函数 `aux` 来遍历整个λ项的结构,并把其中所有自由变元作为一个列表返回。这有点类似我们在第三章里学习的 `show` 函数,同样是基于归纳法的递归函数。与 `show` 函数不同,`aux` 还传递了一个 `env` 变量,用于记录已经被绑定的变元们,当然这相对于当前遍历位置而言的。我们可以看到,在 `Var` 分支的时候,只要是没有被当前的 `env` 记录,就被作为自由变元返回;而在 `Lam` 分支里,当前抽象结构所引入的行参,被加入到 `env` 里面去遍历下一层的λ项;对 `App` 分支的处理,则是简单地把两个子项的自由变元合并返回,这里用的是 `++` 列表衔接,所以最终结果可能会有同一个变元多次出现,如果要避免这种情况,可以改用 `union` 函数,它是 Prelude 里面给出的并集操作。 30 | 31 | 另外值得一提的是,我们用于记录绑定变元集合的变元取名叫 `env`,意指环境,而不是用状态(state)命名。这是一种在函数编程中约定俗成的风格,环境随递归改变,但环境的改变仅影响关于子项的递归;状态也随递归改变,但状态的改变在当前的递归返回后依然有效。因此状态不能仅仅作为参数传递,还必须作为返回值的一部分从子递归返回到当前的层级。能够正确区分和实现一个函数所需的环境和状态,是函数编程当中重要的一环,也是必不可少的训练。 32 | 33 | 有了变元绑定和自由的概念,接下来我们将引入一个概念叫做α变换(α-conversion)。它是对λ项进行如下操作,把某个形参以及所有它绑定的变元全都替换成一个“新”的变元。比如 `(λx.y x) x` 就可以通过α变换得到 `(λz.y z) x`,这个变换的过程简写为 `(λx.y x) => (λz. y z) x`,其中 `=>` 代表做一次α变换。 α变换不改变λ项代表的意思,这也叫做α等价(α-equivalence),这是我们接触到的第一条关于λ项的等价关系。 34 | 35 | α变换涉及到一个相关的操作,就是如何对一个λ项里面某个自由变元做替换操作。在这个操作过程中,我们只要注意不要把被绑定的变元替换掉就好了: 36 | 37 | subV :: Var -> Var -> Term -> Term 38 | subV u w (Var v) | v == u = Var w 39 | | otherwise = Var v 40 | subV u w (Lam v e) | v == u = Lam v e 41 | | otherwise = Lam v (subV u w e) 42 | subV u w (App f e) = App (subV u w f) 43 | (subV u w e) 44 | 45 | 同样,`subV` 是一个基于归纳法的递归操作,在遇到 `Lam` 分支时,如果需要替换的变元 `u` 与此时被引入的型参 `v` 同名,那么根本不需要对子项进行替换操作,因为子项里的同名变元都被当前的λ绑定了。 46 | 47 | 最后,我们回到公理语义上来,我们可以把α等价当做是一条公理,这样我们就有了判别等价关系的依据。又或者我们先给出等价关系的定义,然后试图证明α变换满足等价关系。常见的等价关系(用 `=` 表示),就是满足三个条件,自反 reflexive(`M=M`),对称symmetric(可以由 `M=N` 推导出 `N=M`),传递transitive(可以由 `L=M` 和 `M=N` 推导出 `L=N`)。α变换则可以被证明满足这三个条件,从而也成为一种等价关系。 48 | 49 | α等价的一个引申意义是说,λ项中变元的命名不重要,重要的是它的绑定关系。绝大多数的结构化现代程序语言,都符合这个规律,但鲜少有语言正式给出相应的规则和定义。我们通过学习λ演算,正是要借由公理语义,等价关系,以及α变换,来厘清其中的来龙去脉。 50 | 51 | #习题 52 | 53 | 一、判断以下λ项中,从左往右第一个抽象里的形参都绑定了哪些变元?请用引号标出,比如 `(λx.y "x") x`。 54 | 55 | (λf.λx.f x) f x 56 | (λx.x) λy.x λx.y x 57 | λx.x λx.(λx.x) x 58 | (λx.y z λz.y x z) x y z 59 | 60 | 二、判断以下λ项中,哪些变元是自由的?请用引号标出,比如 `(λx."y" x) "x"`。 61 | 62 | (λf.λx.f x) f x 63 | (λx.x) λy.x λx.y x 64 | λx.x λx.(λx.x) x 65 | (λx.y z λz.y x z) x y z 66 | 67 | 三、判断以下的λ项是否为α等价?实现一个函数 `alphaEq :: Term -> Term -> Bool` 来完成这件事。 68 | 69 | λx.λx.x 和 λy.λx.y 70 | λf.λx.f (λy.f x) 和 λf.λy.f (λy.f x) 71 | λx.λy.x y 和 λy.λx.y x 72 | λx.x λx.(λx.x) x 和 λx.x λy.(λx.x) y 73 | 74 | 四、在本章给出的α变换的定义中,我们注意到有一个条件就是要替换为“新”(也即原λ项中没有出现过)的变元,实际上这个条件其实过于严格(充分非必要),什么才是一个恰到好处的(充要)条件呢? 75 | 76 | -------------------------------------------------------------------------------- /tutorial/zh_cn/08-beta-reduction.md: -------------------------------------------------------------------------------- 1 | #手把手教你做λ(八)替换与归约 2 | 3 | 在上一章我们通过引入α变换学习了关于λ表达式的等价关系,准确说,是等价关系之一。但是α变换本身仅仅是替换了变元的名字,并没有改变一个λ表达式的结构,或者变元的绑定关系。如果在不改变变元绑定关系的前提下,还想改变结构的话,那么就需要把“替换(substitution)”的操作做一下扩展。 4 | 5 | 在正式定义这个广义上的替换操作前,我们先来看几个例子: 6 | 7 | x [ x := λy.y ] 8 | = λy.y 9 | 10 | (λx.z x) [ x := λy.y ] 11 | = λx.z x 12 | 13 | (x (λz.x)) [ x := (λy.y) z ] 14 | = (x (λa.x)) [ x := (λy.y) z ] 15 | = ((λy.y) z) (λa.(λy.y) z) 16 | 17 | 首先需要解释一下这里用到的标记方法(notation),像 `_[_:=_]` 这样的写法通常被用来定义多元函数。广为人知的二元表达式分为前缀(`+ 1 2`)、中缀(`1 + 2`)、后缀(`1 2 +`)三种写法,但它们在多元的情况则不再适用。像 `E[x:=e]` 这种写法,所表达的是把某个(被定义的)函数,作用于三个参数:`E, x, e`。既然我们是讨论替换操作,它的意思是将λ表达式 `E` 里面的所有名叫 `x` 的自由变元,替换成表达式 `e`。如果写成 `_[_:=_]` 的话,其中下划线表示的是参数缺失,也就是有待填入具体参数,而写成 `E[x:=e]` 的话则是参数齐全的表达式了。写成 `_[_:=_]` 其实也可以看作是这个函数的名字,就像三角函数 `sin`,`cos` 这样的名字,只不过是用符号,而不是英文来表示。 18 | 19 | 第一个例子 `x [ x := λy.y ]`,是将 `x` 替换成 `λy.y`。既然原λ项是单个变元 `x`,那么替换结果就是 `λy.y`。 20 | 21 | 第二个例子 `(λx.z x) [ x := λy.y ]`,也是要把 `x` 替换成 `λy.y`。但在原λ项里出现的两个 `x`,第一个是形参,第二个是被这个形参绑定的,非自由变元。所以这里并没有可被替换的自由变元,结果是维持原λ项不变。 22 | 23 | 第三个例子 `(x (λz.x)) [ x := (λy.y) z ]`,是要把 `x` 替换成 `(λy.y) z`。值得注意的是,替换项里的 `z` 是自由变元,如果直接替换到 `λz.x` 里面成为 `λz.(λy.y) z` 的话,那么替换项里的自由变元 `z` 将被原λ项里面的 `λz` 绑定,不再自由。为了避免这种情况的发生,我们可以先把 `(x (λz.x))` 做一个α变换,写成 `(x (λa.x))`,接下来再替换 `x` 就没问题了。 24 | 25 | 从这三个例子里面可以看到,做替换操作的主要原则就是不要改变原λ项和替换项里面的变元绑定关系,这个原则在α变换中已经得到了体现(见前一章的 `subV` 函数),而在扩展后的替换操作中则需要更加小心的对待。用 Haskell 实现如下: 26 | 27 | subT :: Var -> Term -> Term -> Term 28 | subT u w (Var v) | v == u = w 29 | | otherwise = Var v 30 | subT u w (App f e) = App (subT u w f) 31 | (subT u w e) 32 | subT u w (Lam v e) 33 | | v == u = Lam v e 34 | | v `notElem` fvsW = Lam v (subT u w e) 35 | | otherwise = Lam x (subT u w (subV v x e)) 36 | where 37 | bvsE = boundVars e 38 | fvsE = freeVars e 39 | fvsW = freeVars w 40 | x = freshVar (bvsE ++ fvsE ++ fvsW) 41 | 42 | 我们比较一下这个 `subT` 和前一章实现的 `subV` 函数,会发现: 43 | 44 | 1. 首先是它们的函数类型不同,因为 `subT` 是要把 `Var` 替换成 `Term`。 45 | 2. 其次,它们在对 `Term` 这个代数类型的三个分支的处理上很类似,主要的差别是当 `Lam v e` 里面的 `v` 不等于 `u` 的时候,如何处理关于子项 `e` 的递归替换: 46 | a. 我们先做一个条件检查,当 `w` 的自由变元中不包括 `v` 时,不存在原本自由的变元被意外绑定的情况,所以可以直接递归处理子项 `e`。 47 | b. 而当意外绑定有可能发生时,则需要对 `Lam v e` 做α变换,用一个新变元 `x` 来替代 `v`。选取这样一个 `x` 需要满足以下两个条件: 48 | i. `x` 对于 `e` 来说是一个“新”的不曾出现过的变元,这个条件和α变换所需的条件是一样的; 49 | ii. `w` 的自由变元不包括 `x`,以避免在 `w` 的自由变元被这里α变换时引入的 `x` 意外绑定。 50 | 51 | 鉴于前文已经给出了 `freeVars` 函数的实现,另外两个辅助函数 `boundVars` 和 `freshVar` 就留待读者自行定义(见习题一)。 52 | 53 | 在替换操作的基础上,λ演算定义了一个化简λ表达式的方法,叫做β规约(β-reduction): 54 | 55 | (λx.e) e' => e [ x := e' ] 56 | 57 | 这里我们用的符号 `=>` 代表规约关系。比如 `A=>B` 代表的就是λ项 `A` 可以β规约成为λ项 `B`。所有满足 `((λ_._) _)` 这种结构的λ项都可以被规约,我们把它们叫做 redex(reducible expression)。一个复杂的λ项可能有多个子项符合这个结构,也就是有多个 redex。而当一个λ项当中不存在任何 redex 的时候,它被叫做范式(Normal Form)。换句话说,范式就是(所有子项都)不可以继续被规约的λ项。 58 | 59 | β规约讲的是这样一个规则:当一个函数作用于它的参数时,原表达式等价于在函数体里将所有行参(parameter)出现的地方都替换为实参(argument)。有中学代数基础的朋友会说:“这不就是把参数代入到函数里去吗?搞这么复杂干嘛?” 没错,道理是相通的。直觉有助于理解,但只有严格的定义才能铺下牢靠的基石,奠定新的起点。中学的代数显然不曾考虑过高阶函数,人们自然也没有机会深入思考这背后隐藏的秘密,直到 Alonzo Church 把相关的规则做了形式化的整理,在这个基础上才发展出了一整套λ演算的理论。 60 | 61 | 那么β规约是否和α变换一样是一种等价关系呢?如果我们套用最常见的等价的定义,就会发现β规约并不满足自反/对称/传递这三个条件,比如 `A => B` 并不能推导出 `B => A`。所以通常的做法是直接把β规约当作是一条公理,也即β等价(β-equivalence): 62 | 63 | (λx.e) e' = e [ x := e' ] 64 | 65 | 建立在α和β这两个等价关系上的λ演算满足自恰性(soundness),换句话说,在这套公理系统中不存在相互矛盾的结论。而满足自恰性几乎是一切有意义的公理理论的前提。是的,这一切仅仅是一个起点。 66 | 67 | 68 | #习题 69 | 70 | 71 | 一、实现本章节提到的 Haskell 函数 `boundVar` 和 `freshVar`。使得 `boundVar e` 返回 `e` 中所有被绑定的变元,`freshVar fvs` 返回一个不属于 `fvs` 列表的任意变元。 72 | 73 | boundVars :: Term -> [Var] 74 | freshVar :: [Var] -> Var 75 | 76 | 二、以下λ项各有多少个 β-redex? 这些λ项都可以被规约到范式吗?如果可以,那么范式分别是什么? 77 | 78 | f (λx.x y) 79 | λy.(λf.x x) y 80 | (λx.x x)(λx.x x) 81 | (λx.(λy.λz.z y) x) p (λx.x) 82 | (λf.λx.f (f x)) (λf.λx.f (f x)) f x 83 | 84 | 三、实现下面这个 Haskell 函数,返回λ项中所有满足 redex 条件的子项;如果不存在 redex,则返回空表。 85 | 86 | redexes :: Term -> [Term] 87 | 88 | 四、对一个λ项 `M` 或其子项进行零或多次β归约得到 `N`,可以简写为 `M =>* N`。请实现以下 Haskell 函数,对给定的λ项进行多次规约,直到结果不存在 redex 为止,返回最终的λ项。 89 | 90 | reduce :: Term -> Term 91 | 92 | 五、在实现 redexes 和 reduce 这两个函数的过程中,有什么通用的代码是可以复用的?能做得更好吗? 93 | 94 | -------------------------------------------------------------------------------- /tutorial/zh_cn/Makefile: -------------------------------------------------------------------------------- 1 | # A simple makefile that generates image to post on weibo.com. 2 | # It requires Mac OS X, and the following tools: webkit2png, 3 | # pandoc, and netpbm. 4 | 5 | SRCS=00-preface.md \ 6 | 01-lambda-expression.md \ 7 | 02-data-type.md \ 8 | 03-induction.md \ 9 | 04-parser.md \ 10 | 05-parser-combinator.md \ 11 | 06-lambda-parser.md \ 12 | 07-alpha-conversion.md \ 13 | 08-beta-reduction.md 14 | 15 | default: 16 | @echo Use 'make .open' or 'make .png' 17 | 18 | %.open: %.png 19 | open $< 20 | 21 | %.html: %.md tutorial.css 22 | pandoc -s -c tutorial.css $< > $@ 23 | 24 | %.png-full.png: %.html 25 | webkit2png.py -o $(<:.html=.png) file://`pwd`/$< 26 | 27 | %.png: %.png-full.png 28 | pngtopnm $< |pnmcrop|pnmcrop -bottom -margin=30|pnmtopng > $@ 29 | 30 | clean: 31 | @rm -f $(patsubst %.md, %.png, $(SRCS)) \ 32 | $(patsubst %.md, %.png-full.png, $(SRCS)) \ 33 | $(patsubst %.md, %.png-clipped.png, $(SRCS)) \ 34 | $(patsubst %.md, %.png-thumb.png, $(SRCS)) 35 | 36 | .SECONDARY: $(patsubst %.md, %.png, $(SRCS)) 37 | 38 | .PHONY: clean 39 | 40 | -------------------------------------------------------------------------------- /tutorial/zh_cn/tutorial.css: -------------------------------------------------------------------------------- 1 | h1 { font-size: 11pt; } 2 | body { width: 440px; font-family: Optima, Hei; } 3 | code { font-size: 10.5pt; font-family: Monaco; } 4 | blockquote { margin-left: 10px; color: darkblue; } 5 | ol { padding-left: 1em; } 6 | --------------------------------------------------------------------------------