NEU-CS4400-编程语言讲义-全-
NEU CS4400 编程语言讲义(全)
CS4400/CS5400 导论,周二,1 月 10 日
-
课程进展的一般计划。
-
行政事务。(网页上的大部分内容。)
http://pl.barzilay.org/
编程语言简介,周二,1 月 10 日
PLAI §1
-
为什么我们要关心编程语言?(有没有一些大型项目 没有 一点语言?)
-
什么定义了一种语言?
-
语法
-
语义
-
库
-
习语
-
-
这些各自有多重要?
-
库为您提供运行时支持,不是语言本身的重要部分。(顺便说一句,“库”和“语言的一部分”之间的界线并不像看起来那么明显。)
-
习语源自语言设计和文化。 它们经常具有误导性。 例如,JavaScript 程序员经常会写:
function explorer_move() { doThis(); }function mozilla_move() { doThat(); }if (isExplorer) document.onmousemove = explorer_move;else document.onmousemove = mozilla_move;或
if (isExplorer) document.onmousemove = function() { doThis(); };else document.onmousemove = function() { doThat(); };或
document.onmousemove = isExplorer ? function() { ... } : function() { ... };或
document.onmousemove = isExplorer ? () => { doThis(); } : () => { doThat(); };或
document.onmousemove = isExplorer ? doThis : doThat;多少 JavaScript 程序员会知道这个做什么:
function foo(n) { return function(m) { return m+n; };}或这些:
n => m => m+n;(x,y) => s => s(x,y);(真实例子)
-
比较:
-
a[25]+5(Java:异常) -
(+ (vector-ref a 25) 5)(Racket:异常) -
a[25]+5(JavaScript:异常(或 NaN)) -
a[25]+5(Python:异常) -
$a[25]+5(Perl:5) -
a[25]+5(C:BOOM)
-> 语法主要是外观上的东西;语义才是真正的东西。
-
-
-
我们应该如何谈论语义?
-
一些众所周知的语义形式主义。
-
我们将使用程序来解释语义:最好的解释 就是 一个程序。
-
忽略可能的循环问题(但要意识到它们)。 (实际上,它们已经解决了:Scheme 有一个可以被视为从 Scheme 到逻辑的翻译的正式解释,这意味着我们写的东西可以被翻译成逻辑。)
-
我们会使用 Racket 出于许多原因(语法,函数式,实用,简单,形式化,静态类型,环境)。
-
Racket 简介周二,1 月 10 日
-
Racket 的各个部分的一般布局:
-
Racket 语言(大部分)属于 Scheme 家族,或更广泛地属于 Lisp 家族;
-
Racket:核心语言实现(语言和运行时),用 C 编写;
-
在 Racket 中可用的实际语言有很多额外部分是用 Racket 本身实现的;
-
GRacket:一个便携式的 Racket GUI 扩展,也是用 Racket 编写的;
-
DrRacket:一个 GRacket 应用程序(也是用 Racket 编写的);
-
我们的语言……
-
-
文档:Racket 文档是你的朋友(但要注意,有些内容在不同的地方以不同的形式提供)。
旁注:“Goto 语句被认为有害”周二,1 月 10 日
对“Goto 语句被认为有害”的评论,作者是 E.W. 迪克斯特拉
本文试图说服我们,众所周知的
goto语句应该从我们的编程语言中删除,或者至少(因为我认为它永远不会被删除),程序员不应该使用它。不清楚应该用什么来替代它。本文没有解释没有了goto如何使用if语句来重定向执行流的用途:我们的所有后置条件应该由一个单独的语句组成吗,或者我们应该只使用算术if,它不包含令人反感的goto?如果在执行完一个分支后,程序需要在其他地方继续执行,那么应该如何处理?
作者是所谓的“结构化编程”风格的支持者,如果我理解正确,
goto被缩进代替。结构化编程是一个很好的学术练习,在小例子中效果很好,但我怀疑任何真实世界的程序都会以这种方式编写。超过 10 年的 Fortran 工业经验已经明确向所有相关方证明,在现实世界中,goto是有用且必要的:它的存在可能会在调试中造成一些不便,但它是事实上的标准,我们必须接受它。要从我们的语言中删除它,需要更多比纯粹主义者的学术研究更多的东西。发布这篇文章会浪费宝贵的纸张:如果要发布,我确信它会被无人引用和注意到,就像我确信,30 年后,
goto语句仍然会活跃在编程中,并且像今天一样被广泛使用。对编辑的机密评论:作者应该撤回这篇文章,并将其提交到不会进行同行评审的地方。写一封给编辑的信将是一个完美的选择:在那里没有人会注意到它!
Racket 简介星期二,1 月 10 日
Racket 语法:类似于其他基于 S 表达式的语言。
提醒:括号可以与 C 等函数调用的括号进行比较 — 它们总是表示应用了某个函数。这就是为什么(+ (1) (2))不起作用的原因:如果你使用 C 语法,那就是+(1(), 2()),但1不是一个函数,所以1()是一个错误。
语法和语义之间的一个重要区别。一个好的思考方式是,存储在某个文件中的字符串42(两个 ASCII 值)与存储在内存中的数字42(以某种表示形式)之间的区别。你也可以继续上面的例子:“谋杀”没有错 — 它只是一个词,但谋杀是会让你入狱的事情。Racket 使用的求值函数实际上是一个接受一段语法并返回(或执行)其语义的函数。
define表达式用于创建新的绑定,不要试图使用它们来改变值。例如,你不应该尝试写类似(define x (+ x 1))的东西来模仿x = x+1。这不会起作用。
Racket 内置了两个布尔值:#t(真)和#f(假)。它们可以在if语句中使用,例如:
(if (< 2 3) 10 20) --> 10
因为(< 2 3)评估为#t。事实上,任何值除了#f都被认为是真的,所以:
(if 0 1 2) --> 1 ; all of these are "truthy"(if "false" 1 2) --> 1(if "" 1 2) --> 1(if null 1 2) --> 1(if #t 1 2) --> 1 ; including the true value(if #f 1 2) --> 2 ; the only false value(if #false 1 2) --> 2 ; another way to write it(if false 1 2) --> 2 ; also false since it's bound to #f
注意:Racket 是一种函数式语言 — 所以一切都有一个值。
这意味着表达式
(if test consequent)
当test评估为#f时没有意义。这与 Pascal/C 不同,那里的语句会执行一些操作(副作用),比如打印或赋值 — 如果测试为假,那么if语句就什么也不做… 然而,Racket 必须返回一些值 — 它可以决定简单地返回#f(或某个未指定的值)作为
(if #f something)
一些实现可能会这样做,但 Racket 会声明它为语法错误。(正如我们将在未来看到的,Racket 有一个更方便的when,具有更清晰的意图。)
嗯,几乎所有东西都是一个值…
有一些东西是 Racket 语法的一部分 — 例如if和define是特殊形式,它们没有值!稍后会详细介绍更多。
(总结���与其他语言相比,有更多的东西具有值。)
cond用于if … else if … else if … else …序列。问题在于嵌套的if很不方便。例如,
(define (digit-num n) (if (<= n 9) 1 (if (<= n 99) 2 (if (<= n 999) 3 (if (<= n 9999) 4 "a lot")))))
在 C/Java/其他语言中,你会写:
function digit_num(n) { if (n <= 9) return 1; else if (n <= 99) return 2; else if (n <= 999) return 3; else if (n <= 9999) return 4; else return "a lot";}
(旁边的问题:为什么 Racket 中没有return语句?)
但试图强迫 Racket 代码看起来相似:
(define (digit-num n) (if (<= n 9) 1 (if (<= n 99) 2 (if (<= n 999) 3 (if (<= n 9999) 4 "a lot")))))
不仅仅是品味问题 — 缩进规则是有原因的,主要原因是你可以一眼看出程序的结构,而在上面的代码中这不再成立。(这样的代码将受到惩罚!)
因此,我们可以使用 Racket 的cond语句,像这样:
(define (digit-num n) (cond [(<= n 9) 1] [(<= n 99) 2] [(<= n 999) 3] [(<= n 9999) 4] [else "a lot"]))
注意else是cond形式使用的关键字——你应该总是使用一个else子句(出于类似于if的原因,在那里避免额外的表达式评估,并且当我们使用类型语言时,我们将需要它)。另请注意,方括号被 DrRacket 读取为圆括号,它只会确保括号配对匹配。我们使用这个来使代码更易读——具体来说,上面使用[]与常规使用()之间有一个重大区别。你能看出来是什么吗?
cond的一般结构:
(cond [test-1 expr-1] [test-2 expr-2] ... [test-n expr-n] [else else-expr])
使用if表达式和递归函数的示例:
(define (fact n) (if (zero? n) 1 (* n (fact (- n 1)))))
使用这个来展示不同的工具,特别是:
-
无法使用的特殊对象
-
语法检查器
-
步进器
-
提交工具(安装、注册和提交)
将其转换为尾递归形式的示例:
(define (helper n acc) (if (zero? n) acc (helper (- n 1) (* acc n))))(define (fact n) (helper n 1))
有关作业提交的附加说明:
-
每个函数都应以清晰的文档开头:包括一个目的声明和其类型。
-
根据需要记录函数,并按照上述指南和样式指南进行。
-
在函数之后,总是有几个测试案例——它们应该涵盖你的完整代码(确保包括可能的边界情况)。后来,我们将转而通过它的“公共接口”测试整个文件,而不是测试每个函数。
列表与递归星期二,1 月 10 日
列表是 Racket 的基本数据类型。
列表被定义为:
-
空列表(
null、empty或'()), -
任何东西和一个列表的一对(
cons细胞)。
尽管这看起来很简单,但它给了我们精确的 形式 规则,以证明某些东西是一个列表。
- 为什么第一个规则中有一个“the”?
例子:
null(cons 1 null)(cons 1 (cons 2 (cons 3 null)))(list 1 2 3) ; a more convenient function to get the above
列表操作 — 断言:
null? ; true only for the empty listpair? ; true for any cons celllist? ; this can be defined using the above
我们可以根据上述规则推导出list?:
(define (list? x) (if (null? x) #t (and (pair? x) (list? (rest x)))))(define (list? x) (or (null? x) (and (pair? x) (list? (rest x)))))
但为什么我们不能更简单地定义list?为
(define (list? x) (or (null? x) (pair? x)))
上述定义与正确定义之间的区别可以在完整的 Racket 语言中观察到,而不是在学生语言中(其中尾部没有包含非列表值的对)。
列表操作 — 对成对(cons 细胞)的解构器:
firstrest
传统上称为car、cdr。
此外,任何由最多四个a和/或d组成的c<x>r组合 — 我们可能不会使用比cadr、caddr等更多。
包含列表的递归函数的示例:
(define (list-length list) (if (null? list) 0 (+ 1 (list-length (rest list)))))
使用不同的工具,尤其是:
-
语法检查器
-
步进器
为什么我们可以将list用作参数 — 使用语法检查器
(define (list-length-helper list len) (if (null? list) len (list-length-helper (rest list) (+ len 1))))(define (list-length list) (list-length-helper list 0))
主要思想:列表是一个递归结构,所以操作列表的函数应该是遵循列表的递归定义的递归函数。
列表函数的另一个例子 — 对数字列表求和
(define (sum-list l) (if (null? l) 0 (+ (first l) (sum-list (rest l)))))
还展示如何使用这个指南实现rcons。
更多例子:
定义reverse — 使用rcons解决问题。
rcons可以被泛化为非常有用的东西:append。
-
我们如何使用
append而不是rcons? -
这需要多长时间?我们使用
append还是rcons有关系吗?
使用尾递归重新定义reverse。
- 结果更复杂吗?(是的,但不会太糟糕,因为它按相反顺序收集元素。)
一些样式周二,1 月 10 日
当你有一些常见的值需要在几个地方使用时,复制它是不好的。例如:
(define (how-many a b c) (cond [(> (* b b) (* 4 a c)) 2] [(= (* b b) (* 4 a c)) 1] [(< (* b b) (* 4 a c)) 0]))
它有什么不好之处?
-
它比必要的要长,这最终会使你的代码不太可读。
-
它更慢 —— 当你到达最后一个情况时,你已经评估了两个序列三次。
-
它更容易出错 —— 上面的代码足够短,但如果它更长,以至于你看不到同一页上的三个出现?当你在几个月后调试代码时,你会记得修复所有地方吗?
一般来说,使用名称的能力可能是计算机科学中最基本的概念 —— 让计算机程序成为它们所是的事实。
我们已经有了一个命名值的设施:函数参数。我们可以像这样将上面的函数分成两个:
(define (how-many-helper b² 4ac) ; note: valid names! (cond [(> b² 4ac) 2] [(= b² 4ac) 1] [else 0]))(define (how-many a b c) (how-many-helper (* b b) (* 4 a c)))
但是,与为其名称想出一个新函数的笨拙解决方案不同,我们有一个绑定局部名称的设施 —— let。一般来说,let 特殊形式的语法是
(let ([id expr] ...) expr)
例如,
(let ([x 1] [y 2]) (+ x y))
但请注意,绑定是“并行”进行的,例如,试试这个:
(let ([x 1] [y 2]) (let ([x y] [y x]) (list x y)))
使用这个来解决上面的问题:
(define (how-many a b c) (let ([b² (* b b)] [4ac (* 4 a c)]) (cond [(> b² 4ac) 2] [(= b² 4ac) 1] [else 0])))
关于编写代码的一些注意事项(也请参阅手册部分的风格指南)
代码质量将在这门课程中得到评分!
-
尽可能使用抽象,如上所述。这是不好的:
(define (how-many a b c) (cond [(> (* b b) (* 4 a c)) 2] [(= (* b b) (* 4 a c)) 1] [(< (* b b) (* 4 a c)) 0]))(define (what-kind a b c) (cond [(= a 0) 'degenerate] [(> (* b b) (* 4 a c)) 'two] [(= (* b b) (* 4 a c)) 'one] [(< (* b b) (* 4 a c)) 'none])) -
但不要过度抽象:
(define one 1)或者(define two "two") -
总是进行测试用例(显示覆盖工具),你可能想对它们进行注释,但你应该始终确保你的代码能够正常工作。
-
不要文档不足,但也不要文档过多。
-
缩进!(让 DrRacket 决定;适应它的规则) —> 这是上次提到的文化的一部分,但这样做是有充分理由的:几十年的编程经验表明这是最可读的格式。保持良好的缩进非常重要,因为所有 Lisp 的程序员都不统计括号 —— 他们看结构。
-
作为一般规则,
if应该是一行中的全部,或者第一个条件和每个结果在单独的一行上。对于define也是如此 —— 要么全部在一行上,要么在定义的对象之后换行(要么是标识符,要么是带有参数的标识符)。 -
另一个一般规则:在开放括号后面永远不要有空白,或者在关闭括号前面不要有空白(空白包括换行)。同样,在开放括号之前应该是另一个开放括号或者空白,关闭括号之后也是一样。
-
尽可能使用可用的工具:例如,使用
cond而不是嵌套的if(绝对不要强迫缩进使嵌套的if看起来像它的 C 对应物 —— 记得让 DrRacket 为你缩进)。另一个例子 —— 不要使用
(+ 1 (+ 2 3))而是使用(+ 1 2 3)(这可能在极其罕见的情况下需要,只有当你了解你的微积分知识并且对舍入误差有广泛的了解时)。另一个例子 — 不要使用
(cons 1 (cons 2 (cons 3 null)))来代替(list 1 2 3)。同样 — 不要写类似以下的内容:
(if (< x 100) #t #f)因为这与只是
(< x 100)还有一些这样的例子:
(if x #t y) --same-as--> (or x y)(if x y #f) --same-as--> (and x y)(if x #f #t) --same-as--> (not x)(实际上前两个几乎是相同的,例如,
(and 1 2)将返回2,而不是#t。) -
用这些作为许多这些问题的示例:
(define (interest x) (* x (cond [(and (> x 0) (<= x 1000)) 0.04] [(and (> x 1000) (<= x 5000)) 0.045] [else 0.05])))(define (how-many a b c) (cond ((> (* b b) (* (* 4 a) c)) 2) ((< (* b b) (* (* 4 a) c)) 0) (else 1)))(define (what-kind a b c) (if (equal? a 0) 'degenerate (if (equal? (how-many a b c) 0) 'zero (if (equal? (how-many a b c) 1) 'one 'two) ) ) )(define (interest deposit) (cond [(< deposit 0) "invalid deposit"] [(and (>= deposit 0) (<= deposit 1000)) (* deposit 1.04) ] [(and (> deposit 1000) (<= deposit 5000)) (* deposit 1.045)] [(> deposit 5000) (* deposit 1.05)]))(define (interest deposit) (if (< deposit 1001) (* 0.04 deposit) (if (< deposit 5001) (* 0.045 deposit) (* 0.05 deposit))))(define (what-kind a b c) (cond ((= 0 a) 'degenerate) (else (cond ((> (* b b)(*(* 4 a) c)) 'two) (else (cond ((= (* b b)(*(* 4 a) c)) 'one) (else 'none)))))));
一些样式(续)星期二,1 月 17 日
在 Racket 中我们可以将函数用作值的事实非常有用 — 例如,map,foldl 和 foldr,还有许多其他函数。
例子:
;; every? : (A -> Boolean) (Listof A) -> Boolean;; Returns false if any element of lst fails;; the given pred, true if all pass pred.(define (every? pred lst) (or (null? lst) (and (pred (first lst)) (every? pred (rest lst)))))
尾调用
通常你应该知道什么是尾调用,但这里是对这个主题的快速回顾。如果在调用时没有“记住”的上下文,那么函数调用被称为处于尾位置。非常粗略地说,这意味着不嵌套在另一个调用的参数表达式中的函数调用是尾调用。这个定义取决于上下文,例如,在一个表达式中
(if (huh?) (foo (add1 (* x 3))) (foo (/ x 2)))
两个对foo的调用都是尾调用,但它们是这个表达式的尾调用,因此适用于这个上下文。可能这段代码是在另一个调用内部,如
(blah (if (huh?) (foo (add1 (* x 3))) (foo (/ x 2))) something-else)
而foo调用现在不在尾位置。所有 Scheme 实现包括 Racket 在尾调用方面的主要特点是处于函数尾位置的调用被称为“消除”。这意味着如果我们在一个f函数中,并且我们即将在尾位置调用g,因此g返回的任何东西也将是f的结果,那么当 Racket 调用g时,它不会保留f的上下文 — 它不会记住它需要“返回”到f,而是直接返回给它的调用者。换句话说,当你考虑函数调用的传统实现为堆栈上的帧时,Racket 会在可以的时候摆脱一个堆栈帧。
通过使用 DrRacket 的 stepper 来逐步执行函数调用,可以另一种方式来看待这个问题。Stepper 通常是一种替代性调试器,它不是可视化堆栈帧,而是组装一个表示这些帧的表达式。现在,在尾调用的情况下,在这样的表示中没有空间来保留调用 — 而事实是,在 Racket 中这是完全可以的,因为这些调用不会保存在调用堆栈中。
请注意,这个特性有几个名称:
-
“尾递归”。这是一个常见的方式来指代仅将尾递归函数优化为循环的更有限的优化。在具有尾调用作为特性的语言中,这太有限了,因为它们还优化了互相递归的情况,或者任何尾调用的情况。
-
“尾调用优化”。在一些语言或更具体地说在一些编译器中,你会听到这个术语。当尾调用被认为只是一种“优化”时,这是可以接受的 — 但在 Racket(以及 Scheme)的情况下,它不仅仅是一种优化:它是一个语言特性,你可以依赖它。例如,像
(define (loop) (loop))这样的尾递归函数 必须 作为一个无限循环运行,而不仅仅是在编译器认为需要优化为一个循环时。 -
“尾调用消除”。这是迄今为止这个特性最常见的正确名称:它不仅仅是递归,也不是一种优化。
何时应该使用尾调用?
通常,了解尾调用的人们会尝试始终使用它们。这并不总是一个好主意。在考虑使用哪种风格时,你通常应该意识到其中的权衡。要记住的主要事情是,尾调用消除是一种有助于减少空间使用(栈空间)的属性——通常将其从线性空间减少到常数空间。这显然可以加快速度,但通常加速只是一个常数因子,因为无论如何你都需要做相同数量的迭代,所以你只是减少了在空间分配上花费的时间。
这里有一个我们见过的例子:
(define (list-length-1 list) (if (null? list) 0 (+ 1 (list-length-1 (rest list)))));; versus(define (list-length-helper list len) (if (null? list) len (list-length-helper (rest list) (+ len 1))))(define (list-length-2 list) (list-length-helper list 0))
在这种情况下,第一个(递归)版本消耗的空间与列表的长度成线性关系,而第二个版本只需要常数空间。但是,如果仅考虑渐近运行时间,它们都是 O(length(l))。
第二个例子是map的简单实现:
(define (map-1 f l) (if (null? l) l (cons (f (first l)) (map-1 f (rest l)))));; versus(define (map-helper f l acc) (if (null? l) (reverse acc) (map-helper f (rest l) (cons (f (first l)) acc))))(define (map-2 f l) (map-helper f l '()))
在这种情况下,渐近空间和运行时消耗都是相同的。在递归情况下,我们对栈空间有一个常数因子,而在迭代情况下(尾调用版本),我们也有一个类似的因子用于累积反转列表。在这种情况下,最好保留第一个版本,因为代码更简单。事实上,Racket 的栈空间管理可以使第一个版本运行速度比第二个版本更快,因此将其优化为第二个版本是没有用的。
类型注释,星期二,1 月 17 日
处理高阶函数时,类型会变得有趣。例如,map接收一个函数和某种类型的列表,并将该函数应用于此列表以累积其输出,因此其类型是:
;; map : (A -> B) (Listof A) -> (Listof B)
实际上,map可以使用多个列表,它将在所有列表的第一个元素上应用函数,然后是第二个元素,依此类推。因此,具有两个列表的map的类型可以描述为:
;; map : (A B -> C) (Listof A) (Listof B) -> (Listof C)
这是一个复杂的例子 —— 这个函数的类型是什么:
(define (foo x y) (map map x y))
从我们知道的开始 —— 两个map,称它们为map1和map2,具有map的双列表和单列表类型,这里它们是,类型名称不同:
;; the first `map', consumes a function and two listsmap1 : (A B -> C) (Listof A) (Listof B) -> (Listof C);; the second `map', consumes a function and one listmap2 : (X -> Y) (Listof X) -> (Listof Y)
现在,我们知道map2是map1的第一个参数,因此map1的第一个参数的类型应该是map2的类型:
(A B -> C) = (X -> Y) (Listof X) -> (Listof Y)
从这里我们可以得出结论
A = (X -> Y)B = (Listof X)C = (Listof Y)
如果我们在map1的类型中使用这些方程式,我们会得到:
map1 : ((X -> Y) (Listof X) -> (Listof Y)) (Listof (X -> Y)) (Listof (Listof X)) -> (Listof (Listof Y))
现在,foo的两个参数是map1的第 2 个和第 3 个参数,其结果是map1的结果,因此我们现在可以写出foo的类型:
;; foo : (Listof (X -> Y));; (Listof (Listof X));; -> (Listof (Listof Y))(define (foo x y) (map map x y))
这应该可以帮助你理解为什么,例如,这会导致类型错误:
(foo (list add1 sub1 add1) (list 1 2 3))
以及为什么这是有效的:
(foo (list add1 sub1 add1) (map list (list 1 2 3)))
附注:名称很重要,星期二,1 月 17 日
计算机科学中的一个重要“发现”是,我们不需要为每个中间子表达式命名 —— 例如,在几乎任何语言中,我们都可以写出等价的代码:
s = (-b + sqrt(b² - 4*a*c)) / (2*a)
而不是
x = b * by = 4 * ay = y * cx = x - yx = sqrt(x)y = -bx = y + xy = 2 * as = x / y
这些语言与汇编语言形成对比,并且全部都被归类为“高级语言”。
(这是一个有趣的想法 —— 为什么不对函数值做同样的事情呢?)
BNF,语法,AE 语言
回到课程的主题:我们想要研究编程语言,并且我们想要使用一种编程语言来做到这一点。
设计语言的第一步是规定语言。为此我们使用 BNF(巴科斯-诺尔形式)。例如,这是一个简单算术语言的定义:
<AE> ::= <num> | <AE> + <AE> | <AE> - <AE>
解释不同的部分。具体来说,这是具有解析的低级(具体)语法定义的混合体。
我们使用这个来推导某种语言中的表达式。我们从<AE>开始,它应该是这些中的一个:
-
一个数字
<num> -
一个
<AE>,文本“+”,和另一个<AE> -
相同的但是有“
-”
<num> 是一个终结符:当我们在推导中到达它时,我们就完成了。<AE> 是一个非终结符:当我们到达它时,我们必须继续其中的一个选项。应该清楚的是,“+”和“-”是我们期望在输入中找到的东西 —— 因为它们没有被包裹在<>中。
我们可以指定<num>是什么(将其转换为<NUM>非终结符):
<AE> ::= <NUM> | <AE> + <AE> | <AE> - <AE><NUM> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <NUM> <NUM>
但我们没有 —— 为什么?因为在 Racket 中我们有数字作为原语,我们想要使用 Racket 来实现我们的语言。这样会方便很多,并且我们会得到免费的东西,比如浮点数、有理数等等。
要正式使用 BNF,例如,证明1-2+3是一个有效的<AE>表达式,我们首先对规则进行标记:
<AE> ::= <num> (1) | <AE> + <AE> (2) | <AE> - <AE> (3)
然后我们可以使用它们作为每个推导步骤的正式理由:
<AE><AE> + <AE> ; (2)<AE> + <num> ; (1)<AE> - <AE> + <num> ; (3)<AE> - <AE> + 3 ; (num)<num> - <AE> + 3 ; (1)<num> - <num> + 3 ; (1)1 - <num> + 3 ; (num)1 - 2 + 3 ; (num)
这将是做这件事情的一种方式。或者,我们可以使用树来可视化推导,用节点上使用的规则。
这些规范存在歧义:一个表达式可以通过多种方式推导。甚至一个数字的小语法也是模糊的 —— 像123这样的数字可以通过两种方式推导,结果看起来不同的树。现在这种歧义并不是一个“真正”的问题,但很快就会成为一个问题。我们想要消除这种歧义,使得推导所有表达式都有一个单一(=确定性)的方式。
解决这个问题有一种标准方法 —— 我们向定义中添加另一个非终结符,并确保每个规则只能继续到其备选项之一。例如,这是我们可以对数字做的:
<NUM> ::= <DIGIT> | <DIGIT> <NUM><DIGIT> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
类似的解决方案可以应用于<AE> BNF — 我们要么限制推导的方式,要么提出新的非终结符来强制确定性推导树。
作为限制推导的示例,我们看一下当前的语法:
<AE> ::= <num> | <AE> + <AE> | <AE> - <AE>
而不是允许操作两侧都是<AE>,我们强制其中一个是数字:
<AE> ::= <num> | <num> + <AE> | <num> - <AE>
现在只有一种方式来推导任何表达式,它总是将操作与右边关联起来:像1+2+3这样的表达式只能推导为1+(2+3)。要将其改为左关联,我们会使用这个:
<AE> ::= <num> | <AE> + <num> | <AE> - <num>
但是如果我们想要强制优先级呢?假设我们的 AE 语法有加法和乘法:
<AE> ::= <num> | <AE> + <AE> | <AE> * <AE>
我们可以像上面那样做同样的事情并添加新的非终结符 —— 比如一个“因子”:
<AE> ::= <num> | <AE> + <AE> | <PROD><PROD> ::= <num> | <PROD> * <PROD>
现在,我们必须将任何 AE 表达式解析为乘法的加法(或数字)。首先,注意如果 <AE> 变为 <PROD>,而 <PROD> 变为 <num>,那么 <AE> 不需要变为 <num>,因此这是相同的语法:
<AE> ::= <AE> + <AE> | <PROD><PROD> ::= <num> | <PROD> * <PROD>
现在,如果我们仍然想要能够将加法相乘,我们可以强制它们出现在括号中:
<AE> ::= <AE> + <AE> | <PROD><PROD> ::= <num> | <PROD> * <PROD> | ( <AE> )
接下来,注意 <AE> 对于加法仍然存在歧义,可以通过强制加法的左侧是因子来解决:
<AE> ::= <PROD> + <AE> | <PROD><PROD> ::= <num> | <PROD> * <PROD> | ( <AE> )
我们仍然存在对乘法的歧义,所以我们做同样的事情,并为“原子”添加另一个非终端:
<AE> ::= <PROD> + <AE> | <PROD><PROD> ::= <ATOM> * <PROD> | <ATOM><ATOM> ::= <num> | ( <AE> )
你可以尝试推导几个表达式以确信推导现在始终是确定性的。
但正如你所见,这恰恰是我们想要避免的化妆品 — 它会导致我们可能感兴趣但与编程语言原理无关的东西。当我们拥有一个真正的语言而不是这样一个微小的语言时,情况也会变得更加糟糕。
是否有一个好的解决方案? — 答案就在我们眼前:做像 Racket 一样 — 总是使用完全括号表达式:
<AE> ::= <num> | ( <AE> + <AE> ) | ( <AE> - <AE> )
为了避免将 Racket 代码与我们语言中的代码混淆,我们还将括号改为花括号:
<AE> ::= <num> | { <AE> + <AE> } | { <AE> - <AE> }
但在 Racket 中 一切 都有一个值 — 包括那些 + 和 -,这使得这种情况极为方便,可以用未来可能具有比 2 更多或更少参数以及将这些算术运算符视为普通函数的操作。在我们的玩具语言中,最初我们不会这样做(即,+ 和 - 是二阶运算符:它们不能被用作值)。但由于我们以后会涉及到这一点,我们将采用 Racket 的解决方案,并使用完全括号的前缀表示法:
<AE> ::= <num> | { + <AE> <AE> } | { - <AE> <AE> }
(记住,在某种意义上,Racket 代码是以一种已解析的语法形式书写的…)
简单解析星期二,1 月 17 日
进入“解析器”的实现:
与语法实际看起来无关,我们希望尽快解析它 — 将具体语法转换为抽象语法树。
无论我们如何编写语法:
-
3+4(中缀), -
3 4 +(后缀), -
+(3,4)(带括号参数的前缀), -
(+ 3 4)(带括号的前缀),
我们总是指的是同一个抽象的东西 — 加上数字3和数字4。这本质上是一个树形结构,其中加法操作是根,两个数字是叶子。
通过正确的数据定义,我们可以在 Racket 中将其描述为表达式(Add (Num 3) (Num 4)),其中Add和Num是用于语法的树类型的构造函数,或者在类似 C 的语言中,它可能是类似Add(Num(3),Num(4))的东西。
同样,表达式(3-4)+7在 Racket 中将被描述为表达式:
(Add (Sub (Num 3) (Num 4)) (Num 7))
重要提示:“表达式”在上面以两种不同的方式使用 —— 每种方式对应不同的语言,评估第二个“表达式”的结果是一个代表第一个表达式的 Racket 值。
为了定义数据类型和必要的构造函数,我们将使用这个:
(define-type AE [Num Number] [Add AE AE] [Sub AE AE])
-
注意 —— Racket 遵循 Lisp 的传统,使语法问题几乎可以忽略不计 —— 我们使用的语言几乎就像我们直接使用解析树一样。实际上,这是一种非常简单的解析树语法,使得解析变得极其容易。
[这有一个有趣的历史原因... 一些 Lisp 历史 —— M-表达式 vs. S-表达式,以及我们编写的代码与 AST 同构的事实。稍后我们将看到通过这样做我们获得的一些优势。另请参阅“Lisp 的演变”,第 3.5.1 节(特别是最后一句)。]
为了使事情变得非常简单,我们将通过双层方法利用上述事��:
-
我们首先将我们的语言“解析”为一个中间表示 —— 一个 Racket 列表 —— 这主要是通过一个修改版的 Racket 的
read函数完成的,该函数使用大括号{}代替圆括号()。 -
然后,我们编写自己的
parse函数,将结果列表解析为AE类型的实例 —— 一棵抽象语法树(AST)。
这是通过以下简单的递归函数实现的:
(: parse-sexpr : Sexpr -> AE);; parses s-expressions into AEs(define (parse-sexpr sexpr) (cond [(number? sexpr) (Num sexpr)] [(and (list? sexpr) (= 3 (length sexpr))) (let ([make-node (match (first sexpr) ['+ Add] ['- Sub] [else (error 'parse-sexpr "unknown op: ~s" (first sexpr))]) #| the above is the same as: (cond [(equal? '+ (first sexpr)) Add] [(equal? '- (first sexpr)) Sub] [else (error 'parse-sexpr "unknown op: ~s" (first sexpr))]) |#]) (make-node (parse-sexpr (second sexpr)) (parse-sexpr (third sexpr))))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
这个函数非常简单,但随着我们的语言增长,它们会变得更冗长,更难写。因此,我们使用一个新的特殊形式:match,它匹配一个值并将新标识符绑定到不同的部分(尝试使用“检查语法”)。使用match重新编写上述代码:
(: parse-sexpr : Sexpr -> AE);; parses s-expressions into AEs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(list '+ left right) (Add (parse-sexpr left) (parse-sexpr right))] [(list '- left right) (Sub (parse-sexpr left) (parse-sexpr right))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
最后,为了使其更加统一,我们将结合将字符串解析为 sexpr 的函数,以便我们可以使用字符串表示我们的程序:
(: parse : String -> AE);; parses a string containing an AE expression to an AE(define (parse str) (parse-sexpr (string->sexpr str)))
match形式
match的语法是
(match value [pattern result-expr] ...)
将值与每个模式进行匹配,可能在此过程中绑定名称,如果模式匹配,则评估结果表达式。模式的最简单形式只是一个标识符——它总是匹配并绑定该标识符到值:
(match (list 1 2 3) [x x]) ; evaluates to the list
另一个简单的模式是引用的符号,它匹配该符号。例如:
(match foo ['x "yes"] [else "no"])
如果foo是符号x,则将计算为"yes",否则为"no"。请注意,在这里else不是关键字——它恰好是一个总是成功的模式,因此它的行为类似于一个 else 子句,只是它将else绑定到到目前为止未匹配的值。
许多模式看起来像函数应用程序 —— 但不要将它们与应用程序混淆。(list x y z)模式匹配恰好三个项目的列表并绑定三个标识符;或者如果“参数”本身是模式,match将会进入值并匹配它们。更具体地说,这意味着模式可以被嵌套:
(match (list 1 2 3) [(list x y z) (+ x y z)]) ; evaluates to 6(match '((1) (2) 3) [(list (list x) (list y) z) (+ x y z)]) ; also 6
还有一个cons模式,它匹配非空列表,然后将第一部分与列表的头部匹配,将第二部分与列表的尾部匹配。
在list模式中,你可以使用...来指定前一个模式重复零次或多次,并且绑定的名称将绑定到相应匹配的列表。一个简单的结果是(list hd tl ...)模式与(cons hd tl)完全相同,但能够重复任意模式非常有用:
(match '((1 2) (3 4) (5 6) (7 8)) [(list (list x y) ...) (append x y)]); evaluates to (1 3 5 7 2 4 6 8)
几个更有用的模式:
id -- matches anything, binds `id' to it_ -- matches anything, but does not bind(number: n) -- matches any number and binds it to `n'(symbol: s) -- same for symbols(string: s) -- strings(sexpr: s) -- S-expressions (needed sometimes for Typed Racket)(and pat1 pat2) -- matches both patterns(or pat1 pat2) -- matches either pattern (careful with bindings)
模式按顺序逐个尝试,并且如果没有模式匹配该值,则会引发错误。
注意,在list模式中,...可以跟随任何模式,包括上述所有模式,以及嵌套的列表模式。
以下是一些示例 — 您可以在定义窗口顶部使用#lang pl untyped尝试它们。这个:
(match x [(list (symbol: syms) ...) syms])
将x与只接受符号列表的模式进行匹配,并将syms绑定到这些符号。这里是一个匹配任意数量列表的列表的示例,其中每个子列表以一个符号开始,然后有任意数量的数字。请注意n和s绑定如何为所有符号列表和数字列表的列表获取值:
> (define (foo x) (match x [(list (list (symbol: s) (number: n) ...) ...) (list 'symbols: s 'numbers: n)]))> (foo (list (list 'x 1 2 3) (list 'y 4 5)))'(symbols: (x y) numbers: ((1 2 3) (4 5)))
这里是一个快速示例,说明了or与两个文字替代项一起使用的方式,and用于命名特定的数据片段,以及or与一个绑定一起使用的方式:
> (define (foo x) (match x [(list (or 1 2 3)) 'single] [(list (and x (list 1 _)) 2) x] [(or (list 1 x) (list 2 x)) x]))> (foo (list 3))'single> (foo (list (list 1 99) 2))'(1 99)> (foo (list 1 10))10> (foo (list 2 10))10
语义(=评估)Tuesday, January 17th
PLAI §2
回到 BNF——现在,意义。
这些 BNF 规范的一个重要特性:我们可以使用推导来指定含义(在我们的上下文中,含义是“运行”程序(或“解释”,“编译”,但我们将使用“评估”))。例如:
<AE> ::= <num> ; <AE> evaluates to the number | <AE1> + <AE2> ; <AE> evaluates to the sum of evaluating ; <AE1> and <AE2> | <AE1> - <AE2> ; ... the subtraction of <AE2> from <AE1> (... roughly!)
为了稍微正式一些:
a. eval(<num>) = <num> ;*** special rule: translate syntax to valueb. eval(<AE1> + <AE2>) = eval(<AE1>) + eval(<AE2>)c. eval(<AE1> - <AE2>) = eval(<AE1>) - eval(<AE2>)
注意两个+和-的完全不同的角色。事实上,写成这样可能更正确:
a. eval("<num>") = <num>b. eval("<AE1> + <AE2>") = eval("<AE1>") + eval("<AE2>")c. eval("<AE1> - <AE2>") = eval("<AE1>") - eval("<AE2>")
甚至使用一个标记来表示这些字符串中的元占位符:
a. eval("$<num>") = <num>b. eval("$<AE1> + $<AE2>") = eval("$<AE1>") + eval("$<AE2>")c. eval("$<AE1> - $<AE2>") = eval("$<AE1>") - eval("$<AE2>")
但我们将避免假装我们正在做那种字符串操作。(例如,这将需要说明返回<num>代表什么(涉及string->number),右侧的片段表示我们需要将这些指定为子字符串操作。)
注意我们的 BNF 规范中存在类似的非正式性,在这些规范中,我们假设<foo>指的是某个终端或非终端。在需要更正式规范的文本中(例如,在 RFC 规范中),BNF 的每个文字部分通常都是双引号括起来的,所以我们会得到
<AE> ::= <num> | <AE1> "+" <AE2> | <AE1> "-" <AE2>
eval(X)的另一种流行符号是[[X]]:
a. [[<num>]] = <num>b. [[<AE1> + <AE2>]] = [[<AE1>]] + [[<AE2>]]c. [[<AE1> - <AE2>]] = [[<AE1>]] - [[<AE2>]]
这个定义有问题吗?歧义:
eval(1 - 2 + 3) = ?
根据表达式的解析方式,我们可以得到2或-4的结果:
eval(1 - 2 + 3) = eval(1 - 2) + eval(3) [b] = eval(1) - eval(2) + eval(3) [c] = 1 - 2 + 3 [a,a,a] = 2eval(1 - 2 + 3) = eval(1) - eval(2 + 3) [c] = eval(1) - (eval(2) + eval(3)) [a] = 1 - (2 + 3) [a,a,a] = -4
再次,要非常注意混淆的微妙之处,这些微妙之处非常重要:我们只需要在一个子表达式的一侧加括号,为什么?——当我们写:
eval(1 - 2 + 3) = ... = 1 - 2 + 3
我们有两个表达式,但一个代表输入语法,另一个代表真正的数学表达式。
在计算机实现的情况下,左侧的语法(像往常一样)是 AE 语法,右侧的实际表达式是我们用来实现 AE 语言的任何语言中的表达式。
正如我们之前所说,歧义直到实际解析树很重要才成为问题。对于eval来说,这绝对很重要,因此我们不能使得可能以多种方式推导出任何语法,否则我们的评估将是非确定性的。
快速练习:
我们可以类似地为<digit>s 和然后<num>s 定义一个意义:
<NUM> ::= <digit> | <digit> <NUM>eval(0) = 0eval(1) = 1eval(2) = 2...eval(9) = 9eval(<digit>) = <digit>eval(<digit> <NUM>) = 10*eval(<digit>) + eval(<NUM>)
这正是我们想要的吗?——取决于我们实际想要什么…
-
首先,这段代码有一个错误——有一个类似 BNF 的推导
<NUM> ::= <digit> | <digit> <NUM>是无歧义的,但很难解析数字。我们得到:
eval(123) = 10*eval(1) + eval(23) = 10*1 + 10*eval(2) + eval(3) = 10*1 + 10*2 + 3 = 33改变最后一个规则的顺序效果更好:
<NUM> ::= <digit> | <NUM> <digit>然后:
eval(<NUM> <digit>) = 10*eval(<NUM>) + eval(<digit>) -
作为具体例子,看看如何使其与
107配合使用,这说明了组合性的重要性。 -
免费东西的例子看起来微不足道:如果我们以这种方式定义数字的含义,它总是有效吗?想象一个不提供大数的平均语言,当数字太大时,上述规则会失败。在 Racket 中,我们碰巧使用整数表示法来表示整数的语法,两者都是无限的。但是如果我们想在 C 中编写一个 Racket 编译器,或者在 Racket 中编写一个 C 编译器呢?在一个 C 编译器中编写 C 编译器,其中编译器在 64 位机器上运行,结果需要在 32 位机器上运行,会怎么样?
旁注:组合性星期二,1 月 17 日
例如
<NUM> ::= <digit> | <NUM> <digit>
成为一种更容易编写求值器的语言,引导我们到一个重要的概念——组合性。这个定义更容易编写一个求值器,因为结果语言是组合的:表达式的含义——例如123——由其两部分的含义组成,在这个 BNF 中是12和3。具体来说,<NUM> <digit>的评估是第一个的评估乘以 10,加上第二个的评估。在<digit> <NUM>的情况下,这更加困难——这样一个数字的含义不仅取决于两部分的含义,还取决于<NUM>的语法:
eval(<digit> <NUM>) = eval(<digit>) * 10^length(<NUM>) + eval(<NUM>)
在这种情况下,这是可以容忍的,因为表达式的含义仍然由其部分构成——但是命令式编程(当你使用副作用时)要复杂得多,因为它不是组合的(至少不是在明显的意义上)。这与函数式编程相比,函数式编程中,表达式的含义是其子表达式的含义的组合。例如,在函数式程序中,每个子表达式都有一定的含义,所有这些子表达式组成了包含它们的表达式的含义——但在命令式程序中,我们可以有代码的一部分是x++——这本身没有意义,至少不是以直接方式为整个程序的含义做出贡献的意义。
(实际上,我们可以为这种表达式定义一个明确定义的含义:从一个x是某个值 N 的容器的世界,到一个相同容器具有不同值 N+1 的世界。你现在可能能看出这会使事情变得更加复杂。在直觉层面上——如果我们看一个函数式程序的随机部分,我们可以知道它的含义,因此建立整个代码的含义是容易的,但在命令式程序中,一个随机部分的含义几乎是无用的。)
实施一个求值器 Tuesday, January 17th
现在继续实现我们语法的语义——我们通过一个eval函数来表达这一点,该函数评估一个表达式。
我们使用一个基本的编程原则——将代码分成两层,一层用于解析输入,一层用于执行评估。这样做可以避免我们否则会陷入的混乱,例如:
(define (eval sexpr) (match sexpr [(number: n) n] [(list '+ left right) (+ (eval left) (eval right))] [(list '- left right) (- (eval left) (eval right))] [else (error 'eval "bad syntax in ~s" sexpr)]))
这很混乱,因为它将两个非常不同的东西——语法和语义——合并成一个代码块。对于这种特定类型的求值器来说,看起来足够简单,但这仅仅是因为它足够简单,我们所做的一切就是用算术运算符替换构造函数。以后事情会变得更加复杂,将求值器与解析器捆绑在一起将会更加棘手。(注意:我们可以用运行时操作符替换构造函数的事实意味着我们有一种非常简单的、类似计算器的语言,并且实际上我们可以将所有程序“编译”成一个数字。)
如果我们拆分代码,我们可以轻松地包含像制作这样的决策。
{+ 1 {- 3 "a"}}
语法无效。(顺便说一句,这不是 Racket 的做法……)(此外,这就像 XML 语法和格式正确的 XML 语法之间的区别。)
使用两个单独的组件的额外优势在于,可以简单地替换每个组件,从而可以独立更改输入语法和语义——我们只需要保持相同的接口数据(AST),一切都将正常运作。
我们的parse函数将输入语法转换为抽象语法树(AST)。它之所以是抽象的,完全是因为它与您键入、打印等的任何实际具体语法无关。
实现 AE 语言
回到我们的 eval —— 这将是它的(显而易见的)类型:
(: eval : AE -> Number);; consumes an AE and computes the corresponding number
这导致一些明显的测试案例:
(equal? 3 (eval (parse "3")))(equal? 7 (eval (parse "{+ 3 4}")))(equal? 6 (eval (parse "{+ {- 3 4} 7}")))
从现在开始,我们将使用 #lang pl 语言提供的新test形式来编写:
(test (eval (parse "3")) => 3)(test (eval (parse "{+ 3 4}")) => 7)(test (eval (parse "{+ {- 3 4} 7}")) => 6)
请注意,我们仅在接口级别进行测试 —— 仅运行整个函数。例如,你可以考虑这样的一个测试:
(test (parse "{+ {- 3 4} 7}") => (Add (Sub (Num 3) (Num 4)) (Num 7)))
但解析的细节和构造函数的名称是除了我们的求值器之外没有人关心的东西 —— 所以我们不测试它们。事实上,在这些测试中甚至不应该提到 parse,因为它不是我们用户公共接口的一部分;他们只关心将其用作类似编译器的黑盒。 (这有时被称为“集成测试”。)我们很快就会解决这个问题。
像其他所有内容一样,递归 eval 代码的结构遵循其输入的递归结构。按照 HtDP 的术语,我们的模板是:
(: eval : AE -> Number)(define (eval expr) (cases expr [(Num n) ... n ...] [(Add l r) ... (eval l) ... (eval r) ...] [(Sub l r) ... (eval l) ... (eval r) ...]))
在这种情况下,填补空白非常简单。
(: eval : AE -> Number)(define (eval expr) (cases expr [(Num n) n] [(Add l r) (+ (eval l) (eval r))] [(Sub l r) (- (eval l) (eval r))]))
我们现在进一步将 eval 和 parse 合并为一个单一的 run 函数,该函数评估 AE 字符串。
(: run : String -> Number);; evaluate an AE program contained in a string(define (run str) (eval (parse str)))
这个函数成为我们代码的单一公共入口点,并且是应该在验证我们接口的测试中使用的唯一事物:
(test (run "3") => 3)(test (run "{+ 3 4}") => 7)(test (run "{+ {- 3 4} 7}") => 6)
结果的完整代码是:
▶#lang pl#| BNF for the AE language: <AE> ::= <num> | { + <AE> <AE> } | { - <AE> <AE> } | { * <AE> <AE> } | { / <AE> <AE> }|#;; AE abstract syntax trees(define-type AE [Num Number] [Add AE AE] [Sub AE AE] [Mul AE AE] [Div AE AE])(: parse-sexpr : Sexpr -> AE);; parses s-expressions into AEs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> AE);; parses a string containing an AE expression to an AE AST(define (parse str) (parse-sexpr (string->sexpr str)))(: eval : AE -> Number);; consumes an AE and computes the corresponding number(define (eval expr) (cases expr [(Num n) n] [(Add l r) (+ (eval l) (eval r))] [(Sub l r) (- (eval l) (eval r))] [(Mul l r) (* (eval l) (eval r))] [(Div l r) (/ (eval l) (eval r))]))(: run : String -> Number);; evaluate an AE program contained in a string(define (run str) (eval (parse str)));; tests(test (run "3") => 3)(test (run "{+ 3 4}") => 7)(test (run "{+ {- 3 4} 7}") => 6)
(注意,测试是使用 test 形式进行的,我们之前提到过。)
对于任何认为 Racket 是一个糟糕选择的人来说,现在是考虑在其他语言中需要多少代码来完成相同工作的好时机。
Typed Racket 简介星期二,1 月 17 日
计划:
-
为什么要使用类型?
-
为什么要使用 Typed Racket?
-
Typed Racket 有什么不同?
-
一些课程程序的 Typed Racket 示例
类型
-
谁使用过(静态)类型语言?
-
谁使用过不是 Java 的类型语言?
Typed Racket 将与您之前见过的任何东西既相似又非常不同。
为什么要用类型?
-
类型有助于结构化程序。
-
类型提供了强制和强制性的文档。
-
类型有助于捕获错误。
–> 它们 会 对你有所帮助。非常 多。
程序结构化
-
数据定义
;; An AE is one of: ; \;; (make-Num Number) ; > HtDP;; (make-Add AE AE) ; /(define-type AE ; \ [Num number?] ; > Predicates =~= contracts (PLAI) [Add AE? AE?]) ; / (has names of defined types too)(define-type AE ; \ [Num Number] ; > Typed Racket (our PL) [Add AE AE]) ; / -
数据优先
您程序的结构是从数据的结构派生的。
您在基础课程中已经见过这个,设计配方和模板都有。在这门课上,我们将大量使用类型定义和(cases ...)形式。类型使这种普遍 —— 我们必须先考虑我们的数据,再考虑我们的代码。
-
用于描述数据的语言
我们将不再使用在合同行中描述类型的非正式语言,以及在
define-type形式中描述谓词的更正式描述,我们将拥有一个统一的语言来描述这两者。拥有这样一种语言意味着我们可以更精确、更表达(因为类型语言涵盖了您否则会用一些挥手的手势来解决的情况,比如“一个函数”)。
为什么要使用 Typed Racket?
Racket 是我们都知道的语言,它具有我们之前讨论的好处。主要是,它是一个用于尝试编程语言的优秀语言。
-
Typed Racket 允许我们拿我们的 Racket 程序进行类型检查,因此我们可以得到静态类型语言的好处。
-
类型是一个重要的编程语言特性;有类型的 Racket 将帮助我们理解它们。
[此外:Typed Racket 的开发正在东北进行,并将受益于您的反馈。]
Typed Racket 如何与 Racket 不同
-
如果有类型错误,Typed Racket 将拒绝您的程序!这意味着它在编译时就这样做了,在任何代码运行之前 之前 。
-
Typed Racket 文件的开头是这样的:
#lang typed/racket;; Program goes here.但我们将使用 Typed Racket 语言的一个变体,其中包含一些额外的构造:
#lang pl;; Program goes here. -
Typed Racket 要求您在函数上编写合同。
Racket:
;; f : Number -> Number(define (f x) (* x (+ x 1)))Typed Racket:
#lang pl(: f : Number -> Number)(define (f x) (* x (+ x 1)))[在真实的 Typed Racket 中,您还可以在定义内部显示类型注释:
#lang typed/racket(define (f [x : Number]) : Number (* x (+ x 1)))但我们将不使用这种形式。]
-
正如我们所见,Typed Racket 在
define-type中使用类型而不是谓词。(define-type AE [Num Number] [Add AE AE])对比
(define-type AE [Num number?] [Add AE? AE?]) -
还有其他区别,但这些就足够了。
例子
(: digit-num : Number -> (U Number String))(define (digit-num n) (cond [(<= n 9) 1] [(<= n 99) 2] [(<= n 999) 3] [(<= n 9999) 4] [else "a lot"]))(: fact : Number -> Number)(define (fact n) (if (zero? n) 1 (* n (fact (- n 1)))))(: helper : Number Number -> Number)(define (helper n acc) (if (zero? n) acc (helper (- n 1) (* acc n))))(: fact : Number -> Number)(define (fact n) (helper n 1))(: fact : Number -> Number)(define (fact n) (: helper : Number Number -> Number) (define (helper n acc) (if (zero? n) acc (helper (- n 1) (* acc n)))) (helper n 1))(: every? : (All (A) (A -> Boolean) (Listof A) -> Boolean));; Returns false if any element of lst fails the given pred,;; true if all pass pred.(define (every? pred lst) (or (null? lst) (and (pred (first lst)) (every? pred (rest lst)))))(define-type AE [Num Number] [Add AE AE] [Sub AE AE]);; the only difference in the following definition is;; using (: <name> : <type>) instead of ";; <name> : <type>"(: parse-sexpr : Sexpr -> AE);; parses s-expressions into AEs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(list '+ left right) (Add (parse-sexpr left) (parse-sexpr right))] [(list '- left right) (Sub (parse-sexpr left) (parse-sexpr right))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
更有趣的例子
-
Typed Racket 设计成一种对人们在 Racket 中编写的程序友好的语言。例如,它有 unions:
(: foo : (U String Number) -> Number)(define (foo x) (if (string? x) (string-length x) ;; at this point it knows that `x' is not a ;; string, therefore it must be a number (+ 1 x)))这在静态类型语言中并不常见,通常仅限于 不相交 联合。例如,在 OCaml 中,您会写下这个定义:
type string_or_number = Str of string | Int of int ;;let foo x = match x with Str s -> String.length s | Int i -> i+1 ;;并且使用一个显式的构造函数:
foo (Str "bar") ;;foo (Int 3) ;; -
请注意,在 Typed Racket 的情况下,语言通过谓词跟踪收集的信息,这就是为什么它知道一个
x是一个字符串,另一个是一个数字。 -
Typed Racket 具有子类型的概念,这也是大多数静态类型语言所缺乏的。事实上,它具有(任意)联合类型意味着它必须也有子类型,因为类型始终是包含该类型的联合类型的子类型。
-
这个特性的另一个结果是有一个
Any类型,它是所有其他类型的联合。请注意,您始终可以使用此类型,因为一切都在其中 - 但它为您提供了关于值的最少信息。换句话说,Typed Racket 给了你一个选择:你决定使用哪种类型,一个非常受限制但对其值有很多信息的类型,或者一个非常宽容但几乎没有有用信息的类型。这与其他类型系统(HM 系统)形成对比,那里总是只有一个正确的类型。要演示,考虑恒等函数:
(define (id x) x)您可以使用
(: id : Integer -> Integer)类型,这是非常受限制的,但您知道该函数始终返回整数值。或者您可以使用
(: id : Any -> Any)使其非常宽容,但然后您对结果一无所知 - 实际上,(+ 1 (id 2))将抛出类型错误。它确实返回2,如预期的那样,但类型检查器不知道那个2的类型。如果您想使用此类型,您需要检查结果是否为数字,例如:(let ([x (id 123)]) (if (number? x) (+ x 10) 999))这意味着对于这个特定函数,我们无法选择一个好的具体类型 - 但我们仍然可以创���一个多态类型:
(: id : (All (A) A -> A))允许任何输入类型,并且其输出将是相同的,保留其输入上的相同信息水平。
-
另一个有趣的事情是看一下
error的类型:它是一个返回Nothing类型的函数 - 一个与空联合相同的类型:(U)。它是一个没有值的类型 - 它适合error,因为它是一个不返回任何值的函数,实际上,它根本不返回。此外,这意味着error表达式可以在任何您想要的地方使用,因为它是任何东西的子类型。 -
在
cond表达式中几乎总是需要一个else子句,例如:(: digit-num : Number -> (U Number String))(define (digit-num n) (cond [(<= n 9) 1] [(<= n 99) 2] [(<= n 999) 3] [(<= n 9999) 4] [(> n 9999) "a lot"]))(如果你认为类型检查器应该知道这是在做什么,那么如何
(> (* n 10) (/ (* (- 10000 1) 20) 2))或者
(>= n 10000)对于最后一个测试?
-
在某些罕见情况下,您可能会遇到 Typed Racket 的一个限制:当多态函数传递给高阶函数时,很难(也就是说:目前不知道通用解决方案)进行正确的推断。例如:
(: call : (All (A B) (A -> B) A -> B))(define (call f x) (f x))(call rest (list 4))在这种情况下,我们可以使用
inst来实例化一个具有多态类型的函数为给定类型 - 在这种情况下,我们可以使用它使rest被视为特定于数字列表的函数:(call (inst rest Number) (list 4))在其他罕见情况下,Typed Racket 会推断出一个对我们不合适的类型 — 还有另一种形式,
ann,允许我们指定某种类型。在call示例中使用这个更冗长:(call (ann rest : ((Listof Number) -> (Listof Number))) (list 4))然而,这些情况将会很少出现,并且在需要时会明确提到。
绑定和替换
现在我们来到一个重要的概念:替换。
即使在我们简单的语言中,我们也会遇到重复的表达式。例如,如果我们想要计算某个表达式的平方:
{* {+ 4 2} {+ 4 2}}
我们为什么要摆脱重复的子表达式?
-
它引入了冗余的计算。在这个例子中,我们希望避免第二次计算相同的子表达式。
-
它使计算比没有重复时更加复杂。与上面的进行比较:
with x = {+ 4 2}, {* x x} -
这与编程中一个我们已经讨论过的基本事实有关:复制信息总是一件坏事。除了其他坏后果外,它甚至可能导致如果我们不复制代码就不会发生的错误。一个玩具例子是在一个表达式中“修复”一个数字,却忘记修复对应的数字:
{* {+ 4 2} {+ 4 1}}现实世界的例子涉及更多的代码,这使得这种错误非常难以发现,但它们仍然遵循相同的原则。
-
这给了我们更多的表达能力 — 我们不只是说我们想要将两个表达式相乘,这两个表达式恰好都是
{+ 4 2},我们说我们将{+ 4 2}表达式与自身相乘。它允许我们表达两个值的相等,以及使用两个恰好相同的值。
因此,避免冗余的常规方法是引入一个标识符。即使在说话时,我们可能会说:“让 x 等于 4 加 2,将 x 乘以 x”。
(这些通常被称为“变量”,但我们将尽量避免使用这个名字:如果标识符不变呢?)
为了实现这一点,我们在我们的语言中引入了一个新的形式:
{with {x {+ 4 2}} {* x x}}
我们希望能够将这个简化为:
{* 6 6}
通过在with的主体子表达式中用 6 替换x。
一个稍微复杂的例子:
{with {x {+ 4 2}} {with {y {* x x}} {+ y y}}}[add] = {with {x 6} {with {y {* x x}} {+ y y}}}[subst]= {with {y {* 6 6}} {+ y y}}[mul] = {with {y 36} {+ y y}}[subst]= {+ 36 36}[add] = 72
WAE: 向 AETuesday 添加绑定, January 24th
PLAI §3
要将这个添加到我们的语言中,我们从 BNF 开始。我们现在将我们的语言称为“WAE”(With+AE):
<WAE> ::= <num> | { + <WAE> <WAE> } | { - <WAE> <WAE> } | { * <WAE> <WAE> } | { / <WAE> <WAE> } | { with { <id> <WAE> } <WAE> } | <id>
请注意,我们不得不引入两个新规则:一个用于引入标识符,另一个用于使用它。这在许多语言规范中都很常见,例如 define-type 引入了一个新类型,并且它带有 cases,允许我们解构其实例。
对于 <id>,我们需要使用某种形式的标识符,在 Racket 中的自然选择是使用符号。因此,我们可以写出相应的类型定义:
(define-type WAE [Num Number] [Add WAE WAE] [Sub WAE WAE] [Mul WAE WAE] [Div WAE WAE] [Id Symbol] [With Symbol WAE WAE])
解析器很容易扩展以产生这些语法对象:
(: parse-sexpr : Sexpr -> WAE);; parses s-expressions into WAEs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
但请注意,这个解析器很不方便 —— 如果这些表达式中的任何一个:
{* 1 2 3}{foo 5 6}{with x 5 {* x 8}}{with {5 x} {* x 8}}
会导致“bad syntax”错误,这并不是很有帮助。为了改进情况,我们可以为 with 表达式添加另一个情况,即格式不正确的情况,并在这种情况下给出更具体的消息:
(: parse-sexpr : Sexpr -> WAE);; parses s-expressions into WAEs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [(cons 'with more) (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
最后,为了将所有与 with 表达式相关的解析代码(无论是有效还是无效的)分组,我们可以对它们使用一个单独的情况:
(: parse-sexpr : Sexpr -> WAE);; parses s-expressions into WAEs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) ;; go in here for all sexpr that begin with a 'with (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
现在,我们已经完成了 with 扩展的语法部分。
快速说明 —— 为什么我们会像在这样的代码中一样对
With进行缩进(With 'x (Num 2) (Add (Id 'x) (Num 4)))而不是一个看起来像
let的缩进(With 'x (Num 2) (Add (Id 'x) (Num 4)))?
这样做的原因是第二个缩进看起来像是一个绑定结构(例如,在
let表达式中使用的缩进),但With不是 一个绑定形式 —— 它是一个 普通函数,因为它在 Racket 级别上。因此,您应该记住With与出现在 WAE 程序中的with之间的巨大差异:{with {x 2} {+ x 4}}另一种看待它的方式:想象一下,我们打算让这种语言被西班牙或中文使用者使用。在这种情况下,我们将翻译 “
with”:{con {x 2} {+ x 4}}{ha {x 2} {+ x 4}}但是,如果我们(语言实现者)是英语人士,我们将 不会 对
With做同样的操作。
评估 withTuesday, January 24th
现在,为了使这个工作,我们需要进行一些替换。
我们基本上想说,要评估:
{with {id WAE1} WAE2}
我们需要用 WAE1 替换 WAE2 中的 id 并进行评估。形式上:
eval( {with {id WAE1} WAE2} ) = eval( subst(WAE2,id,WAE1) )
替换的常见语法(快速:我所说的“语法”是什么意思?)有一个更常见的语法:
eval( {with {id WAE1} WAE2} ) = eval( WAE2[WAE1/id] )
旁注:这种语法起源于使用
[x/v]e的逻辑学家,后来出现了一种模仿函数参数更自然顺序的惯例,即e[x->v],最终这两者被合并为e[v/x],这有点令人困惑,因为参数的从左到右顺序与subst函数的顺序不同。
现在我们所需要的就是替换的确切定义。
请注意,替换不同于评估,它仅是评估过程的一部分。在前面的例子中,当我们评估表达式时,我们除了常规的算术运算外,还进行了替换,而这些算术运算已经是 AE 评估器的一部分。在这个最后的定义中,仍然缺少一个评估步骤,请看看你能否找到它。
所以现在让我们试着定义替换:
替换(第一次尝试):
e[v/i]要将表达式
e中的标识符i替换为表达式v,请用表达式v替换e中所有与名称i相同的标识符。
这似乎适用于简单的表达式,例如:
{with {x 5} {+ x x}} --> {+ 5 5}{with {x 5} {+ 10 4}} --> {+ 10 4}
然而,如果我们尝试这样做,我们就会崩溃,并出现无效的语法:
{with {x 5} {+ x {with {x 3} 10}}} --> {+ 5 {with {5 3} 10}} ???
— 我们得到了一个无效的表达式。
要解决这个问题,我们需要区分标识符的常规出现和用作新绑定的出现。我们需要一些新术语来解决这个问题:
-
绑定实例:标识符的绑定实例是指在新绑定中用于命名它的实例。在我们的
<WAE>语法中,绑定实例仅为with表单的<id>位置。 -
范围:绑定实例的范围是程序文本的区域,在该区域内标识符的实例指的是绑定实例中绑定的值。(请注意,这个定义实际上依赖于替换的定义,因为它用于指定标识符如何指代值。)
-
绑定实例(或绑定出现):如果标识符的实例包含在其名称的绑定实例的范围内,则该实例被绑定。
-
自由实例(或自由出现):不包含其名称的任何绑定实例的标识符称为自由的。
使用这个我们可以说前面定义的替换的问题在于它未能区分绑定实例(应替换)和绑定实例(不应替换)。所以我们尝试修复这个问题:
替换(第二次尝试):
e[v/i]要将表达式
e中的标识符i替换为表达式v,请用表达式v替换e中不是绑定实例本身的所有i的实例。
首先,检查前面的例子:
{with {x 5} {+ x x}} --> {+ 5 5}{with {x 5} {+ 10 4}} --> {+ 10 4}
仍然起作用,并且
{with {x 5} {+ x {with {x 3} 10}}} --> {+ 5 {with {x 3} 10}} --> {+ 5 10}
也可以起作用。但是,如果我们尝试这样做:
{with {x 5} {+ x {with {x 3} x}}}
我们得到:
--> {+ 5 {with {x 3} 5}}--> {+ 5 5}--> 10
但我们希望它是 8:内部的 x 应该由最近的绑定它的 with 绑定。
问题在于我们现在对替换的新定义尊重绑定实例,但未能处理它们的作用域。在上面的示例中,我们希望内部的 with 屏蔽 外部 with 对 x 的绑定。
替换(第三次尝试):
e[v/i]要用表达式
v替换表达式e中的标识符i,请用表达式v替换所有不是自身绑定实例,并且不在任何i的嵌套作用域中的i的实例。
这样可以避免上述错误的替换,但现在做事情太小心了:
{with {x 5} {+ x {with {y 3} x}}}
变成
--> {+ 5 {with {y 3} x}}--> {+ 5 x}
这是一个错误,因为 x 没有绑定(并且没有合理的规则可以指定我们可以用来评估它)。
问题在于我们的替换在每个新作用域处中止,这种情况下,在新的 y 作用域处停止了,但实际上不应该停止,因为它使用了不同的名称。事实上,替换的最后一个定义无法处理任何嵌套作用域。
再次修改:
替换(第四次尝试):
e[v/i]要用表达式
v替换表达式e中的标识符i,请用表达式v替换所有不是自身绑定实例,并且不在i的任何嵌套作用域中的i的实例。
最后,这是一个好定义。这只是有点机械化。请注意,我们实际上是指的所有 i 的实例,这些实例不在 i 的绑定实例的作用域内,这只是意味着所有在 e 中的 自由出现 的 i —— 在 e 中自由(为什么? —— 记得“自由”的定义吗?):
替换(第四次尝试 b):
e[v/i]要用表达式
v替换表达式e中的标识符i,请用表达式v替换e中的所有自由i。
基于此,我们最终可以为其编写代码:
(: subst : WAE Symbol WAE -> WAE);; substitutes the second argument with the third argument in the;; first argument, as per the rules of substitution; the resulting;; expression contains no free instances of the second argument(define (subst expr from to) ; returns expr[to/from] (cases expr [(Num n) expr] [(Add l r) (Add (subst l from to) (subst r from to))] [(Sub l r) (Sub (subst l from to) (subst r from to))] [(Mul l r) (Mul (subst l from to) (subst r from to))] [(Div l r) (Div (subst l from to) (subst r from to))] [(Id name) (if (eq? name from) to expr)] [(With bound-id named-expr bound-body) (if (eq? bound-id from) expr ;*** don't go in! (With bound-id named-expr (subst bound-body from to)))]))
…这与编写形式化的“纸质版本”替换规则完全相同。
我们仍然有错误:但我们需要一些额外的工作来解决它们。
在我们找到错误之前,我们需要看一下替换是如何在评估过程中使用的。
要修改我们的评估器,我们需要处理新的语法片段的规则 —— with 表达式和标识符。
当我们看到一个看起来像这样的表达式时:
{with {x E1} E2}
我们继续评估 E1 以获得值 V1,然后我们将标识符 x 用表达式 V1 替换在 E2 中,并继续评估这个新表达式。换句话说,我们有以下评估规则:
eval( {with {x E1} E2} ) = eval( E2[eval(E1)/x] )
所以我们知道如何处理with表达式。那么标识符呢?正如在目的说明中所述,subst的主要特征是它不会在周围留下任何替换变量的自由实例。这意味着如果初始表达式是有效的(不包含任何自由变量),那么当我们从
{with {x E1} E2}
到
E2[E1/x]
结果是一个 没有 x 的自由实例的表达式。因此,我们不需要在评估器中处理标识符 —— 替换会让它们全部消失。
我们现在可以将 AE 的形式定义扩展到 WAE 的形式:
eval(...) = ... same as the AE rules ...eval({with {x E1} E2}) = eval(E2[eval(E1)/x])eval(id) = error!
如果您仔细观察,您可能会在这个定义中发现一个潜在问题:我们正在将eval(E1)替换为x在E2中 — 一个需要 WAE 表达式的操作,但eval(E1)是一个数字。(查看我们为 AE 定义的eval定义的类型,然后查看上面的subst定义。)这似乎过于迂腐,但当我们到达代码时,它将需要一些解决方案。上述规则可以轻松编码如下:
(: eval : WAE -> Number);; evaluates WAE expressions by reducing them to numbers(define (eval expr) (cases expr [(Num n) n] [(Add l r) (+ (eval l) (eval r))] [(Sub l r) (- (eval l) (eval r))] [(Mul l r) (* (eval l) (eval r))] [(Div l r) (/ (eval l) (eval r))] [(With bound-id named-expr bound-body) (eval (subst bound-body bound-id (Num (eval named-expr))))] ;*** [(Id name) (error 'eval "free identifier: ~s" name)]))
注意标记行中的Num表达式:评估命名表达式会给我们一个数字 — 我们需要将这个数字转换为语法以便能够与subst一起使用。解决方案是使用Num将结果数字转换为数字(数字的语法)。这不是一个优雅的解决方案,但现在可以使用。
最后,这里有几个测试案例。我们使用一个新的test特殊形式,这是课程插件的一部分。使用test的方式是用两个表达式和一个=>箭头 — DrRacket 评估两者,如果结果相等,则不会发生任何事情。如果结果不同,您将收到一个警告行,但评估将继续,这样您可以尝试其他测试。您还可以使用=error>箭头来测试错误消息 — 使用来自预期错误的一些文本,?代表任何单个字符,*代表零个或多个字符的序列。(当您在作业中使用test时,当测试失败时,交付服务器将中止。)我们期望这些测试能成功(确保您明白为什么它们应该成功)。
;; tests(test (run "5") => 5)(test (run "{+ 5 5}") => 10)(test (run "{with {x {+ 5 5}} {+ x x}}") => 20)(test (run "{with {x 5} {+ x x}}") => 10)(test (run "{with {x {+ 5 5}} {with {y {- x 3}} {+ y y}}}") => 14)(test (run "{with {x 5} {with {y {- x 3}} {+ y y}}}") => 4)(test (run "{with {x 5} {+ x {with {x 3} 10}}}") => 15)(test (run "{with {x 5} {+ x {with {x 3} x}}}") => 8)(test (run "{with {x 5} {+ x {with {y 3} x}}}") => 10)(test (run "{with {x 5} {with {y x} y}}") => 5)(test (run "{with {x 5} {with {x x} x}}") => 5)(test (run "{with {x 1} y}") =error> "free identifier")
将所有这些内容放在一起,我们得到以下代码;尝试运行此代码将引发意外错误…
#lang pl#| BNF for the WAE language: <WAE> ::= <num> | { + <WAE> <WAE> } | { - <WAE> <WAE> } | { * <WAE> <WAE> } | { / <WAE> <WAE> } | { with { <id> <WAE> } <WAE> } | <id>|#;; WAE abstract syntax trees(define-type WAE [Num Number] [Add WAE WAE] [Sub WAE WAE] [Mul WAE WAE] [Div WAE WAE] [Id Symbol] [With Symbol WAE WAE])(: parse-sexpr : Sexpr -> WAE);; parses s-expressions into WAEs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> WAE);; parses a string containing a WAE expression to a WAE AST(define (parse str) (parse-sexpr (string->sexpr str)))(: subst : WAE Symbol WAE -> WAE);; substitutes the second argument with the third argument in the;; first argument, as per the rules of substitution; the resulting;; expression contains no free instances of the second argument(define (subst expr from to) (cases expr [(Num n) expr] [(Add l r) (Add (subst l from to) (subst r from to))] [(Sub l r) (Sub (subst l from to) (subst r from to))] [(Mul l r) (Mul (subst l from to) (subst r from to))] [(Div l r) (Div (subst l from to) (subst r from to))] [(Id name) (if (eq? name from) to expr)] [(With bound-id named-expr bound-body) (if (eq? bound-id from) expr (With bound-id named-expr (subst bound-body from to)))]))(: eval : WAE -> Number);; evaluates WAE expressions by reducing them to numbers(define (eval expr) (cases expr [(Num n) n] [(Add l r) (+ (eval l) (eval r))] [(Sub l r) (- (eval l) (eval r))] [(Mul l r) (* (eval l) (eval r))] [(Div l r) (/ (eval l) (eval r))] [(With bound-id named-expr bound-body) (eval (subst bound-body bound-id (Num (eval named-expr))))] [(Id name) (error 'eval "free identifier: ~s" name)]))(: run : String -> Number);; evaluate a WAE program contained in a string(define (run str) (eval (parse str)));; tests(test (run "5") => 5)(test (run "{+ 5 5}") => 10)(test (run "{with {x {+ 5 5}} {+ x x}}") => 20)(test (run "{with {x 5} {+ x x}}") => 10)(test (run "{with {x {+ 5 5}} {with {y {- x 3}} {+ y y}}}") => 14)(test (run "{with {x 5} {with {y {- x 3}} {+ y y}}}") => 4)(test (run "{with {x 5} {+ x {with {x 3} 10}}}") => 15)(test (run "{with {x 5} {+ x {with {x 3} x}}}") => 8)(test (run "{with {x 5} {+ x {with {y 3} x}}}") => 10)(test (run "{with {x 5} {with {y x} y}}") => 5)(test (run "{with {x 5} {with {x x} x}}") => 5)(test (run "{with {x 1} y}") =error> "free identifier")
糟糕,这个程序仍然存在问题,被测试捕捉到 — 我们遇到了意外的自由标识符错误。现在问题是什么?在这样的表达式中:
{with {x 5} {with {y x} y}}
我们忘记了在y绑定的表达式中替换x。我们需要在with的主体表达式以及其命名表达式中进行递归替换:
(: subst : WAE Symbol WAE -> WAE);; substitutes the second argument with the third argument in the;; first argument, as per the rules of substitution; the resulting;; expression contains no free instances of the second argument(define (subst expr from to) (cases expr [(Num n) expr] [(Add l r) (Add (subst l from to) (subst r from to))] [(Sub l r) (Sub (subst l from to) (subst r from to))] [(Mul l r) (Mul (subst l from to) (subst r from to))] [(Div l r) (Div (subst l from to) (subst r from to))] [(Id name) (if (eq? name from) to expr)] [(With bound-id named-expr bound-body) (if (eq? bound-id from) expr (With bound-id (subst named-expr from to) ;*** new (subst bound-body from to)))]))
而仍然我们有问题…现在是
{with {x 5} {with {x x} x}}
它以错误终止,但我们希望它评估为5!仔细尝试我们的替换代码揭示了问题:当我们将5替换为外部x时,我们不进入内部with因为它有相同的名称 — 但我们需要进入其命名表达式。即使标识符是相同的,我们也需要在命名表达式中进行替换:
(: subst : WAE Symbol WAE -> WAE);; substitutes the second argument with the third argument in the;; first argument, as per the rules of substitution; the resulting;; expression contains no free instances of the second argument(define (subst expr from to) (cases expr [(Num n) expr] [(Add l r) (Add (subst l from to) (subst r from to))] [(Sub l r) (Sub (subst l from to) (subst r from to))] [(Mul l r) (Mul (subst l from to) (subst r from to))] [(Div l r) (Div (subst l from to) (subst r from to))] [(Id name) (if (eq? name from) to expr)] [(With bound-id named-expr bound-body) (With bound-id (subst named-expr from to) (if (eq? bound-id from) bound-body (subst bound-body from to)))]))
完整(最终,正确的)代码版本现在是:
▶#lang pl#| BNF for the WAE language: <WAE> ::= <num> | { + <WAE> <WAE> } | { - <WAE> <WAE> } | { * <WAE> <WAE> } | { / <WAE> <WAE> } | { with { <id> <WAE> } <WAE> } | <id>|#;; WAE abstract syntax trees(define-type WAE [Num Number] [Add WAE WAE] [Sub WAE WAE] [Mul WAE WAE] [Div WAE WAE] [Id Symbol] [With Symbol WAE WAE])(: parse-sexpr : Sexpr -> WAE);; parses s-expressions into WAEs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> WAE);; parses a string containing a WAE expression to a WAE AST(define (parse str) (parse-sexpr (string->sexpr str)))#| Formal specs for `subst': (`N' is a <num>, `E1', `E2' are <WAE>s, `x' is some <id>, `y' is a *different* <id>) N[v/x] = N {+ E1 E2}[v/x] = {+ E1[v/x] E2[v/x]} {- E1 E2}[v/x] = {- E1[v/x] E2[v/x]} {* E1 E2}[v/x] = {* E1[v/x] E2[v/x]} {/ E1 E2}[v/x] = {/ E1[v/x] E2[v/x]} y[v/x] = y x[v/x] = v {with {y E1} E2}[v/x] = {with {y E1[v/x]} E2[v/x]} {with {x E1} E2}[v/x] = {with {x E1[v/x]} E2}|#(: subst : WAE Symbol WAE -> WAE);; substitutes the second argument with the third argument in the;; first argument, as per the rules of substitution; the resulting;; expression contains no free instances of the second argument(define (subst expr from to) (cases expr [(Num n) expr] [(Add l r) (Add (subst l from to) (subst r from to))] [(Sub l r) (Sub (subst l from to) (subst r from to))] [(Mul l r) (Mul (subst l from to) (subst r from to))] [(Div l r) (Div (subst l from to) (subst r from to))] [(Id name) (if (eq? name from) to expr)] [(With bound-id named-expr bound-body) (With bound-id (subst named-expr from to) (if (eq? bound-id from) bound-body (subst bound-body from to)))]))#| Formal specs for `eval': eval(N) = N eval({+ E1 E2}) = eval(E1) + eval(E2) eval({- E1 E2}) = eval(E1) - eval(E2) eval({* E1 E2}) = eval(E1) * eval(E2) eval({/ E1 E2}) = eval(E1) / eval(E2) eval(id) = error! eval({with {x E1} E2}) = eval(E2[eval(E1)/x])|#(: eval : WAE -> Number);; evaluates WAE expressions by reducing them to numbers(define (eval expr) (cases expr [(Num n) n] [(Add l r) (+ (eval l) (eval r))] [(Sub l r) (- (eval l) (eval r))] [(Mul l r) (* (eval l) (eval r))] [(Div l r) (/ (eval l) (eval r))] [(With bound-id named-expr bound-body) (eval (subst bound-body bound-id (Num (eval named-expr))))] [(Id name) (error 'eval "free identifier: ~s" name)]))(: run : String -> Number);; evaluate a WAE program contained in a string(define (run str) (eval (parse str)));; tests(test (run "5") => 5)(test (run "{+ 5 5}") => 10)(test (run "{with {x {+ 5 5}} {+ x x}}") => 20)(test (run "{with {x 5} {+ x x}}") => 10)(test (run "{with {x {+ 5 5}} {with {y {- x 3}} {+ y y}}}") => 14)(test (run "{with {x 5} {with {y {- x 3}} {+ y y}}}") => 4)(test (run "{with {x 5} {+ x {with {x 3} 10}}}") => 15)(test (run "{with {x 5} {+ x {with {x 3} x}}}") => 8)(test (run "{with {x 5} {+ x {with {y 3} x}}}") => 10)(test (run "{with {x 5} {with {y x} y}}") => 5)(test (run "{with {x 5} {with {x x} x}}") => 5)(test (run "{with {x 1} y}") =error> "free identifier")
提醒:
-
我们开始进行替换,使用一个类似
let的形式:with。 -
使用绑定的原因:
-
避免重复编写表达式。
-
更具表现力的语言(可以表达身份)。
-
复制是不好的!(“DRY”:不要重复自己。)
-
避免静态冗余。
-
-
避免冗余计算。
-
当避免指数资源时,不仅仅是一种优化。
-
避免动态冗余。
-
-
-
BNF:
<WAE> ::= <num> | { + <WAE> <WAE> } | { - <WAE> <WAE> } | { * <WAE> <WAE> } | { / <WAE> <WAE> } | { with { <id> <WAE> } <WAE> } | <id>请注意,我们不得不引入两个新规则:一个用于引入标识符,一个用于使用标识符。
-
类型定义:
(define-type WAE [Num Number] [Add WAE WAE] [Sub WAE WAE] [Mul WAE WAE] [Div WAE WAE] [Id Symbol] [With Symbol WAE WAE]) -
解析器:
(: parse-sexpr : Sexpr -> WAE);; parses s-expressions into WAEs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)])) -
我们需要定义替换。术语:
-
绑定实例。
-
作用域。
-
绑定实例。
-
自由实例。
-
-
经过多次尝试后:
e[v/i] — 将表达式
e中的标识符i替换为表达式v,用表达式v替换e中所有自由的i实例。 -
实现了代码,然后又需要修复一些错误:
(: subst : WAE Symbol WAE -> WAE);; substitutes the second argument with the third argument in the;; first argument, as per the rules of substitution; the resulting;; expression contains no free instances of the second argument(define (subst expr from to) (cases expr [(Num n) expr] [(Add l r) (Add (subst l from to) (subst r from to))] [(Sub l r) (Sub (subst l from to) (subst r from to))] [(Mul l r) (Mul (subst l from to) (subst r from to))] [(Div l r) (Div (subst l from to) (subst r from to))] [(Id name) (if (eq? name from) to expr)] [(With bound-id named-expr bound-body) (With bound-id (subst named-expr from to) (if (eq? bound-id from) bound-body (subst bound-body from to)))]))(请注意,我们修复的错误澄清了我们作用域工作的确切方式:在
{with {x 2} {with {x {+ x 2}} x}}中,第一个x的作用域是{+ x 2}表达式。) -
然后我们扩展了 AE 评估规则:
eval(...) = ... same as the AE rules ...eval({with {x E1} E2}) = eval(E2[eval(E1)/x])eval(id) = error!并注意可能的类型问题。
-
上述内容被翻译为一个 Racket 定义的
eval函数(通过一个小技巧来避免类型问题):(: eval : WAE -> Number);; evaluates WAE expressions by reducing them to numbers(define (eval expr) (cases expr [(Num n) n] [(Add l r) (+ (eval l) (eval r))] [(Sub l r) (- (eval l) (eval r))] [(Mul l r) (* (eval l) (eval r))] [(Div l r) (/ (eval l) (eval r))] [(With bound-id named-expr bound-body) (eval (subst bound-body bound-id (Num (eval named-expr))))] [(Id name) (error 'eval "free identifier: ~s" name)]))
正式规范
注意包含在 WAE 代码中的正式定义。它们是描述我们语言片段的方式,比普通英语更正式,但仍不像实际代码那样正式(和冗长)。
subst的正式定义:
(N是一个<num>,E1,E2是<WAE>,x是某个<id>,y是不同的<id>)
N[v/x] = N{+ E1 E2}[v/x] = {+ E1[v/x] E2[v/x]}{- E1 E2}[v/x] = {- E1[v/x] E2[v/x]}{* E1 E2}[v/x] = {* E1[v/x] E2[v/x]}{/ E1 E2}[v/x] = {/ E1[v/x] E2[v/x]}y[v/x] = yx[v/x] = v{with {y E1} E2}[v/x] = {with {y E1[v/x]} E2[v/x]}{with {x E1} E2}[v/x] = {with {x E1[v/x]} E2}
以及eval的正式定义:
eval(N) = Neval({+ E1 E2}) = eval(E1) + eval(E2)eval({- E1 E2}) = eval(E1) - eval(E2)eval({* E1 E2}) = eval(E1) * eval(E2)eval({/ E1 E2}) = eval(E1) / eval(E2)eval(id) = error!eval({with {x E1} E2}) = eval(E2[eval(E1)/x])
懒惰与急切评估
正如我们之前所见,评估有两种基本方法:急切或懒惰。在懒惰评估中,绑定用于某种文本引用——这仅用于避免两次编写表达式,但相关的计算无论如何都会执行两次。在急切评估中,我们不仅消除了文本冗余,还消除了计算。
我们的评估器使用了哪种评估方法?相关的形式化部分是with的处理:
eval({with {x E1} E2}) = eval(E2[eval(E1)/x])
相应的代码片段是:
[(With bound-id named-expr bound-body) (eval (subst bound-body bound-id (Num (eval named-expr))))]
我们如何使其变为懒惰评估?
在正式的方程中:
eval({with {x E1} E2}) = eval(E2[E1/x])
以及代码中:
(: eval : WAE -> Number);; evaluates WAE expressions by reducing them to numbers(define (eval expr) (cases expr [(Num n) n] [(Add l r) (+ (eval l) (eval r))] [(Sub l r) (- (eval l) (eval r))] [(Mul l r) (* (eval l) (eval r))] [(With bound-id named-expr bound-body) (eval (subst bound-body bound-id named-expr))] ;*** no eval and no Num wrapping [(Id name) (error 'eval "free identifier: ~s" name)]))
我们可以通过跟踪eval来验证其工作方式(比较您在两个版本中获得的跟踪):
> (trace eval) ; (put this in the definitions window)> (run "{with {x {+ 1 2}} {* x x}}")
暂时忽略追踪,修改后的 WAE 解释器与以前的工作方式相同,具体来说,所有测试都通过了。因此问题是我们得到的语言是否实际上与以前的不同。一个区别在于执行速度,但我们实际上无法注意到区别,而我们更关心的是含义。是否有任何程序在两种语言中运行时会产生不同的结果?
懒惰评估器的主要特点是,直到实际需要时才评估命名表达式。正如我们所见,如果绑定标识符使用多次,则会导致重复计算——这意味着它并未消除动态冗余。但是如果绑定标识符根本没有被使用呢?在这种情况下,命名表达式就会消失。这是一个表现出两种语言中行为不同的表达式的很好提示——如果我们在两种语言中都添加除法,当我们尝试运行时,我们会得到不同的结果:
{with {x {/ 8 0}} 7}
急切的评估器在尝试评估除法时会停止并显示错误,而懒惰的评估器则会简单地忽略它。
即使没有除法,我们也会得到类似的行为
{with {x y} 7}
但是这是否正确行为,即其评估为 7,是值得怀疑的——我们真的希望禁止使用自由变量的程序。
此外,名称捕获存在问题——我们不希望将一个表达式替换为捕获其某些自由变量的上下文。但是我们的替换允许这样做,通常不是问题,因为在进行替换时,命名表达式不应该有需要替换的自由变量。然而,考虑评估此程序:
{with {y x} {with {x 2} {+ x y}}}
在两种评估制度下:急切版本会停止并显示错误,而懒惰版本会成功。这指向我们的替换中存在错误,或者说我们没有处理一个我们没有遇到的问题。
所以总结一下:只要初始程序是正确的,两种评估制度都会产生相同的结果。如果一个程序包含自由变量,它们可能会在一个天真的惰性求值器实现中被捕获(但这是一个应该修复的错误)。此外,有些情况下,急切求值会遇到运行时问题,而在惰性求值器中不会发生,因为表达式没有被使用。可以证明,当你评估一个表达式时,如果有一个错误可以避免,惰性求值器总是会避免它,而急切求值器总是会遇到它。另一方面,惰性求值器通常比急切求值器慢,所以这是速度与稳健性的权衡。
请注意,惰性求值中,我们说标识符绑定到一个表达式而不是一个值。(再次说明,这就是为什么急切版本需要将eval的结果包装在Num中,而这个版本不需要。)
(可以改变一些东西并得到一个更好的替换,基本上我们需要找到是否可能发生捕获,然后重命名东西以避免它。例如,
{with {y E1} E2}[v/x] if `x' and `y' are equal = {with {y E1[v/x]} E2} = {with {x E1[v/x]} E2} if `y' has a free occurrence in `v' = {with {y1 E1[v/x]} E2[y1/y][v/x]} ; `y1' is "fresh" otherwise = {with {x E1[v/x]} E2[v/x]}
但你可以看到这更加复杂(更多的代码:需要一个free-in谓词,能够发明新的新鲜名称等等)。而且这甚至还不是故事的结束...)
德布鲁因索引星期二,1 月 24 日
整个故事都围绕着名称展开,具体来说,名称捕获是应该始终避免的问题(它是编程语言头疼的主要源泉之一)。
但名称是我们唯一可以使用绑定的方式吗?
还有至少一种替代方式:注意我们使用名称的唯一目的是用于引用。我们并不真的在乎名称是什么,当我们考虑这两个 WAE 表达式时,这是非常明显的:
{with {x 5} {+ x x}}{with {y 5} {+ y y}}
或者两个 Racket 函数定义:
(define (foo x) (list x x))(define (foo y) (list y y))
这两者都展示了一对我们在某种意义上应该视为相等的表达式(这被称为“α等价”)。我们关心的唯一事情是变量指向的位置:绑定结构是唯一重要的事情。换句话说,只要 DrRacket 在我们使用“检查语法”时产生相同的箭头,我们就认为程序是相同的,不管名称选择如何(对于参数名称和局部名称而言,全局名称如上面的 foo 不在此列)。
另一种替代方法是利用这个原则:如果我们关心的只是箭头的去向,那么只需摆脱名称… 不是通过名称引用绑定,而是指定我们想要引用的周围作用域。例如,而不是:
{with {x 5} {with {y 6} {+ x y}}}
我们可以使用一个新的“引用”语法 — [N] — 并使用这个语法代替上述的:
{with 5 {with 6 {+ [1] [0]}}}
因此,对于 [N] 的规则是 — [0] 是在当前作用域中绑定的值,[1] 是下一个作用域中的值等等。
当然,为了进行这种翻译,我们必须知道精确的作用域规则。两个更复杂的例子:
{with {x 5} {+ x {with {y 6} {+ x y}}}}
is translated to:
{with 5 {+ [0] {with 6 {+ [1] [0]}}}}
(注意 x 如何根据它在原始代码中出现的位置而作为不同的引用。)更微妙的是:
{with {x 5} {with {y {+ x 1}} {+ x y}}}
is translated to:
{with 5 {with {+ [0] 1} {+ [1] [0]}}}
因为内部的 with 没有自己的命名表达式在其作用域中,所以命名表达式立即在外部 with 的作用域中。
这被称为“德布鲁因索引”:我们不使用标识符的名称,而是使用周围绑定上下文的索引。正如上面的例子所示,其主要缺点在于对人类来说不方便。具体来说,同一标识符使用不同的数字进行引用,这使得理解某些代码在做什么变得困难。
然而,几乎所有的编译器都使用这种方法来生成编译后的代码(考虑一下栈指针)。例如,GCC 编译此代码:
{ int x = 5; { int y = x + 1; return x + y; }}
转换为:
subl $8, %espmovl $5, -4(%ebp) ; int x = 5movl -4(%ebp), %eaxincl %eaxmovl %eax, -8(%ebp) ; int y = %eaxmovl -8(%ebp), %eaxaddl -4(%ebp), %eax
函数和函数值星期二,1 月 24 日
PLAI §4
现在我们有了一个用于本地绑定的形式,这迫使我们处理适当的替换和所有相关的事情,我们可以开始谈论函数了。函数的概念本身与替换非常接近,也与我们的with形式接近。例如,当我们写下:
{with {x 5} {* x x}}
然后{* x x}体本身是针对x的某个值进行参数化的。如果我们取出这个表达式中的5,我们剩下的就是一个具有函数所有必要成分的东西——一堆代码,它是针对某个输入标识符进行参数化的:
{with {x} {* x x}}
我们只需要替换with并使用一个适当的名称来指示它是一个函数:
{fun {x} {* x x}}
现在我们的语言中有了一个新的形式,这个形式应该有一个作为其含义的功能。正如我们在with表达式的情况中所看到的,我们也需要一个新的形式来使用这些功能。我们将使用call来实现这一点,这样
{call {fun {x} {* x x}} 5}
将与我们最初开始的原始with表达式相同——fun表达式就像没有值的with表达式,将其应用于5就是提供那个值:
{with {x 5} {* x x}}
当然,这并没有太大帮助——我们得到的只是一种比起最初更啰嗦的使用本地绑定的方式。我们真正缺少的是一种命名这些函数的方法。如果我们得到正确的评估规则,我们可以将fun表达式评估为某个值——这将允许我们使用with将其绑定到一个变量。类似这样:
{with {sqr {fun {x} {* x x}}} {+ {call sqr 5} {call sqr 6}}}
在这个表达式中,我们说x是形式参数(或参数),而5和6是实际参数(有时缩写为形式和实际)。请注意,通常给函数命名是有帮助的,但很多时候有些小函数可以不用指定名称——例如,考虑一个两阶段加法函数,其中没有明显好的返回函数的名称:
{with {add {fun {x} {fun {y} {+ x y}}}} {call {call add 8} 9}}
实现第一类函数星期二,1 月 24 日
PLAI §6(使用了一些来自 PLAI §5 的内容,我们稍后会讨论)
这是一个简单的计划,但它与函数在我们的语言中如何使用直接相关。我们知道{call {fun {x} E1} E2}等同于一个with表达式,但这里的新事物是我们允许仅仅写{fun ...}表达式本身,因此我们需要为其赋予一些含义。这个表达式的含义,或者说这个表达式的值,大致应该是“一个需要为x插入值的表达式”。换句话说,我们的语言将具有这些新类型的值,其中包含一个稍后要评估的表达式。
有三种基本方法可以将编程语言分类为与函数处理方式相关的:
-
一阶:函数不是真正的值。它们不能被其他函数使用或返回作为值。这意味着它们不能被存储在数据结构中。这是大多数“传统”语言过去所具有的特征。(你将在作业 4 中实现这样一种语言。)
这样一种语言的例子是在 HtDP 中使用的初学者学生语言,该语言故意是一阶的,以帮助学生编写正确的代码(在通常将函数用作值的早期阶段通常是错误的情况下)。很难找到属于这一类别的实用现代语言。
-
高阶:函数可以接收和返回其他函数作为值。这就是你在 C 和现代 Fortran 中得到的东西。
-
第一类:函数是具有其他值所有权利的值。特别是,它们可以被提供给其他函数,从函数返回,存储在数据结构中,并且可以在运行时创建新函数。(大多数现代语言都具有第一类函数。)
最后一类是最有趣的。在过去,复杂表达式不是第一类的,因为它们不能自由组合。这在机器码中仍然是这样:正如我们之前所见,要计算一个表达式,例如
(-b + sqrt(b² - 4*a*c)) / 2a
你必须像这样做:
x = b * by = 4 * ay = y * cx = x - yx = sqrt(x)y = -bx = y + xy = 2 * as = x / y
换句话说,每个中间值都需要有自己的名称。但是使用适当的(“高级”)编程语言(至少大多数…),你可以只写原始表达式,而不为这些值命名。
使用第一类函数时会发生类似的事情 — 可以有消耗和返回函数的复杂表达式,它们不需要被命名。
如果我们可以使我们的fun表达式工作,我们得到的正是这个:它生成一个函数,你可以选择将其绑定到一个名称,或者不绑定。重要的是该值独立于名称存在。
这对编程语言的“个性”产生了重大影响,我们将会看到。事实上,仅仅添加这个特性就会使我们的语言比仅具有高阶或一阶函数的语言更加先进。
实现一流函数(续)星期二,1 月 31 日
快速示例:以下是可工作的 JavaScript 代码,使用了一流函数。
function foo(x) { function bar(y) { return x + y; } return bar;}function main() { var f = foo(1); var g = foo(10); return [f(2), g(2)];}
注意上述 foo 的定义 不 使用匿名的“lambda 表达式” —— 用 Racket 术语来说,它被翻译为
(define (foo x) (define (bar y) (+ x y)) bar)
返回的函数不是匿名的,但也不是真正的命名:bar 名称仅在 foo 的主体内绑定,在外部不存在,因为它不在其范围内。如果显示函数值,它会以打印形式使用,但这仅仅是一种调试辅助工具。在 Racket 中常见的匿名 lambda 版本也可以在 JavaScript 中使用:
function foo(x) { return function(y) { return x + y; }}
侧面说明:GCC 包含允许内部函数定义的扩展,但仍然没有一流函数 —— 尝试上述操作会失败:
#include <stdio.h>typedef int(*int2int)(int);int2int foo(int x) { int bar(int y) { return x + y; } return bar;}int main() { int2int f = foo(1); int2int g = foo(10); printf(">> %d, %d\n", f(2), g(2));}
FLANG 语言星期二,1 月 31 日
现在进行实现 — 我们称这种新语言为 FLANG。
首先,BNF:
<FLANG> ::= <num> | { + <FLANG> <FLANG> } | { - <FLANG> <FLANG> } | { * <FLANG> <FLANG> } | { / <FLANG> <FLANG> } | { with { <id> <FLANG> } <FLANG> } | <id> | { fun { <id> } <FLANG> } | { call <FLANG> <FLANG> }
匹配的类型定义:
(define-type FLANG [Num Number] [Add FLANG FLANG] [Sub FLANG FLANG] [Mul FLANG FLANG] [Div FLANG FLANG] [Id Symbol] [With Symbol FLANG FLANG] [Fun Symbol FLANG] ; No named-expression [Call FLANG FLANG])
这个语法的解析器通常很简单:
(: parse-sexpr : Sexpr -> FLANG);; parses s-expressions into FLANGs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: name)) body) (Fun name (parse-sexpr body))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [(list 'call fun arg) (Call (parse-sexpr fun) (parse-sexpr arg))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
我们还需要修补替换函数以处理这些情况。新函数形式的作用域规则与with的规则类似,只是现在没有额外的表达式,而call的作用域规则与算术运算符的规则相同:
N[v/x] = N{+ E1 E2}[v/x] = {+ E1[v/x] E2[v/x]}{- E1 E2}[v/x] = {- E1[v/x] E2[v/x]}{* E1 E2}[v/x] = {* E1[v/x] E2[v/x]}{/ E1 E2}[v/x] = {/ E1[v/x] E2[v/x]}y[v/x] = yx[v/x] = v{with {y E1} E2}[v/x] = {with {y E1[v/x]} E2[v/x]}{with {x E1} E2}[v/x] = {with {x E1[v/x]} E2}{call E1 E2}[v/x] = {call E1[v/x] E2[v/x]}{fun {y} E}[v/x] = {fun {y} E[v/x]}{fun {x} E}[v/x] = {fun {x} E}
匹配的代码:
(: subst : FLANG Symbol FLANG -> FLANG);; substitutes the second argument with the third argument in the;; first argument, as per the rules of substitution; the resulting;; expression contains no free instances of the second argument(define (subst expr from to) (cases expr [(Num n) expr] [(Add l r) (Add (subst l from to) (subst r from to))] [(Sub l r) (Sub (subst l from to) (subst r from to))] [(Mul l r) (Mul (subst l from to) (subst r from to))] [(Div l r) (Div (subst l from to) (subst r from to))] [(Id name) (if (eq? name from) to expr)] [(With bound-id named-expr bound-body) (With bound-id (subst named-expr from to) (if (eq? bound-id from) bound-body (subst bound-body from to)))] [(Call l r) (Call (subst l from to) (subst r from to))] [(Fun bound-id bound-body) (if (eq? bound-id from) expr (Fun bound-id (subst bound-body from to)))]))
现在,在我们开始编写评估器之前,我们需要决定用什么来表示这种语言的值。在我们有函数之前,我们只有数字值,并且我们使用 Racket 数字来表示它们。现在我们有两种类型的值 — 数字和函数。继续使用 Racket 数字来表示数字似乎很容易,但是函数呢?评估
{fun {x} {+ x 1}}
嗯,这是我们拥有的新玩具:它应该是一个函数值,这是一种可以像数字一样使用的东西,但是不同于算术运算,我们可以通过call这些东西。我们需要一种方法来避免评估函数的主体表达式 — 延迟它 — 并且使用一些值来包含这个延迟的表达式,以便稍后可以使用。
为了适应这一点,我们将稍微改变我们的实现策略:我们将使用我们的语法对象来表示数字((Num n)而不仅仅是n),这在进行算术运算时可能有点不方便,但通过使函数的评估方式类似于返回它们自己的语法对象,将简化生活。语法对象具有我们需要的内容:需要在函数调用时稍后评估的主体表达式,它还具有应该用实际输入替换的标识符名称。这意味着评估:
(Add (Num 1) (Num 2))
现在产生
(Num 3)
一个数字(Num 5)评估为(Num 5)。
类似地,(Fun 'x (Num 2))评估为(Fun 'x (Num 2))。
为什么这会起作用?因为call将与with非常相似 — 唯一的区别是其参数的顺序有点不同,从应用的函数和参数中检索。
因此,正式的评估规则将函数视为数字,并使用语法对象来表示这两种值:
eval(N) = Neval({+ E1 E2}) = eval(E1) + eval(E2)eval({- E1 E2}) = eval(E1) - eval(E2)eval({* E1 E2}) = eval(E1) * eval(E2)eval({/ E1 E2}) = eval(E1) / eval(E2)eval(id) = error!eval({with {x E1} E2}) = eval(E2[eval(E1)/x])eval(FUN) = FUN ; assuming FUN is a function expressioneval({call E1 E2}) = eval(Ef[eval(E2)/x]) if eval(E1) = {fun {x} Ef} = error! otherwise
请注意,最后一个规则可以使用with表达式的转换来编写:
eval({call E1 E2}) = eval({with {x E2} Ef}) if eval(E1) = {fun {x} Ef} = error! otherwise
或者,我们可以使用call和fun来指定with:
eval({with {x E1} E2}) = eval({call {fun {x} E2} E1})
这些规则中存在一个小问题,直观地看,call的评估规则应该与算术运算的规则非常相似。现在我们有两种类型的值,因此我们也需要检查算术运算的参数:
eval({+ E1 E2}) = N1 + N2 if eval(E1), eval(E2) evaluate to numbers N1, N2 otherwise error!...
相应的代码是:
(: eval : FLANG -> FLANG) ;*** note return type;; evaluates FLANG expressions by reducing them to *expressions*(define (eval expr) (cases expr [(Num n) expr] ;*** change here [(Add l r) (arith-op + (eval l) (eval r))] [(Sub l r) (arith-op - (eval l) (eval r))] [(Mul l r) (arith-op * (eval l) (eval r))] [(Div l r) (arith-op / (eval l) (eval r))] [(With bound-id named-expr bound-body) (eval (subst bound-body bound-id (eval named-expr)))] ;*** no `(Num ...)' [(Id name) (error 'eval "free identifier: ~s" name)] [(Fun bound-id bound-body) expr] ;*** similar to `Num' [(Call (Fun bound-id bound-body) arg-expr) ;*** nested pattern (eval (subst bound-body ;*** just like `with' bound-id (eval arg-expr)))] [(Call something arg-expr) (error 'eval "`call' expects a function, got: ~s" something)]))
请注意,Call 案例正在执行与 With 案例中相同的操作。实际上,我们本可以生成一个 With 表达式并评估它:
... [(Call (Fun bound-id bound-body) arg-expr) (eval (With bound-id arg-expr bound-body))] ...
arith-op 函数负责检查输入值是否为数字(以 FLANG 数字表示),将它们转换为普通数字,执行 Racket 操作,然后将结果重新包装为 Num。请注意,它的类型表明它是一个高阶函数。
(: arith-op : (Number Number -> Number) FLANG FLANG -> FLANG);; gets a Racket numeric binary operator, and uses it within a FLANG;; `Num' wrapper (note the H.O type)(define (arith-op op expr1 expr2) (Num (op (Num->number expr1) (Num->number expr2))))
它使用以下函数将 FLANG 数字转换为 Racket 数字。(请注意,else 几乎总是一个坏主意,因为它可能会阻止编译器显示要编辑代码的位置 —— 但这种情况是个例外,因为我们永远不希望处理除了 Num 以外的任何东西。)这个函数相对简单的原因是,我们选择了简单的方式,并使用 Racket 数字表示数字,但我们也可以使用字符串或其他任何东西。
(: Num->number : FLANG -> Number);; convert a FLANG number to a Racket one(define (Num->number e) (cases e [(Num n) n] [else (error 'arith-op "expected a number, got: ~s" e)]))
如果我们让 run 将结果转换为数字,我们也可以使使用变得更加简单:
(: run : String -> Number);; evaluate a FLANG program contained in a string(define (run str) (let ([result (eval (parse str))]) (cases result [(Num n) n] [else (error 'run "evaluation returned a non-number: ~s" result)])))
添加一些简单的测试我们得到:
;; The Flang interpreter#lang pl#|The grammar: <FLANG> ::= <num> | { + <FLANG> <FLANG> } | { - <FLANG> <FLANG> } | { * <FLANG> <FLANG> } | { / <FLANG> <FLANG> } | { with { <id> <FLANG> } <FLANG> } | <id> | { fun { <id> } <FLANG> } | { call <FLANG> <FLANG> }Evaluation rules: subst: N[v/x] = N {+ E1 E2}[v/x] = {+ E1[v/x] E2[v/x]} {- E1 E2}[v/x] = {- E1[v/x] E2[v/x]} {* E1 E2}[v/x] = {* E1[v/x] E2[v/x]} {/ E1 E2}[v/x] = {/ E1[v/x] E2[v/x]} y[v/x] = y x[v/x] = v {with {y E1} E2}[v/x] = {with {y E1[v/x]} E2[v/x]} ; if y =/= x {with {x E1} E2}[v/x] = {with {x E1[v/x]} E2} {call E1 E2}[v/x] = {call E1[v/x] E2[v/x]} {fun {y} E}[v/x] = {fun {y} E[v/x]} ; if y =/= x {fun {x} E}[v/x] = {fun {x} E} eval: eval(N) = N eval({+ E1 E2}) = eval(E1) + eval(E2) \ if both E1 and E2 eval({- E1 E2}) = eval(E1) - eval(E2) \ evaluate to numbers eval({* E1 E2}) = eval(E1) * eval(E2) / otherwise error! eval({/ E1 E2}) = eval(E1) / eval(E2) / eval(id) = error! eval({with {x E1} E2}) = eval(E2[eval(E1)/x]) eval(FUN) = FUN ; assuming FUN is a function expression eval({call E1 E2}) = eval(Ef[eval(E2)/x]) if eval(E1)={fun {x} Ef}, otherwise error!|#(define-type FLANG [Num Number] [Add FLANG FLANG] [Sub FLANG FLANG] [Mul FLANG FLANG] [Div FLANG FLANG] [Id Symbol] [With Symbol FLANG FLANG] [Fun Symbol FLANG] [Call FLANG FLANG])(: parse-sexpr : Sexpr -> FLANG);; parses s-expressions into FLANGs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: name)) body) (Fun name (parse-sexpr body))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [(list 'call fun arg) (Call (parse-sexpr fun) (parse-sexpr arg))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> FLANG);; parses a string containing a FLANG expression to a FLANG AST(define (parse str) (parse-sexpr (string->sexpr str)))(: subst : FLANG Symbol FLANG -> FLANG);; substitutes the second argument with the third argument in the;; first argument, as per the rules of substitution; the resulting;; expression contains no free instances of the second argument(define (subst expr from to) (cases expr [(Num n) expr] [(Add l r) (Add (subst l from to) (subst r from to))] [(Sub l r) (Sub (subst l from to) (subst r from to))] [(Mul l r) (Mul (subst l from to) (subst r from to))] [(Div l r) (Div (subst l from to) (subst r from to))] [(Id name) (if (eq? name from) to expr)] [(With bound-id named-expr bound-body) (With bound-id (subst named-expr from to) (if (eq? bound-id from) bound-body (subst bound-body from to)))] [(Call l r) (Call (subst l from to) (subst r from to))] [(Fun bound-id bound-body) (if (eq? bound-id from) expr (Fun bound-id (subst bound-body from to)))]))(: Num->number : FLANG -> Number);; convert a FLANG number to a Racket one(define (Num->number e) (cases e [(Num n) n] [else (error 'arith-op "expected a number, got: ~s" e)]))(: arith-op : (Number Number -> Number) FLANG FLANG -> FLANG);; gets a Racket numeric binary operator, and uses it within a FLANG;; `Num' wrapper(define (arith-op op expr1 expr2) (Num (op (Num->number expr1) (Num->number expr2))))(: eval : FLANG -> FLANG);; evaluates FLANG expressions by reducing them to *expressions*(define (eval expr) (cases expr [(Num n) expr] [(Add l r) (arith-op + (eval l) (eval r))] [(Sub l r) (arith-op - (eval l) (eval r))] [(Mul l r) (arith-op * (eval l) (eval r))] [(Div l r) (arith-op / (eval l) (eval r))] [(With bound-id named-expr bound-body) (eval (subst bound-body bound-id (eval named-expr)))] [(Id name) (error 'eval "free identifier: ~s" name)] [(Fun bound-id bound-body) expr] [(Call (Fun bound-id bound-body) arg-expr) (eval (subst bound-body bound-id (eval arg-expr)))] [(Call something arg-expr) (error 'eval "`call' expects a function, got: ~s" something)]))(: run : String -> Number);; evaluate a FLANG program contained in a string(define (run str) (let ([result (eval (parse str))]) (cases result [(Num n) n] [else (error 'run "evaluation returned a non-number: ~s" result)])));; tests(test (run "{call {fun {x} {+ x 1}} 4}") => 5)(test (run "{with {add3 {fun {x} {+ x 3}}} {call add3 1}}") => 4)(test (run "{with {add3 {fun {x} {+ x 3}}} {with {add1 {fun {x} {+ x 1}}} {with {x 3} {call add1 {call add3 x}}}}}") => 7)
这个版本仍然存在问题。首先是一个问题 —— 如果 call 类似于算术操作(并且与 with 在实际执行上也类似),那么为什么代码差异如此之大,以至于甚至不需要辅助函数呢?
第二个问题:如果我们评估这些代码片段,应该会发生什么?
(run "{with {add {fun {x} {fun {y} {+ x y}}}} {call {call add 8} 9}}")(run "{with {identity {fun {x} x}} {with {foo {fun {x} {+ x 1}}} {call {call identity foo} 123}}}")(run "{call {call {fun {x} {call x 1}} {fun {x} {fun {y} {+ x y}}}} 123}")
第三个问题,如果我们执行上述操作,会发生什么?
我们缺少的是对函数表达式的评估,以防它不是一个字面上的 fun 形式。以下修复此问题:
(: eval : FLANG -> FLANG);; evaluates FLANG expressions by reducing them to *expressions*(define (eval expr) (cases expr [(Num n) expr] [(Add l r) (arith-op + (eval l) (eval r))] [(Sub l r) (arith-op - (eval l) (eval r))] [(Mul l r) (arith-op * (eval l) (eval r))] [(Div l r) (arith-op / (eval l) (eval r))] [(With bound-id named-expr bound-body) (eval (subst bound-body bound-id (eval named-expr)))] [(Id name) (error 'eval "free identifier: ~s" name)] [(Fun bound-id bound-body) expr] [(Call fun-expr arg-expr) (let ([fval (eval fun-expr)]) ;*** need to evaluate this! (cases fval [(Fun bound-id bound-body) (eval (subst bound-body bound-id (eval arg-expr)))] [else (error 'eval "`call' expects a function, got: ~s" fval)]))]))
完整的代码是:
▶;; The Flang interpreter#lang pl#|The grammar: <FLANG> ::= <num> | { + <FLANG> <FLANG> } | { - <FLANG> <FLANG> } | { * <FLANG> <FLANG> } | { / <FLANG> <FLANG> } | { with { <id> <FLANG> } <FLANG> } | <id> | { fun { <id> } <FLANG> } | { call <FLANG> <FLANG> }Evaluation rules: subst: N[v/x] = N {+ E1 E2}[v/x] = {+ E1[v/x] E2[v/x]} {- E1 E2}[v/x] = {- E1[v/x] E2[v/x]} {* E1 E2}[v/x] = {* E1[v/x] E2[v/x]} {/ E1 E2}[v/x] = {/ E1[v/x] E2[v/x]} y[v/x] = y x[v/x] = v {with {y E1} E2}[v/x] = {with {y E1[v/x]} E2[v/x]} ; if y =/= x {with {x E1} E2}[v/x] = {with {x E1[v/x]} E2} {call E1 E2}[v/x] = {call E1[v/x] E2[v/x]} {fun {y} E}[v/x] = {fun {y} E[v/x]} ; if y =/= x {fun {x} E}[v/x] = {fun {x} E} eval: eval(N) = N eval({+ E1 E2}) = eval(E1) + eval(E2) \ if both E1 and E2 eval({- E1 E2}) = eval(E1) - eval(E2) \ evaluate to numbers eval({* E1 E2}) = eval(E1) * eval(E2) / otherwise error! eval({/ E1 E2}) = eval(E1) / eval(E2) / eval(id) = error! eval({with {x E1} E2}) = eval(E2[eval(E1)/x]) eval(FUN) = FUN ; assuming FUN is a function expression eval({call E1 E2}) = eval(Ef[eval(E2)/x]) if eval(E1)={fun {x} Ef}, otherwise error!|#(define-type FLANG [Num Number] [Add FLANG FLANG] [Sub FLANG FLANG] [Mul FLANG FLANG] [Div FLANG FLANG] [Id Symbol] [With Symbol FLANG FLANG] [Fun Symbol FLANG] [Call FLANG FLANG])(: parse-sexpr : Sexpr -> FLANG);; parses s-expressions into FLANGs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: name)) body) (Fun name (parse-sexpr body))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [(list 'call fun arg) (Call (parse-sexpr fun) (parse-sexpr arg))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> FLANG);; parses a string containing a FLANG expression to a FLANG AST(define (parse str) (parse-sexpr (string->sexpr str)))(: subst : FLANG Symbol FLANG -> FLANG);; substitutes the second argument with the third argument in the;; first argument, as per the rules of substitution; the resulting;; expression contains no free instances of the second argument(define (subst expr from to) (cases expr [(Num n) expr] [(Add l r) (Add (subst l from to) (subst r from to))] [(Sub l r) (Sub (subst l from to) (subst r from to))] [(Mul l r) (Mul (subst l from to) (subst r from to))] [(Div l r) (Div (subst l from to) (subst r from to))] [(Id name) (if (eq? name from) to expr)] [(With bound-id named-expr bound-body) (With bound-id (subst named-expr from to) (if (eq? bound-id from) bound-body (subst bound-body from to)))] [(Call l r) (Call (subst l from to) (subst r from to))] [(Fun bound-id bound-body) (if (eq? bound-id from) expr (Fun bound-id (subst bound-body from to)))]))(: Num->number : FLANG -> Number);; convert a FLANG number to a Racket one(define (Num->number e) (cases e [(Num n) n] [else (error 'arith-op "expected a number, got: ~s" e)]))(: arith-op : (Number Number -> Number) FLANG FLANG -> FLANG);; gets a Racket numeric binary operator, and uses it within a FLANG;; `Num' wrapper(define (arith-op op expr1 expr2) (Num (op (Num->number expr1) (Num->number expr2))))(: eval : FLANG -> FLANG);; evaluates FLANG expressions by reducing them to *expressions*(define (eval expr) (cases expr [(Num n) expr] [(Add l r) (arith-op + (eval l) (eval r))] [(Sub l r) (arith-op - (eval l) (eval r))] [(Mul l r) (arith-op * (eval l) (eval r))] [(Div l r) (arith-op / (eval l) (eval r))] [(With bound-id named-expr bound-body) (eval (subst bound-body bound-id (eval named-expr)))] [(Id name) (error 'eval "free identifier: ~s" name)] [(Fun bound-id bound-body) expr] [(Call fun-expr arg-expr) (let ([fval (eval fun-expr)]) (cases fval [(Fun bound-id bound-body) (eval (subst bound-body bound-id (eval arg-expr)))] [else (error 'eval "`call' expects a function, got: ~s" fval)]))]))(: run : String -> Number);; evaluate a FLANG program contained in a string(define (run str) (let ([result (eval (parse str))]) (cases result [(Num n) n] [else (error 'run "evaluation returned a non-number: ~s" result)])));; tests(test (run "{call {fun {x} {+ x 1}} 4}") => 5)(test (run "{with {add3 {fun {x} {+ x 3}}} {call add3 1}}") => 4)(test (run "{with {add3 {fun {x} {+ x 3}}} {with {add1 {fun {x} {+ x 1}}} {with {x 3} {call add1 {call add3 x}}}}}") => 7)(test (run "{with {add {fun {x} {fun {y} {+ x y}}}} {call {call add 8} 9}}") => 17)(test (run "{with {identity {fun {x} x}} {with {foo {fun {x} {+ x 1}}} {call {call identity foo} 123}}}") => 124)(test (run "{call {call {fun {x} {call x 1}} {fun {x} {fun {y} {+ x y}}}} 123}") => 124)
介绍 Racket 的lambda,周二,1 月 31 日
fun和lambda之间的区别,lambda 和简单值之间不能使用let进行递归函数,let*作为带有 lambda 的派生形式在 Racket 中的 let –> 可以是一个派生形式if如何用来实现and和or作为派生形式
牛顿式语法与 lambda 表达式。
不要被误导,认为 Racket 的语法与其unique功能之间存在虚假的联系... 事实上,它不是唯一具有此功能的语言。例如,这个:
(define (f g) (g 2 3))(f +) ==> 5(f *) ==> 6(f (lambda (x y) (+ (square x) (square y)))) ==> 13
可以像这样用 JavaScript 写:
function f(g) { return g(2,3); }function square(x) { return x*x; }console.log(f(function (x,y) { return square(x) + square(y); }));
在 Perl 中:
sub f { my ($g) = @_; return $g->(2,3); }sub square { my ($x) = @_; return $x * $x; }print f(sub { my ($x, $y) = @_; return square($x) + square($y); });
在 Ruby 中:
def f(g) g.call(2,3) enddef square(x) x*x endputs f(lambda{|x,y| square(x) + square(y)})
等等。即使Java 也有 lambda 表达式,最近C++也添加了它们。
使用函数作为对象,星期二,1 月 31 日
Racket 的一个非常重要的方面 — 使用“高阶”函数 — 即获取和返回函数的函数。这里是一个非常简单的例子:
(define (f x) (lambda () x))(define a (f 2))(a) --> 2(define b (f 3))(b) --> 3
注意:我们实际上得到的是一个记住(通过我们正在进行的替换)一个数字的对象。怎么样:
(define aa (f a))(aa) --> #<procedure> (this is a)((aa)) --> 2
将这个想法提升到下一个级别:
(define (kons x y) (lambda (b) (if b x y)))(define (kar p) (p #t))(define (kdr p) (p #f))(define a (kons 1 2))(define b (kons 3 4))(list (kar a) (kdr a))(list (kar b) (kdr b))
或者,带有类型的版本:
(: kons : (All (A B) A B -> (Boolean -> (U A B))))(define (kons x y) (lambda (b) (if b x y)))(: kar : (All (T) (Boolean -> T) -> T))(define (kar p) (p #t))(: kdr : (All (T) (Boolean -> T) -> T))(define (kdr p) (p #f))(define a (kons 1 2))(define b (kons 3 4))(list (kar a) (kdr a))(list (kar b) (kdr b))
更进一步 — 为什么内部函数应该期望一个布尔值并选择返回什么?我们可以简单地期望一个接受两个值并返回一个值的函数:
(define (kons x y) (lambda (s) (s x y)))(define (kar p) (p (lambda (x y) x)))(define (kdr p) (p (lambda (x y) y)))(define a (kons 1 2))(define b (kons 3 4))(list (kar a) (kdr a))(list (kar b) (kdr b))
而且一个带有类型的版本,使用我们自己的构造函数使其稍微不那么痛苦:
(define-type (Kons A B) = ((A B -> (U A B)) -> (U A B)))(: kons : (All (A B) A B -> (Kons A B)))(define (kons x y) (lambda (s) (s x y)))(: kar : (All (A B) (Kons A B) -> (U A B)))(define (kar p) (p (lambda (x y) x)))(: kdr : (All (A B) (Kons A B) -> (U A B)))(define (kdr p) (p (lambda (x y) y)))(define a (kons 1 2))(define b (kons 3 4))(list (kar a) (kdr a))(list (kar b) (kdr b))
注意Kons类型定义与以下相同:
(define-type Kons = (All (A B) (A B -> (U A B)) -> (U A B)))
因此,All对多态类型定义来说就像lambda对函数定义一样。
最后在 JavaScript 中:
function kons(x,y) { return function(s) { return s(x, y); } }function kar(p) { return p(function(x,y){ return x; }); }function kdr(p) { return p(function(x,y){ return y; }); }a = kons(1,2);b = kons(3,4);console.log('a = <' + kar(a) + ',' + kdr(a) + '>' );console.log('b = <' + kar(b) + ',' + kdr(b) + '>' );
或者使用 ES6 箭头函数,函数定义变为:
var kons = (x,y) => s => s(x,y);var kar = p => p((x,y) => x);var kdr = p => p((x,y) => y);
柯里化星期二,一月 31 日
一个柯里化函数是一个函数,它不是接受两个(或更多)参数,而是只接受一个参数并返回一个接受剩余参数的函数。例如:
(: plus : Number -> (Number -> Number))(define (plus x) (lambda (y) (+ x y)))
编写用于在普通版本和柯里化版本之间进行转换的函数非常容易。
(define (currify f) (lambda (x) (lambda (y) (f x y))))
有例子的那种类型版本:
(: currify : (All (A B C) (A B -> C) -> (A -> (B -> C))));; convert a double-argument function to a curried one(define (currify f) (lambda (x) (lambda (y) (f x y))))(: add : Number Number -> Number)(define (add x y) (+ x y))(: plus : Number -> (Number -> Number))(define plus (currify add))(test ((plus 1) 2) => 3)(test (((currify add) 1) 2) => 3)(test (map (plus 1) '(1 2 3)) => '(2 3 4))(test (map ((currify add) 1) '(1 2 3)) => '(2 3 4))(test (map ((currify +) 1) '(1 2 3)) => '(2 3 4))
用法 — 像map这样的高阶函数很常见,我们希望固定一个参数。
当处理这样的高阶代码时,类型非常有帮助,因为每个箭头对应一个函数:
(: currify : (All (A B C) (A B -> C) -> (A -> (B -> C))))
将->函数类型右结合是很常见的,所以你会发现这种类型写成:
currify : (A B -> C) -> (A -> B -> C)
或者甚至是
currify : (A B -> C) -> A -> B -> C
但这可能会有点令人困惑…
使用高阶和匿名函数
假设我们有一个用于估计函数在特定点上的导数的函数:
(define dx 0.01)(: deriv : (Number -> Number) Number -> Number);; compute the derivative of `f' at the given point `x'(define (deriv f x) (/ (- (f (+ x dx)) (f x)) dx))(: integrate : (Number -> Number) Number -> Number);; compute an integral of `f' at the given point `x'(define (integrate f x) (: loop : Number Number -> Number) (define (loop y acc) (if (> y x) (/ acc dx) (loop (+ y dx) (+ acc (f y))))) (loop 0 0))
假设我们想要尝试各种函数,给定一些绘制数值函数图形的plot函数,例如:
(plot sin)
问题在于plot期望一个单一的(Number -> Number)函数——如果我们想要尝试使用导数,我们可以这样做:
(: sin-deriv : Number -> Number);; the derivative of sin(define sin-deriv (lambda (x) (deriv sin x)))(plot sin-deriv)
但是这将非常快速变得非常乏味——使用匿名函数要简单得多:
(plot (lambda (x) (deriv sin x)))
我们甚至可以通过将一个已知函数与其导数进行比较来验证我们的导数是否正确
(plot (lambda (x) (- (deriv sin x) (cos x))))
但是这仍然不完全自然地做这些事情——您需要明确地组合函数,这并不太方便。 而不是这样做,我们可以编写将使用功能输入和输出的 H.O. 函数。 例如,我们可以编写一个函数来减去函数:
(: fsub : (Number -> Number) (Number -> Number) -> (Number -> Number));; subtracts two numeric 1-argument functions(define (fsub f g) (lambda (x) (- (f x) (g x))))
导数也是如此:
(: fderiv : (Number -> Number) -> (Number -> Number));; compute the derivative function of `f'(define (fderiv f) (lambda (x) (deriv f x)))
现在我们可以以更简单的方式尝试相同的内容:
(plot (fsub (fderiv sin) cos))
更重要的是——我们的fderiv可以自动从deriv创建:
(: currify : (All (A B C) (A B -> C) -> (A -> B -> C)));; convert a double-argument function to a curried one(define (currify f) (lambda (x) (lambda (y) (f x y))))(: fderiv : (Number -> Number) -> (Number -> Number));; compute the derivative function of `f'(define fderiv (currify deriv))
与fsub相同的原则:我们可以编写一个将二进制算术函数转换为对一元数值函数进行操作的函数。 但是为了使事情更易于阅读,我们可以为一元和二元数值函数定义新类型:
(define-type UnaryFun = (Number -> Number))(define-type BinaryFun = (Number Number -> Number))(: binop->fbinop : BinaryFun -> (UnaryFun UnaryFun -> UnaryFun));; turns an arithmetic binary operator to a function operator(define (binop->fbinop op) (lambda (f g) (lambda (x) (op (f x) (g x)))))(: fsub : UnaryFun UnaryFun -> UnaryFun);; functional pointwise subtraction(define fsub (binop->fbinop -))
我们可以用任何东西来做这件事——开发一个丰富的函数和函数式(对函数的函数)库非常容易... 这是一个相当广泛但非常简短的函数库:
#lang pl untyped(define (currify f) (lambda (x) (lambda (y) (f x y))))(define (binop->fbinop op) (lambda (f g) (lambda (x) (op (f x) (g x)))))(define (compose f g) (lambda (x) (f (g x))))(define dx 0.01)(define (deriv f x) (/ (- (f (+ x dx)) (f x)) dx))(define (integrate f x) (define over (if (< x 0) < >)) (define step (if (< x 0) - +)) (define add (if (< x 0) - +)) (define (loop y acc) (if (over y x) (* acc dx) (loop (step y dx) (add acc (f y))))) (loop 0 0))(define fadd (binop->fbinop +))(define fsub (binop->fbinop -))(define fmul (binop->fbinop *))(define fdiv (binop->fbinop /))(define fderiv (currify deriv))(define fintegrate (currify integrate));; ...
这是用类语言的“无类型方言”编写的,但现在应该很容易添加类型。
例子:
;; want to verify that `integrate' is the opposite of `deriv':;; take a function, subtract it from its derivative's integral(plot (fsub sin (fintegrate (fderiv sin))));; want to magnify the errors? -- here's how you magnify:(plot (compose ((currify *) 5) sin));; so:(plot (compose ((currify *) 20) (fsub sin (fintegrate (fderiv sin)))))
旁注:“点无关”组合器
在不使用
lambda(或使用define语法糖隐式使用lambda)的情况下形成函数称为点无关样式。 它在 Haskell 中特别受欢迎,因为由于隐式柯里化和大量的高阶函数组合器,以这种方式形成函数更容易。 如果过度使用,它很容易导致代码混淆。
所有这些都类似于运行时代码生成,但实际上并不是。 fderiv唯一要做的就是取一个函数并将其存储在返回的函数中,然后当该函数接收一个数字时,它使用存储的函数并将其发送给 deriv 以进行计算。 我们可以简单地将 deriv 写成fderiv所表示的——这才是真正的导数函数:
(define (deriv f) (lambda (x) (/ (- (f (+ x dx)) (f x)) dx)))
但是再次注意,这与普通的deriv既不更快也不更慢。 但是,在某些情况下,我们可以在第一阶段参数上执行一些计算,从而节省第二阶段的工作。 这是一个夸张的例子——我们想要一个函数,它接收两个输入x、y并返回fib(x)*y,但我们必须使用一个愚蠢的fib:
(define (fib n) (if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2)))))
我们想要的函数是:
(define (bogus x y) (* (fib x) y))
如果我们像往常一样柯里化它(或者只是使用currify),我们会得到:
(define (bogus x) (lambda (y) (* (fib x) y)))
并多次尝试此操作:
(define bogus24 (bogus 24))(map bogus24 '(1 2 3 4 5))
但是在bogus的定义中,请注意(fib x)不依赖于y——所以我们可以稍微以不同的方式重写它:
(define (bogus x) (let ([fibx (fib x)]) (lambda (y) (* fibx y))))
现在再次尝试上述方法会快得多:
(define bogus24 (bogus 24))(map bogus24 '(1 2 3 4 5))
替换缓存
PLAI §5(在此称为“延迟替换”)
使用替换进行评估非常低效 - 在每个作用域,我们都会复制程序 AST 的一部分。这包括所有函数调用,这意味着成本不切实际(函数调用应该是廉价的!)。
要克服这个问题,我们希望使用替换缓存。
基本思想:我们开始时没有缓存的替换进行评估,然后在遇到绑定时收集它们。
对我们的评估器意味着另一个改变:在那时我们并不真正替换缓存。
实现缓存功能星期二,一月三十一日
首先,我们需要一个替换缓存的类型。为此,我们将使用一个由两个元素组成的列表的列表 —— 一个名称和其值 FLANG:
;; a type for substitution caches:(define-type SubstCache = (Listof (List Symbol FLANG)))
我们需要有一个空的替换缓存,一个扩展它的方法,以及一个查找事物的方法:
(: empty-subst : SubstCache)(define empty-subst null)(: extend : Symbol FLANG SubstCache -> SubstCache);; extend a given substitution cache with a new mapping(define (extend id expr sc) (cons (list id expr) sc))(: lookup : Symbol SubstCache -> FLANG);; lookup a symbol in a substitution cache, return the value it is;; bound to (or throw an error if it isn't bound)(define (lookup name sc) (cond [(null? sc) (error 'lookup "no binding for ~s" name)] [(eq? name (first (first sc))) (second (first sc))] [else (lookup name (rest sc))]))
实际上,使用这样的列表列表的原因是 Racket 有一个内置函数叫做 assq,它将执行这种搜索(assq 是在关联列表中使用 eq? 进行键比较的搜索)。这是一个使用 assq 的 lookup 版本:
(define (lookup name sc) (let ([cell (assq name sc)]) (if cell (second cell) (error 'lookup "no binding for ~s" name))))
缓存替换的正式规则星期二,1 月 31 日
现在正式评估规则有所不同。评估携带一个替换缓存,它在开始时为空:因此eval需要额外的参数。我们首先编写处理缓存的规则,并为简单起见使用上述函数名称 - 这三个定义的行为可以总结为一个lookup规则:
lookup(x,empty-subst) = error!lookup(x,extend(x,E,sc)) = Elookup(x,extend(y,E,sc)) = lookup(x,sc) if `x' is not `y'
现在我们可以为eval写新规则了。
eval(N,sc) = Neval({+ E1 E2},sc) = eval(E1,sc) + eval(E2,sc)eval({- E1 E2},sc) = eval(E1,sc) - eval(E2,sc)eval({* E1 E2},sc) = eval(E1,sc) * eval(E2,sc)eval({/ E1 E2},sc) = eval(E1,sc) / eval(E2,sc)eval(x,sc) = lookup(x,sc)eval({with {x E1} E2},sc) = eval(E2,extend(x,eval(E1,sc),sc))eval({fun {x} E},sc) = {fun {x} E}eval({call E1 E2},sc) = eval(Ef,extend(x,eval(E2,sc),sc)) if eval(E1,sc) = {fun {x} Ef} = error! otherwise
请注意,没有提到subst - 整个重点是我们实际上并没有进行替换,而是使用缓存。lookup规则以及使用extend的地方替换了subst,因此指定了我们的作用域规则。
还要注意,call的规则仍然与with的规则非常相似,但看起来我们丢失了一些东西 - 替换到fun表达式中的有趣部分。
使用替换缓存进行评估星期二,2 月 7 日
现在实现新的eval很容易——它的扩展方式与正式的eval规则扩展方式相同:
(: eval : FLANG SubstCache -> FLANG);; evaluates FLANG expressions by reducing them to expressions(define (eval expr sc) (cases expr [(Num n) expr] [(Add l r) (arith-op + (eval l sc) (eval r sc))] [(Sub l r) (arith-op - (eval l sc) (eval r sc))] [(Mul l r) (arith-op * (eval l sc) (eval r sc))] [(Div l r) (arith-op / (eval l sc) (eval r sc))] [(With bound-id named-expr bound-body) (eval bound-body (extend bound-id (eval named-expr sc) sc))] [(Id name) (lookup name sc)] [(Fun bound-id bound-body) expr] [(Call fun-expr arg-expr) (let ([fval (eval fun-expr sc)]) (cases fval [(Fun bound-id bound-body) (eval bound-body (extend bound-id (eval arg-expr sc) sc))] [else (error 'eval "`call' expects a function, got: ~s" fval)]))]))
再次注意,我们不再需要subst,但代码的其余部分(数据类型定义、解析和arith-op)完全相同。
最后,我们需要确保最初使用空缓存调用eval。这在我们的主要run入口点中很容易更改:
(: run : String -> Number);; evaluate a FLANG program contained in a string(define (run str) (let ([result (eval (parse str) empty-subst)]) (cases result [(Num n) n] [else (error 'run "evaluation returned a non-number: ~s" result)])))
完整的代码(包括相同的测试,但暂时不包括正式规则)如下。请注意,一个测试未通过。
#lang pl(define-type FLANG [Num Number] [Add FLANG FLANG] [Sub FLANG FLANG] [Mul FLANG FLANG] [Div FLANG FLANG] [Id Symbol] [With Symbol FLANG FLANG] [Fun Symbol FLANG] [Call FLANG FLANG])(: parse-sexpr : Sexpr -> FLANG);; parses s-expressions into FLANGs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: name)) body) (Fun name (parse-sexpr body))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [(list 'call fun arg) (Call (parse-sexpr fun) (parse-sexpr arg))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> FLANG);; parses a string containing a FLANG expression to a FLANG AST(define (parse str) (parse-sexpr (string->sexpr str)));; a type for substitution caches:(define-type SubstCache = (Listof (List Symbol FLANG)))(: empty-subst : SubstCache)(define empty-subst null)(: extend : Symbol FLANG SubstCache -> SubstCache);; extend a given substitution cache with a new mapping(define (extend name val sc) (cons (list name val) sc))(: lookup : Symbol SubstCache -> FLANG);; lookup a symbol in a substitution cache, return the value it is;; bound to (or throw an error if it isn't bound)(define (lookup name sc) (let ([cell (assq name sc)]) (if cell (second cell) (error 'lookup "no binding for ~s" name))))(: Num->number : FLANG -> Number);; convert a FLANG number to a Racket one(define (Num->number e) (cases e [(Num n) n] [else (error 'arith-op "expected a number, got: ~s" e)]))(: arith-op : (Number Number -> Number) FLANG FLANG -> FLANG);; gets a Racket numeric binary operator, and uses it within a FLANG;; `Num' wrapper(define (arith-op op expr1 expr2) (Num (op (Num->number expr1) (Num->number expr2))))(: eval : FLANG SubstCache -> FLANG);; evaluates FLANG expressions by reducing them to expressions(define (eval expr sc) (cases expr [(Num n) expr] [(Add l r) (arith-op + (eval l sc) (eval r sc))] [(Sub l r) (arith-op - (eval l sc) (eval r sc))] [(Mul l r) (arith-op * (eval l sc) (eval r sc))] [(Div l r) (arith-op / (eval l sc) (eval r sc))] [(With bound-id named-expr bound-body) (eval bound-body (extend bound-id (eval named-expr sc) sc))] [(Id name) (lookup name sc)] [(Fun bound-id bound-body) expr] [(Call fun-expr arg-expr) (let ([fval (eval fun-expr sc)]) (cases fval [(Fun bound-id bound-body) (eval bound-body (extend bound-id (eval arg-expr sc) sc))] [else (error 'eval "`call' expects a function, got: ~s" fval)]))]))(: run : String -> Number);; evaluate a FLANG program contained in a string(define (run str) (let ([result (eval (parse str) empty-subst)]) (cases result [(Num n) n] [else (error 'run "evaluation returned a non-number: ~s" result)])));; tests(test (run "{call {fun {x} {+ x 1}} 4}") => 5)(test (run "{with {add3 {fun {x} {+ x 3}}} {call add3 1}}") => 4)(test (run "{with {add3 {fun {x} {+ x 3}}} {with {add1 {fun {x} {+ x 1}}} {with {x 3} {call add1 {call add3 x}}}}}") => 7)(test (run "{with {identity {fun {x} x}} {with {foo {fun {x} {+ x 1}}} {call {call identity foo} 123}}}") => 124)(test (run "{call {with {x 3} {fun {y} {+ x y}}} 4}") => 7)(test (run "{with {x 3} {with {f {fun {y} {+ x y}}} {with {x 5} {call f 4}}}}") => "???")(test (run "{call {call {fun {x} {call x 1}} {fun {x} {fun {y} {+ x y}}}} 123}") => 124)
动态和词法作用域
这看起来应该可以,甚至在一些例子中运行正常,除了一个很难理解的。似乎我们有一个 bug...
现在我们遇到了一个棘手的问题,它成功地成为了很多语言实现者的问题,包括 Lisp 的第一个版本。让我们尝试运行以下表达式 - 试着弄清楚它将求值为什么:
(run "{with {x 3} {with {f {fun {y} {+ x y}}} {with {x 5} {call f 4}}}}")
我们期望它返回7(至少我是这样认为的!),但我们得到了9... 问题是 - 它应该返回9吗?
我们到达的是所谓的动态作用域。作用域由动态运行时环境确定(由我们的替换缓存表示)。这 几乎总是 不可取的,我希望能说服你。
在我们开始之前,我们为编程语言定义了两个选项:
-
静态作用域(也称为词法作用域):在一个具有静态作用域的语言中,每个标识符都从其定义的作用域获取其值,而不是其使用的作用域。
-
动态作用域:在具有动态作用域的语言中,每个标识符都从其使用的作用域获取其值,而不是其定义的作用域。
Racket 使用词法作用域,我们的新求值器使用动态作用域,旧的基于替换的求值器是静态的等等。
作为一个旁注,Lisp 开始其生命为一个动态作用域的语言。这些遗留的问题被(某种程度上)视为一个实现 bug。当 Scheme 被引入时,它是第一个使用严格词法作用域的 Lisp 方言,而 Racket 显然也是这样做的。(一些 Lisp 实现对解释代码使用动态作用域,对编译代码使用词法作用域!)事实上,Emacs Lisp 是唯一 活跃 的 Lisp 方言,默认情况下仍然具有动态作用域。要看到这一点,可以将以上代码的版本与 Racket 中的版本进行比较:
(let ((x 3)) (let ((f (lambda (y) (+ x y)))) (let ((x 5)) (f 4))))
和 Emacs Lisp 版本(看起来几乎相同):
(let ((x 3)) (let ((f (lambda (y) (+ x y)))) (let ((x 5)) (funcall f 4))))
当我们在路径上使用另一个函数时也会发生这种情况:
(defun blah (func val) (funcall func val))(let ((x 3)) (let ((f (lambda (y) (+ x y)))) (let ((x 5)) (blah f 4))))
请注意,重命名标识符可能会导致不同的代码 - 将val改为x:
(defun blah (func x) (funcall func x))(let ((x 3)) (let ((f (lambda (y) (+ x y)))) (let ((x 5)) (blah f 4))))
你得到了8,因为参数名改变了内部函数所见的x!
还考虑一下这个 Emacs Lisp 函数:
(defun return-x () x)
它本身没有意义(x未绑定),
(return-x)
但可以使用let给出一个动态含义:
(let ((x 5)) (return-x))
或者一个函数应用:
(defun foo (x) (return-x))(foo 5)
课程语言中还有一种动态作用域语言:
#lang pl dynamic(define x 123)(define (getx) x)(define (bar1 x) (getx))(define (bar2 y) (getx))(test (getx) => 123)(test (let ([x 456]) (getx)) => 456)(test (getx) => 123)(test (bar1 999) => 999)(test (bar2 999) => 123)(define (foo x) (define (helper) (+ x 1)) helper)(test ((foo 0)) => 124);; and *much* worse:(define (add x y) (+ x y))(test (let ([+ *]) (add 6 7)) => 42)
注意最后一个例子有多糟糕:你基本上不能调用任何函数并预先知道它会做什么。
有一些情况下,动态作用域可能会很有用,因为它允许你“远程”定制任何代码片段。一个典型的例子就是 Emacs:最初,它是基于一种古老的 Lisp 方言,该方言仍然具有动态作用域,但即使几乎所有的 Lisp 方言都默认采用词法作用域,Emacs 仍保留了这一特性。原因是动态作用域的危险也是使系统非常开放的一种方式,几乎任何东西都可以通过“远程”更改来定制。下面是一个类似动态作用域用法的具体示例,它使系统非常易于修改和开放:
#lang pl dynamic(define tax% 6.5)(define (with-tax n) (+ n (* n (/ tax% 100))))(with-tax 10) ; how much do we pay?(let ([tax% 18.0]) (with-tax 10)) ; how much would we pay in Israel?;; make that into a function(define il-tax% 18.0)(define (us-over-il-saving n) (- (let ([tax% il-tax%]) (with-tax n)) (with-tax n)))(us-over-il-saving 10);; can even control that: how much would we save if;; the tax in israel went down one percent?(let ([il-tax% (- il-tax% 1)]) (us-over-il-saving 10))
显然,这种定制一切的能力也是代码没有任何保证的主要问题的根源。在这两种世界中找到最佳解决方案的常见方法是拥有可控的动态作用域。例如,Common Lisp 默认情况下在所有地方都具有词法作用域,但一些变量可以声明为special,这意味着它们是动态作用域的。主要问题在于,你无法通过查看使用它的代码来确定变量是否是 special,因此更流行的方法是 Racket 中使用的方法:所有绑定始终具有词法作用域,但有parameters,它们是一种动态作用域的值容器 —— 但它们绑定到纯粹(词法作用域)的标识符上。下面是将上述代码翻译为 Racket 并使用参数的相同代码:
#lang racket(define tax% (make-parameter 6.5)) ; create the dynamic container(define (with-tax n) (+ n (* n (/ (tax%) 100)))) ; note how its value is accessed(with-tax 10) ; how much do we pay?(parameterize ([tax% 18.0]) (with-tax 10)) ; not a `let';; make that into a function(define il-tax% (make-parameter 18.0))(define (us-over-il-saving n) (- (parameterize ([tax% (il-tax%)]) (with-tax n)) (with-tax n)))(us-over-il-saving 10)(parameterize ([il-tax% (- (il-tax%) 1)]) (us-over-il-saving 10))
这里的主要观点是动态作用域值被使用的地方在程序员的控制之下 —— 例如,你无法“定制” - 在做什么。这给了我们我们喜欢拥有的保证(= 代码能够正常工作),但当然这些点是预先确定的,不像一个可以定制一切的环境,包括那些意外有用的东西。
顺带一提,在经过几十年的辩论之后,Emacs 最终在其核心语言中添加了词法作用域,但这仍然由一个标志来确定 —— 一个全局的
lexical-binding变量。
动态与词法作用域 Tuesday, February 7th
再回到动态或词法作用域的讨论:
-
最重要的事实是,我们希望将程序视为由正常的替换求值器执行。我们最初的动机是仅优化评估 - 而不是更改语义!由此可见,我们希望此优化的结果行为方式相同。我们只需要评估:
(run "{with {x 3} {with {f {fun {y} {+ x y}}} {with {x 5} {call f 4}}}}")在原始求值器中,以确信
7应该是正确的结果(还要注意,相同的代码在转换为 Racket 时评估为7)。(然而,这是一个非常重要的优化,没有它很多程序变得太慢而不可行,所以你可能会声称你对修改后的语义很满意...)
-
它不允许将函数用作对象,例如,我们已经看到我们对于对(pairs)有一个函数表示:
(define (kons x y) (lambda (n) (match n ['first x] ['second y] [else (error ...)])))(define my-pair (kons 1 2))如果在动态作用域语言中评估这个,我们会得到一个函数作为结果,但绑定到
x和y的值现在消失了!使用替换模型我们替换了这些值,但现在它们只存在于一个不再有任何条目的缓存中...同样,柯里化也不起作用,我们漂亮的
deriv函数也不起作用等等等等。 -
使推理变得不可能,因为任何一段代码的行为方式在运行时是无法预测的。例如,如果在 Racket 中使用动态作用域,则无法知道此函数在做什么:
(define (foo) x)目前,它将导致运行时错误,但如果您这样调用它:
(let ([x 1]) (foo))然后它将返回
1,如果您稍后执行此操作:(define (bar x) (foo))(let ([x 1]) (bar 2))然后你会得到
2!这些问题在 Emacs Lisp 中也可以演示,但 Racket 更进一步 - 它使用相同的规则来评估函数及其值(Lisp 使用不同的名称空间来表示函数)。因此,您甚至不能依赖以下函数:
(define (add x y) (+ x y))总是添加
x和y! - 与上述类似的例子:(let ([+ -]) (add 1 2))将返回
-1! -
许多所谓的“脚本”语言以动态作用域开始它们的生活。正如我们所见,主要原因是实现它非常简单(不,在现实世界中没有人做替换!(好吧,几乎没有人...))。
另一个原因是,如果你想在像 Racket 中一样使用函数作为对象,那么这些问题将使生活变得不可能,所以你会很快注意到它们 - 但在一个没有一等函数的“普通”语言中,问题并不那么明显。
-
例如,bash 有
local变量,但它们具有动态作用域:x="the global x"print_x() { echo "The current value of x is \"$x\""; }foo() { local x="x from foo"; print_x; }print_x; foo; print_xPerl 以对于声明为
local的变量具有动态作用域开始其生活:$x="the global x";sub print_x { print "The current value of x is \"$x\"\n"; }sub foo { local($x); $x="x from foo"; print_x; }print_x; foo; print_x;面对这个问题,“Perl 的方式”显然不是删除或修复功能,而是堆叠它们 - 所以
local仍然以这种方式行为,现在有了一个my声明,它实现了适当的词法作用域(每个严肃的 Perl 程序员都知道你应该始终使用my)...还有其他一些语言的示例发生了变化,以及希望发生变化的语言(例如,没有人喜欢 Emacs Lisp 中的动态作用域,但现在已经有太多的代码了)。
-
这仍然是一个棘手的问题,就像任何与绑定相关的问题一样。例如,我很快就通过搜索找到了一篇关于 Python 的博文,其中对“动态作用域”是什么感到困惑... 它声称 Python 使用动态作用域(搜索“Python uses dynamic as opposed to lexical scoping”),但 Python 始终使用词法作用域规则,可以通过将它们的代码转换为 Racket 来看到(在此计算中忽略副作用):
(define (orange-juice) (* x 2))(define x 3)(define y (orange-juice)) ; y is now 6(define x 1)(define y (orange-juice)) ; y is now 2或者尝试在 Python 中执行此操作:
def orange_juice(): return x*2def foo(x): return orange_juice()foo(2)Python 的真正问题(2.1 之前,以及没有这个有趣的
from __future__ import nested_scope行)是它没有创建闭包,我们很快会谈论到。
-
另一个示例,表明容易搞乱你的作用域的是以下 Ruby bug — 在
irb中运行:% irbirb(main):001:0> x = 0=> 0irb(main):002:0> lambda{|x| x}.call(5)=> 5irb(main):003:0> x=> 5(这是由于变量的奇怪作用域规则而导致的错误,这在较新版本的 Ruby 中已修复。有关详细信息,请参阅此处的 Ruby rant,或阅读有关Ruby 和不受欢迎惊喜原则的其他宝石。
-
另一件要考虑的事情是编译是基于程序的词法结构进行的,因为编译器实际上从不运行代码。这意味着动态作用域使得编译几乎不可能。
-
动态作用域也有一些优点。两个值得注意的优点是:
-
动态作用域使得在调用代码片段的范围内轻松地改变“配置变量”变得容易(例如,在 Emacs 中广泛使用)。问题在于通常我们希望控制以这种方式“可配置”的变量,而词法作用域语言通常选择了一个单独的工具来处理这些问题。换句话说,动态作用域的问题在于 所有 变量都是可修改的。
对函数也可以说同样的话:有时候动态更改函数是可取的(例如,参见“面向方面的编程”),但如果没有控制,而且所有函数都可以更改,那么我们将得到一个没有代码可以可靠的世界。
-
它使递归立即可用 - 例如,
{with {f {fun {x} {call f x}}} {call f 0}}是一个在动态作用域语言中的无限循环。但在词法作用域语言中,我们需要做更多工作来使递归正常运行。
-
实现词法作用域:闭包和环境
那么我们如何解决这个问题呢?
让我们回到问题的根源:新的求值器的行为与替换求值器不同。在旧的求值器中,很容易看出函数如何作为记住值的对象。例如,当我们这样做时:
{with {x 1} {fun {y} {+ x y}}}
结果是一个函数值,实际上是这个的语法对象:
{fun {y} {+ 1 y}}
现在如果我们从其他地方调用这个函数,比如:
{with {f {with {x 1} {fun {y} {+ x y}}}} {with {x 2} {call f 3}}}
很明显结果会是什么:f 绑定到一个将 1 添加到其输入的函数,所以在上述情况下,对x的后期绑定根本没有任何效果。
但是对于缓存求值器,值
{with {x 1} {fun {y} {+ x y}}}
简单地是:
{fun {y} {+ x y}}
并且没有地方保存 1 — 这就是我们问题的根源。(这也是人们怀疑在 Racket 和任何其他函数式语言中使用 lambda 涉及一些低效的代码重新编译魔法的原因。)事实上,我们可以通过检查返回值来验证这一点,并且看到它确实包含一个自由标识符。
显然,我们需要创建一个包含主体和参数列表的对象,就像函数语法对象一样——但我们不做任何替换,所以除了主体和参数名,我们还需要记住我们仍然需要用1替换x。这意味着我们需要知道的信息是:
- formal argument(s): {y}- body: {+ x y}- pending substitutions: [1/x]
最后那一点是缺少的1。生成的对象称为闭包,因为它将函数体封闭在仍然挂起的替换上(它的环境)。
所以,第一个改变是函数的值,它现在需要所有这些部分,不像语法对象的Fun情况。
需要更改的第二个地方是函数调用时。当我们完成评估call参数(函数值和参数值)但在应用函数之前,我们有两个值 —— 这时候当前替换缓存已经没有用处了:我们已经完成了对当前表达式所有必要的替换处理 —— 我们现在继续评估函数体,用形式参数和给定的实际值进行新的替换。但是函数体本身是我们以前的相同的函数体 —— 它具有挂起的替换,我们仍然没有做。
重写评估规则——所有规则都是相同的,只有评估fun形式和call形式不同:
eval(N,sc) = Neval({+ E1 E2},sc) = eval(E1,sc) + eval(E2,sc)eval({- E1 E2},sc) = eval(E1,sc) - eval(E2,sc)eval({* E1 E2},sc) = eval(E1,sc) * eval(E2,sc)eval({/ E1 E2},sc) = eval(E1,sc) / eval(E2,sc)eval(x,sc) = lookup(x,sc)eval({with {x E1} E2},sc) = eval(E2,extend(x,eval(E1,sc),sc))eval({fun {x} E},sc) = <{fun {x} E}, sc>eval({call E1 E2},sc1) = eval(Ef,extend(x,eval(E2,sc1),sc2)) if eval(E1,sc1) = <{fun {x} Ef}, sc2> = error! otherwise
顺便提一下,这些替换缓存现在不仅仅是“只是一个缓存” —— 它们实际上保存了一个环境,在这个环境中应该对表达式进行评估。所以我们现在会切换到常见的环境名称:
eval(N,env) = Neval({+ E1 E2},env) = eval(E1,env) + eval(E2,env)eval({- E1 E2},env) = eval(E1,env) - eval(E2,env)eval({* E1 E2},env) = eval(E1,env) * eval(E2,env)eval({/ E1 E2},env) = eval(E1,env) / eval(E2,env)eval(x,env) = lookup(x,env)eval({with {x E1} E2},env) = eval(E2,extend(x,eval(E1,env),env))eval({fun {x} E},env) = <{fun {x} E}, env>eval({call E1 E2},env1) = eval(Ef,extend(x,eval(E2, env1),env2)) if eval(E1,env1) = <{fun {x} Ef}, env2> = error! otherwise
如果你觉得这个更容易理解,评估call的“平面算法”如下:
1\. f := evaluate E1 in env12\. if f is not a <{fun ...},...> closure then error!3\. x := evaluate E2 in env14\. new_env := extend env_of(f) by mapping arg_of(f) to x5\. evaluate (and return) body_of(f) in new_env
注意这个定义隐含的作用域规则与替换为基础的规则所暗示的作用域规则是匹配的。(应该可以证明它们是相同的。)
代码的更改几乎是微不足道的,除了我们需要一种方式来表示<{fun {x} Ef}, env>对。
这种改变的含义是,我们现在不能再使用相同的类型来表示函数语法和函数值,因为函数值不仅仅是语法。对此有一个简单的解决方案 — 我们现在不再进行任何替换,因此不需要将值转换为表达式 — 我们可以为值提出一个新类型,与抽象语法树的类型分开。
当我们这样做时,我们还将修复使用 FLANG 作为值类型的问题:这仅仅是一种便利,因为 AST 类型包含了我们需要的所有值的情况。 (实际上,你应该已经注意到 Racket 也这样做了:数字、字符串、布尔值等都被程序和语法表示(s-表达式)使用 — 但请注意函数值不在语法中使用。)我们现在将为运行时值实现一个单独的VAL类型。
首先,我们现在需要一个这样的环境类型 — 我们可以使用Listof来表示:
;; a type for environments:(define-type ENV = (Listof (List Symbol VAL)))
但我们同样可以为环境值定义一个新类型:
(define-type ENV [EmptyEnv] [Extend Symbol VAL ENV])
重新实现lookup现在很简单:
(: lookup : Symbol ENV -> VAL);; lookup a symbol in an environment, return its value or throw an;; error if it isn't bound(define (lookup name env) (cases env [(EmptyEnv) (error 'lookup "no binding for ~s" name)] [(Extend id val rest-env) (if (eq? id name) val (lookup name rest-env))]))
… 我们不需要extend,因为我们从类型定义中得到了Extend,我们也得到了(EmptyEnv)而不是empty-subst。
现在我们使用这个新的值类型 — 有两个变体:
(define-type VAL [NumV Number] [FunV Symbol FLANG ENV]) ; arg-name, body, scope
现在是使用新类型并实现词法作用域的eval的新实现:
(: eval : FLANG ENV -> VAL);; evaluates FLANG expressions by reducing them to values(define (eval expr env) (cases expr [(Num n) (NumV n)] [(Add l r) (arith-op + (eval l env) (eval r env))] [(Sub l r) (arith-op - (eval l env) (eval r env))] [(Mul l r) (arith-op * (eval l env) (eval r env))] [(Div l r) (arith-op / (eval l env) (eval r env))] [(With bound-id named-expr bound-body) (eval bound-body (Extend bound-id (eval named-expr env) env))] [(Id name) (lookup name env)] [(Fun bound-id bound-body) (FunV bound-id bound-body env)] [(Call fun-expr arg-expr) (let ([fval (eval fun-expr env)]) (cases fval [(FunV bound-id bound-body f-env) (eval bound-body (Extend bound-id (eval arg-expr env) f-env))] [else (error 'eval "`call' expects a function, got: ~s" fval)]))]))
我们还需要更新arith-op以使用VAL对象。完整的代码如下 — 现在通过了所有测试,包括我们用来发现问题的示例。
▶;; The Flang interpreter, using environments#lang pl#|The grammar: <FLANG> ::= <num> | { + <FLANG> <FLANG> } | { - <FLANG> <FLANG> } | { * <FLANG> <FLANG> } | { / <FLANG> <FLANG> } | { with { <id> <FLANG> } <FLANG> } | <id> | { fun { <id> } <FLANG> } | { call <FLANG> <FLANG> }Evaluation rules: eval(N,env) = N eval({+ E1 E2},env) = eval(E1,env) + eval(E2,env) eval({- E1 E2},env) = eval(E1,env) - eval(E2,env) eval({* E1 E2},env) = eval(E1,env) * eval(E2,env) eval({/ E1 E2},env) = eval(E1,env) / eval(E2,env) eval(x,env) = lookup(x,env) eval({with {x E1} E2},env) = eval(E2,extend(x,eval(E1,env),env)) eval({fun {x} E},env) = <{fun {x} E}, env> eval({call E1 E2},env1) = eval(Ef,extend(x,eval(E2,env1),env2)) if eval(E1,env1) = <{fun {x} Ef}, env2> = error! otherwise|#(define-type FLANG [Num Number] [Add FLANG FLANG] [Sub FLANG FLANG] [Mul FLANG FLANG] [Div FLANG FLANG] [Id Symbol] [With Symbol FLANG FLANG] [Fun Symbol FLANG] [Call FLANG FLANG])(: parse-sexpr : Sexpr -> FLANG);; parses s-expressions into FLANGs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: name)) body) (Fun name (parse-sexpr body))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [(list 'call fun arg) (Call (parse-sexpr fun) (parse-sexpr arg))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> FLANG);; parses a string containing a FLANG expression to a FLANG AST(define (parse str) (parse-sexpr (string->sexpr str)));; Types for environments, values, and a lookup function(define-type ENV [EmptyEnv] [Extend Symbol VAL ENV])(define-type VAL [NumV Number] [FunV Symbol FLANG ENV])(: lookup : Symbol ENV -> VAL);; lookup a symbol in an environment, return its value or throw an;; error if it isn't bound(define (lookup name env) (cases env [(EmptyEnv) (error 'lookup "no binding for ~s" name)] [(Extend id val rest-env) (if (eq? id name) val (lookup name rest-env))]))(: NumV->number : VAL -> Number);; convert a FLANG runtime numeric value to a Racket one(define (NumV->number val) (cases val [(NumV n) n] [else (error 'arith-op "expected a number, got: ~s" val)]))(: arith-op : (Number Number -> Number) VAL VAL -> VAL);; gets a Racket numeric binary operator, and uses it within a NumV;; wrapper(define (arith-op op val1 val2) (NumV (op (NumV->number val1) (NumV->number val2))))(: eval : FLANG ENV -> VAL);; evaluates FLANG expressions by reducing them to values(define (eval expr env) (cases expr [(Num n) (NumV n)] [(Add l r) (arith-op + (eval l env) (eval r env))] [(Sub l r) (arith-op - (eval l env) (eval r env))] [(Mul l r) (arith-op * (eval l env) (eval r env))] [(Div l r) (arith-op / (eval l env) (eval r env))] [(With bound-id named-expr bound-body) (eval bound-body (Extend bound-id (eval named-expr env) env))] [(Id name) (lookup name env)] [(Fun bound-id bound-body) (FunV bound-id bound-body env)] [(Call fun-expr arg-expr) (let ([fval (eval fun-expr env)]) (cases fval [(FunV bound-id bound-body f-env) (eval bound-body (Extend bound-id (eval arg-expr env) f-env))] [else (error 'eval "`call' expects a function, got: ~s" fval)]))]))(: run : String -> Number);; evaluate a FLANG program contained in a string(define (run str) (let ([result (eval (parse str) (EmptyEnv))]) (cases result [(NumV n) n] [else (error 'run "evaluation returned a non-number: ~s" result)])));; tests(test (run "{call {fun {x} {+ x 1}} 4}") => 5)(test (run "{with {add3 {fun {x} {+ x 3}}} {call add3 1}}") => 4)(test (run "{with {add3 {fun {x} {+ x 3}}} {with {add1 {fun {x} {+ x 1}}} {with {x 3} {call add1 {call add3 x}}}}}") => 7)(test (run "{with {identity {fun {x} x}} {with {foo {fun {x} {+ x 1}}} {call {call identity foo} 123}}}") => 124)(test (run "{with {x 3} {with {f {fun {y} {+ x y}}} {with {x 5} {call f 4}}}}") => 7)(test (run "{call {with {x 3} {fun {y} {+ x y}}} 4}") => 7)(test (run "{call {call {fun {x} {call x 1}} {fun {x} {fun {y} {+ x y}}}} 123}") => 124)
修复一个被忽视的 bug,星期二,2 月 7 日
顺便说一句,这个版本修复了我们之前在 FLANG 的替换版本中遇到的一个 bug:
(run "{with {f {fun {y} {+ x y}}} {with {x 7} {call f 1}}}")
这个 bug 是由于我们天真的 subst 导致的,它没有避免捕获重命名。但请注意,由于评估器的这个版本是从外部到内部的,对于有效程序(即没有自由标识符的程序),语义上没有区别。
(提醒:这不是一个动态作用域的语言,只是一个 bug,在 x 在 f 被替换为引用 x 之前没有被替换掉时发生。)
使用 Racket 闭包的词法作用域
PLAI §11(不包括关于递归的最后部分)
一个环境的另一种表示形式。
我们已经看到了如何使用头等函数来实现包含一些信息的“对象”。我们可以使用相同的想法来表示一个环境。基本直觉是 — 一个环境是一个映射(一个函数),在标识符和某个值之间。例如,我们可以使用这个函数来表示将'a映射到1,将'b映射到2(仅使用数字简单表示)的环境:
(: my-map : Symbol -> Number)(define (my-map id) (cond [(eq? 'a id) 1] [(eq? 'b id) 2] [else (error ...)]))
以这种方式实现的空映射具有相同的类型:
(: empty-mapping : Symbol -> Number)(define (empty-mapping id) (error ...))
我们可以利用这个想法来实现我们的环境:我们只需要定义三件事 — EmptyEnv、Extend 和 lookup。如果我们设法保持这些函数的契约完整,我们就能够简单地将其插入到相同的求值器代码中,而无需进行其他更改。将ENV定义为适用于VAL类型定义的适当函数类型也更方便,而不是使用实际类型:
;; Define a type for functional environments(define-type ENV = Symbol -> VAL)
现在我们来到EmptyEnv — 这被期望是一个不需要参数并创建一个空环境的函数,一个行为类似于上面定义的empty-mapping函数。我们可以这样定义它(将empty-mapping类型更改为返回VAL):
(define (EmptyEnv) empty-mapping)
但我们可以跳过额外定义的需要,简单地返回一个空映射函数:
(: EmptyEnv : -> ENV)(define (EmptyEnv) (lambda (id) (error ...)))
(非 Rackety 名称是为了避免替换先前使用EmptyEnv名称为由类型定义创建的构造函数的代码。)
我们要处理的下一件事是lookup。先前使用的定义是:
(: lookup : Symbol ENV -> VAL)(define (lookup name env) (cases env [(EmptyEnv) (error 'lookup "no binding for ~s" name)] [(Extend id val rest-env) (if (eq? id name) val (lookup name rest-env))]))
现在应该如何修改它?很简单 — 一个环境是一个映射:一个 Racket 函数,将自己执行搜索任务。我们不需要修改契约,因为我们仍在使用ENV,只是为其提供了不同的实现。新的定义是:
(: lookup : Symbol ENV -> VAL)(define (lookup name env) (env name))
请注意,lookup几乎什么都不做 — 它只是将真正的工作委托给env参数。这是空映射应该抛出的错误消息的一个很好的提示 —
(: EmptyEnv : -> ENV)(define (EmptyEnv) (lambda (id) (error 'lookup "no binding for ~s" id)))
最后,Extend — 这是先前由 ENV 类型定义的变体案例创建的:
[Extend Symbol VAL ENV]
保持由此变体暗示的相同类型意味着新的Extend应该如下所示:
(: Extend : Symbol VAL ENV -> ENV)(define (Extend id val rest-env) ...)
问题是 — 我们如何扩展给定的环境?首先,我们知道结果应该是映射 — 一个symbol -> VAL函数,期望一个标识符来查找:
(: Extend : Symbol VAL ENV -> ENV)(define (Extend id val rest-env) (lambda (name) ...))
接下来,我们知道在生成的映射中,如果我们查找id,那么结果应该是val:
(: Extend : Symbol VAL ENV -> ENV)(define (Extend id val rest-env) (lambda (name) (if (eq? name id) val ...)))
如果我们正在寻找的name与id不同,那么我们需要通过先前的环境进行搜索,例如:(lookup name rest)。但我们知道lookup做什么 — 它只是简单地委托给映射函数(即我们的rest参数),所以我们可以采取直接路线:
(: Extend : Symbol VAL ENV -> ENV)(define (Extend id val rest-env) (lambda (name) (if (eq? name id) val (rest-env name))))
(请注意,最后一行只是(lookup name rest-env),但我们知道我们有一个功能实现。)
要了解所有这些是如何工作的,请尝试扩展一个空环境几次并检查结果。例如,我们开始的环境:
(define (my-map id) (cond [(eq? 'a id) 1] [(eq? 'b id) 2] [else (error ...)]))
行为方式相同(如果值的类型是数字)如
(Extend 'a 1 (Extend 'b 2 (EmptyEnv)))
新代码现在是相同的,除了环境代码:
#lang pl#|The grammar: <FLANG> ::= <num> | { + <FLANG> <FLANG> } | { - <FLANG> <FLANG> } | { * <FLANG> <FLANG> } | { / <FLANG> <FLANG> } | { with { <id> <FLANG> } <FLANG> } | <id> | { fun { <id> } <FLANG> } | { call <FLANG> <FLANG> }Evaluation rules: eval(N,env) = N eval({+ E1 E2},env) = eval(E1,env) + eval(E2,env) eval({- E1 E2},env) = eval(E1,env) - eval(E2,env) eval({* E1 E2},env) = eval(E1,env) * eval(E2,env) eval({/ E1 E2},env) = eval(E1,env) / eval(E2,env) eval(x,env) = lookup(x,env) eval({with {x E1} E2},env) = eval(E2,extend(x,eval(E1,env),env)) eval({fun {x} E},env) = <{fun {x} E}, env> eval({call E1 E2},env1) = eval(Ef,extend(x,eval(E2,env1),env2)) if eval(E1,env1) = <{fun {x} Ef}, env2> = error! otherwise|#(define-type FLANG [Num Number] [Add FLANG FLANG] [Sub FLANG FLANG] [Mul FLANG FLANG] [Div FLANG FLANG] [Id Symbol] [With Symbol FLANG FLANG] [Fun Symbol FLANG] [Call FLANG FLANG])(: parse-sexpr : Sexpr -> FLANG);; parses s-expressions into FLANGs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: name)) body) (Fun name (parse-sexpr body))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [(list 'call fun arg) (Call (parse-sexpr fun) (parse-sexpr arg))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> FLANG);; parses a string containing a FLANG expression to a FLANG AST(define (parse str) (parse-sexpr (string->sexpr str)));; Types for environments, values, and a lookup function(define-type VAL [NumV Number] [FunV Symbol FLANG ENV]);; Define a type for functional environments(define-type ENV = Symbol -> VAL)(: EmptyEnv : -> ENV)(define (EmptyEnv) (lambda (id) (error 'lookup "no binding for ~s" id)))(: Extend : Symbol VAL ENV -> ENV);; extend a given environment cache with a new binding(define (Extend id val rest-env) (lambda (name) (if (eq? name id) val (rest-env name))))(: lookup : Symbol ENV -> VAL);; lookup a symbol in an environment, return its value or throw an;; error if it isn't bound(define (lookup name env) (env name))(: NumV->number : VAL -> Number);; convert a FLANG runtime numeric value to a Racket one(define (NumV->number val) (cases val [(NumV n) n] [else (error 'arith-op "expected a number, got: ~s" val)]))(: arith-op : (Number Number -> Number) VAL VAL -> VAL);; gets a Racket numeric binary operator, and uses it within a NumV;; wrapper(define (arith-op op val1 val2) (NumV (op (NumV->number val1) (NumV->number val2))))(: eval : FLANG ENV -> VAL);; evaluates FLANG expressions by reducing them to values(define (eval expr env) (cases expr [(Num n) (NumV n)] [(Add l r) (arith-op + (eval l env) (eval r env))] [(Sub l r) (arith-op - (eval l env) (eval r env))] [(Mul l r) (arith-op * (eval l env) (eval r env))] [(Div l r) (arith-op / (eval l env) (eval r env))] [(With bound-id named-expr bound-body) (eval bound-body (Extend bound-id (eval named-expr env) env))] [(Id name) (lookup name env)] [(Fun bound-id bound-body) (FunV bound-id bound-body env)] [(Call fun-expr arg-expr) (let ([fval (eval fun-expr env)]) (cases fval [(FunV bound-id bound-body f-env) (eval bound-body (Extend bound-id (eval arg-expr env) f-env))] [else (error 'eval "`call' expects a function, got: ~s" fval)]))]))(: run : String -> Number);; evaluate a FLANG program contained in a string(define (run str) (let ([result (eval (parse str) (EmptyEnv))]) (cases result [(NumV n) n] [else (error 'run "evaluation returned a non-number: ~s" result)])));; tests(test (run "{call {fun {x} {+ x 1}} 4}") => 5)(test (run "{with {add3 {fun {x} {+ x 3}}} {call add3 1}}") => 4)(test (run "{with {add3 {fun {x} {+ x 3}}} {with {add1 {fun {x} {+ x 1}}} {with {x 3} {call add1 {call add3 x}}}}}") => 7)(test (run "{with {identity {fun {x} x}} {with {foo {fun {x} {+ x 1}}} {call {call identity foo} 123}}}") => 124)(test (run "{with {x 3} {with {f {fun {y} {+ x y}}} {with {x 5} {call f 4}}}}") => 7)(test (run "{call {with {x 3} {fun {y} {+ x y}}} 4}") => 7)(test (run "{call {call {fun {x} {call x 1}} {fun {x} {fun {y} {+ x y}}}} 123}") => 124)
更多闭包(在两个级别上)Tuesday, February 7th
Racket 闭包(=函数)也可以在其他地方使用,正如我们所见,它们不仅可以封装各种值,还可以保存这些值所期望的行为。
为了演示这一点,我们将处理我们语言中的闭包。我们目前使用的一种变体保存了三个相关信息:
[FunV Symbol FLANG ENV]
我们可以将其替换为一个功能对象,它将保存这三个值。首先,将VAL类型更改为保存FunV值的函数:
(define-type VAL [NumV Number] [FunV (? -> ?)])
注意函数应该以某种方式封装先前存在的相同信息,问题是如何完成这些信息,这将决定实际类型。这些信息在我们的求值器中扮演两个角色 — 在Fun情况下生成一个闭包,在Call情况下使用它:
[(Fun bound-id bound-body) (FunV bound-id bound-body env)][(Call fun-expr arg-expr) (let ([fval (eval fun-expr env)]) (cases fval [(FunV bound-id bound-body f-env) (eval bound-body ;*** (Extend bound-id ;*** (eval arg-expr env) ;*** f-env))] ;*** [else (error 'eval "`call' expects a function, got: ~s" fval)]))]
我们可以简单地将Call中标记的功能位折叠到一个 Racket 函数中,该函数将存储在FunV对象中 — 这个功能部分接受一个参数值,将闭包的环境扩展为其值和函数的名称,并继续评估函数体。将所有这些折叠到一个函数中给我们带来:
(lambda (arg-val) (eval bound-body (Extend bound-id arg-val env)))
其中bound-body、bound-id和val的值在构造FunV时已知。这样做给我们带来了两种情况的以下代码:
[(Fun bound-id bound-body) (FunV (lambda (arg-val) (eval bound-body (Extend bound-id arg-val env))))][(Call fun-expr arg-expr) (let ([fval (eval fun-expr env)]) (cases fval [(FunV proc) (proc (eval arg-expr env))] [else (error 'eval "`call' expects a function, got: ~s" fval)]))]
现在函数的类型是清晰的:
(define-type VAL [NumV Number] [FunV (VAL -> VAL)])
再次,代码的其余部分未经修改:
#lang pl(define-type FLANG [Num Number] [Add FLANG FLANG] [Sub FLANG FLANG] [Mul FLANG FLANG] [Div FLANG FLANG] [Id Symbol] [With Symbol FLANG FLANG] [Fun Symbol FLANG] [Call FLANG FLANG])(: parse-sexpr : Sexpr -> FLANG);; parses s-expressions into FLANGs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: name)) body) (Fun name (parse-sexpr body))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [(list 'call fun arg) (Call (parse-sexpr fun) (parse-sexpr arg))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> FLANG);; parses a string containing a FLANG expression to a FLANG AST(define (parse str) (parse-sexpr (string->sexpr str)));; Types for environments, values, and a lookup function(define-type VAL [NumV Number] [FunV (VAL -> VAL)]);; Define a type for functional environments(define-type ENV = Symbol -> VAL)(: EmptyEnv : -> ENV)(define (EmptyEnv) (lambda (id) (error 'lookup "no binding for ~s" id)))(: Extend : Symbol VAL ENV -> ENV);; extend a given environment cache with a new binding(define (Extend id val rest-env) (lambda (name) (if (eq? name id) val (rest-env name))))(: lookup : Symbol ENV -> VAL);; lookup a symbol in an environment, return its value or throw an;; error if it isn't bound(define (lookup name env) (env name))(: NumV->number : VAL -> Number);; convert a FLANG runtime numeric value to a Racket one(define (NumV->number val) (cases val [(NumV n) n] [else (error 'arith-op "expected a number, got: ~s" val)]))(: arith-op : (Number Number -> Number) VAL VAL -> VAL);; gets a Racket numeric binary operator, and uses it within a NumV;; wrapper(define (arith-op op val1 val2) (NumV (op (NumV->number val1) (NumV->number val2))))(: eval : FLANG ENV -> VAL);; evaluates FLANG expressions by reducing them to values(define (eval expr env) (cases expr [(Num n) (NumV n)] [(Add l r) (arith-op + (eval l env) (eval r env))] [(Sub l r) (arith-op - (eval l env) (eval r env))] [(Mul l r) (arith-op * (eval l env) (eval r env))] [(Div l r) (arith-op / (eval l env) (eval r env))] [(With bound-id named-expr bound-body) (eval bound-body (Extend bound-id (eval named-expr env) env))] [(Id name) (lookup name env)] [(Fun bound-id bound-body) (FunV (lambda (arg-val) (eval bound-body (Extend bound-id arg-val env))))] [(Call fun-expr arg-expr) (let ([fval (eval fun-expr env)]) (cases fval [(FunV proc) (proc (eval arg-expr env))] [else (error 'eval "`call' expects a function, got: ~s" fval)]))]))(: run : String -> Number);; evaluate a FLANG program contained in a string(define (run str) (let ([result (eval (parse str) (EmptyEnv))]) (cases result [(NumV n) n] [else (error 'run "evaluation returned a non-number: ~s" result)])));; tests(test (run "{call {fun {x} {+ x 1}} 4}") => 5)(test (run "{with {add3 {fun {x} {+ x 3}}} {call add3 1}}") => 4)(test (run "{with {add3 {fun {x} {+ x 3}}} {with {add1 {fun {x} {+ x 1}}} {with {x 3} {call add1 {call add3 x}}}}}") => 7)(test (run "{with {identity {fun {x} x}} {with {foo {fun {x} {+ x 1}}} {call {call identity foo} 123}}}") => 124)(test (run "{with {x 3} {with {f {fun {y} {+ x y}}} {with {x 5} {call f 4}}}}") => 7)(test (run "{call {with {x 3} {fun {y} {+ x y}}} 4}") => 7)(test (run "{call {call {fun {x} {call x 1}} {fun {x} {fun {y} {+ x y}}}} 123}") => 124)
评估器的类型星期二,2 月 14 日
刚才我们所做的是在我们使用词法环境和闭包实现的语言中实现词法环境和闭包(Racket)!
这是将主机语言的特性嵌入到实现语言中的另一个例子,这是我们已经讨论过的问题。
这种情况有很多例子,即使涉及的两种语言是不同的。例如,如果我们在 Racket 的 C 实现中有这样一点:
// Disclaimer: not real Racket codeRacket_Object *eval_and( int argc, Racket_Object *argv[] ){ Racket_Object *tmp; if ( argc != 2 ) signal_racket_error("bad number of arguments"); else if ( racket_eval(argv[0]) != racket_false && (tmp = racket_eval(argv[1])) != racket_false ) return tmp; else return racket_false;}
然后,评估 Racket and形式的特殊语义是从 C 对&&的特殊处理中继承的。你可以通过 C 编译器中存在 bug 时,它也会传播到结果 Racket 实现中来看到这一点。另一种解决方案是根本不使用&&:
// Disclaimer: not real Racket codeRacket_Object *eval_and( int argc, Racket_Object *argv[] ){ Racket_Object *tmp; if ( argc != 2 ) signal_racket_error("bad number of arguments"); else if ( racket_eval(argv[0]) != racket_false ) return racket_eval(argv[1]); else return racket_false;}
我们可以说这甚至更好,因为它在尾位置评估第二个表达式。但在这种情况下,我们并没有真正获得这个好处,因为 C 本身并没有将尾调用优化作为标准特性(尽管一些编译器在某些情况下会这样做)。
我们已经看到了几种不同风格的评估器实现。它们提出了以下分类法。
-
一个语法评估器是使用自己的语言仅表示被评估语言的表达式,并明确实现所有相应行为的评估器。
-
一个元评估器是使用自己语言的语言特性直接实现被评估语言行为的评估器。
虽然我们基于替换的 FLANG 评估器接近于语法评估器,但到目前为止我们还没有编写任何纯粹的语法评估器:我们仍然依赖于像 Racket 算术等东西。我们所学习的最新评估器,明显是一个元评估器。
通过被评估语言和实现语言之间的良好匹配,编写元评估器可能会非常容易。然而,如果匹配不好,那么可能会非常困难。对于一个语法评估器,实现每个语义特性都会有一定难度,但作为回报,你不必太担心实现和被评估语言之间的匹配程度。特别是,如果实现和被评估语言之间存在特别强烈的不匹配,编写一个语法评估器可能比编写一个元评估器需要更少的工作。作为练习,我们可以在我们最新的评估器基础上去除评估器响应在 VAL 类型中的封装。下面显示了结果评估器。这是一个真正的元评估器:它使用 Racket 闭包来实现 FLANG 闭包,Racket 函数应用于 FLANG 函数应用,Racket 数字用于 FLANG 数字,Racket 算术用于 FLANG 算术。实际上,忽略 Racket 和 FLANG 之间的一些小的语法差异,这个最新的评估器可以被分类为比元评估器更具体的东西:
- 元循环评估器是一种元评估器,其中实现和被评估的语言是相同的。
(换句话说,评估器的平凡性提示我们两种语言之间存在深层联系,无论它们的语法差异如何。)
特征嵌入星期二,2 月 14 日
我们看到了惰性求值和急切求值之间的区别在于with形式、函数应用等的求值规则:
eval({with {x E1} E2}) = eval(E2[eval(E1)/x])
急于,而且
eval({with {x E1} E2}) = eval(E2[E1/x])
是惰性的。但是第一个规则真的是急切的吗?事实是,唯一使其急切的是我们对数学符号的理解是急切的事实 — 如果我们将数学视为惰性,那么规则的描述就变成了对惰性求值的描述。
另一种看待这个问题的方式是——考虑一下实现这个评估的代码片段:
(: eval : FLANG -> Number);; evaluates FLANG expressions by reducing them to numbers(define (eval expr) (cases expr ... [(With bound-id named-expr bound-body) (eval (subst bound-body bound-id (Num (eval named-expr))))] ...))
同样的问题也适用:这真的实现了急切求值吗?我们知道这确实是急切的——我们只需简单尝试一下并检查它是否是,但这只是急切的因为我们在实现中使用了急切的语言!如果我们自己的语言是惰性的,那么求值器的实现也将以惰性方式运行,这意味着上述eval和subst函数的应用也将是惰性的,使得我们的求值器也是惰性的。
这是一种普遍现象,其中我们使用的语言的一些语义特征(在正式描述中是数学,在我们的代码中是 Racket)被嵌入到我们实现的语言中。
这里是另一个例子——考虑一下实现算术的代码:
(: eval : FLANG -> Number);; evaluates FLANG expressions by reducing them to numbers(define (eval expr) (cases expr [(Num n) n] [(Add l r) (+ (eval l) (eval r))] ...))
如果它是这样写的:
FLANG eval(FLANG expr) { if (is_Num(expr)) return num_of_Num(expr); else if (is_Add(expr)) return eval(lhs_of_Add(expr)) + eval(rhs_of_Add(expr)); else if ... ...}
它仍然会实现无限整数和精确分数吗?这取决于用来实现它的语言:上述语法建议使用 C、C++、Java 或其他相关语言,通常带有有限的整数和没有精确分数。但这实际上取决于语言——即使是我们自己的代码也仅仅因为 Racket 有它们才具有无限整数和精确分数。如果我们使用的语言没有这些功能(有这样的 Scheme 实现),那么我们实现的语言也将吸收这些(缺乏)功能,并且它自己的数字将以同样的方式受限制。 (这包括数字的语法,我们有意嵌入了它们,就像标识符的语法一样)。
底线是,我们应该意识到这样的问题,并在谈论语义时要非常小心。即使是我们用来交流的语言(半正式逻辑)也可能意味着不同的事情。
附注:阅读肯·汤普森的《对“信任信任”的反思》(您可以跳到“第二阶段”部分以找到有趣的内容)。
(完成后,寻找“XcodeGhost”以查看相关示例,并不要错过维基百科页面上的泄露文档...)
这里是我们评估器的另一种变体,甚至更接近于元循环评估器。它直接使用 Racket 值来实现值,因此算术运算变得简单明了。特别要注意函数应用情况与算术的类似性:FLANG 函数应用转换为 Racket 函数应用。在这两种情况下(应用和算术运算),我们甚至不检查对象,因为它们是简单的 Racket 对象——如果我们的语言碰巧对带有函数的算术或应用数字具有某种意义,那么我们将继承我们的语言中相同的语义。这意味着我们现在规定的行为更少,更多地依赖于 Racket 的行为。
我们使用以下类型定义的 Racket 值:
(define-type VAL = (U Number (VAL -> VAL)))
而且评估函数现在可以是:
(: eval : FLANG ENV -> VAL);; evaluates FLANG expressions by reducing them to values(define (eval expr env) (cases expr [(Num n) n] ;*** return the actual number [(Add l r) (+ (eval l env) (eval r env))] [(Sub l r) (- (eval l env) (eval r env))] [(Mul l r) (* (eval l env) (eval r env))] [(Div l r) (/ (eval l env) (eval r env))] [(With bound-id named-expr bound-body) (eval bound-body (Extend bound-id (eval named-expr env) env))] [(Id name) (lookup name env)] [(Fun bound-id bound-body) (lambda ([arg-val : VAL]) ;*** return the racket function ;; note that this requires input type specifications since ;; typed racket can't guess the right one (eval bound-body (Extend bound-id arg-val env)))] [(Call fun-expr arg-expr) ((eval fun-expr env) ;*** trivial like the arithmetics! (eval arg-expr env))]))
注意算术运算的实现是简单的——它直接将 FLANG 语法转换为 Racket 操作,并且由于我们不检查 Racket 操作的输入,因此让 Racket 为我们抛出类型错误。还要注意函数应用与算术操作的相似性:FLANG 应用直接转换为 Racket 应用。
然而,在 Typed Racket 中,情况并不那么简单。类型检查的整个重点在于我们永远不会遇到类型错误——因此我们不能依赖于 Racket 的错误,因为可能产生错误的代码是被禁止的!绕过这一点的方法是执行明确的检查,以确保 Racket 不会遇到类型错误。我们在 eval 中定义了以下两个辅助函数来实现这一点:
(: evalN : FLANG -> Number) (define (evalN e) (let ([n (eval e env)]) (if (number? n) n (error 'eval "got a non-number: ~s" n)))) (: evalF : FLANG -> (VAL -> VAL)) (define (evalF e) (let ([f (eval e env)]) (if (function? f) f (error 'eval "got a non-function: ~s" f))))
注意 Typed Racket 是“足够智能”的,它能够推断出在 evalF 中递归评估的结果必须是 Number 或 (VAL -> VAL);而且由于 if 会排除数字,所以我们只剩下 (VAL -> VAL) 函数,而不是任意函数。
#lang pl(define-type FLANG [Num Number] [Add FLANG FLANG] [Sub FLANG FLANG] [Mul FLANG FLANG] [Div FLANG FLANG] [Id Symbol] [With Symbol FLANG FLANG] [Fun Symbol FLANG] [Call FLANG FLANG])(: parse-sexpr : Sexpr -> FLANG);; parses s-expressions into FLANGs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: name)) body) (Fun name (parse-sexpr body))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [(list 'call fun arg) (Call (parse-sexpr fun) (parse-sexpr arg))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> FLANG);; parses a string containing a FLANG expression to a FLANG AST(define (parse str) (parse-sexpr (string->sexpr str)));; Types for environments, values, and a lookup function;; Values are plain Racket values, no new VAL wrapper;;; (but note that this is a recursive definition)(define-type VAL = (U Number (VAL -> VAL)));; Define a type for functional environments(define-type ENV = (Symbol -> VAL))(: EmptyEnv : -> ENV)(define (EmptyEnv) (lambda (id) (error 'lookup "no binding for ~s" id)))(: Extend : Symbol VAL ENV -> ENV);; extend a given environment cache with a new binding(define (Extend id val rest-env) (lambda (name) (if (eq? name id) val (rest-env name))))(: lookup : Symbol ENV -> VAL);; lookup a symbol in an environment, return its value or throw an;; error if it isn't bound(define (lookup name env) (env name))(: eval : FLANG ENV -> VAL);; evaluates FLANG expressions by reducing them to values(define (eval expr env) (: evalN : FLANG -> Number) (define (evalN e) (let ([n (eval e env)]) (if (number? n) n (error 'eval "got a non-number: ~s" n)))) (: evalF : FLANG -> (VAL -> VAL)) (define (evalF e) (let ([f (eval e env)]) (if (function? f) f (error 'eval "got a non-function: ~s" f)))) (cases expr [(Num n) n] [(Add l r) (+ (evalN l) (evalN r))] [(Sub l r) (- (evalN l) (evalN r))] [(Mul l r) (* (evalN l) (evalN r))] [(Div l r) (/ (evalN l) (evalN r))] [(With bound-id named-expr bound-body) (eval bound-body (Extend bound-id (eval named-expr env) env))] [(Id name) (lookup name env)] [(Fun bound-id bound-body) (lambda ([arg-val : VAL]) (eval bound-body (Extend bound-id arg-val env)))] [(Call fun-expr arg-expr) ((evalF fun-expr) (eval arg-expr env))]))(: run : String -> VAL) ; no need to convert VALs to numbers;; evaluate a FLANG program contained in a string(define (run str) (eval (parse str) (EmptyEnv)));; tests(test (run "{call {fun {x} {+ x 1}} 4}") => 5)(test (run "{with {add3 {fun {x} {+ x 3}}} {call add3 1}}") => 4)(test (run "{with {add3 {fun {x} {+ x 3}}} {with {add1 {fun {x} {+ x 1}}} {with {x 3} {call add1 {call add3 x}}}}}") => 7)(test (run "{with {identity {fun {x} x}} {with {foo {fun {x} {+ x 1}}} {call {call identity foo} 123}}}") => 124)(test (run "{with {x 3} {with {f {fun {y} {+ x y}}} {with {x 5} {call f 4}}}}") => 7)(test (run "{call {with {x 3} {fun {y} {+ x y}}} 4}") => 7)(test (run "{call {call {fun {x} {call x 1}} {fun {x} {fun {y} {+ x y}}}} 123}") => 124)
递归,递归,递归星期二,二月十四日
PLAI §9
我们的语言中仍然缺少一个重要特性:我们没有办法执行递归(因此没有任何类型的循环)。到目前为止,我们只能在有名称时使用递归。在 FLANG 中,我们唯一能够拥有名称的方式是通过 with,这对于递归来说是不够的。
要讨论递归问题,我们切换到一个“破坏”的(无类型的)Racket 版本 —— 其中 define 有不同的作用域规则:定义的名称的作用域不覆盖定义的表达式。具体来说,在这种语言中,这是行不通的:
#lang pl broken(define (fact n) (if (zero? n) 1 (* n (fact (- n 1)))))(fact 5)
在我们的语言中,这种转换也不起作用(假设我们有 if 等):
{with {fact {fun {n} {if {= n 0} 1 {* n {call fact {- n 1}}}}}} {call fact 5}}
同样,在普通的 Racket 中,如果 let 是你用来创建绑定的唯一工具,这也不起作用:
(let ([fact (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))]) (fact 5))
在破坏作用域的语言中,define 形式更类似于数学定义。例如,当我们写:
(define (F x) x)(define (G y) (F y))(G F)
它实际上是一个简写,表示为
(define F (lambda (x) x))(define G (lambda (y) (F y)))(G F)
然后我们可以用它们的定义替换定义的名称:
(define F (lambda (x) x))(define G (lambda (y) (F y)))((lambda (y) (F y)) (lambda (x) x))
而且这可以一直进行下去,直到我们到达实际编写的代码为止:
((lambda (y) ((lambda (x) x) y)) (lambda (x) x))
这意味着上面的 fact 定义类似于写:
fact := (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))(fact 5)
这不是一个完整的定义 —— 它是无意义的(这是对 “无意义” 一词的正式使用)。我们真正想要的是,采用 等式(使用 = 而不是 :=)
fact = (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))
并找到一个解决方案,该解决方案将是一个对于使这个等式成立的 fact 的值。
如果你查看网页上的 Racket 评估规则手册,你会发现这个问题与我们介绍 Racket define 的方式有关:有一个模糊的解释谈论了解事物。
最大的问题是:我们能够定义没有 Racket 魔术 define 形式的递归函数吗?
注意:这个问题与在我们的语言中实现递归的问题有点不同 —— 在 Racket 的情况下,我们无法控制语言的实现。随着时间的推移,当我们以特定方式使用突变时,在我们自己的语言中实现递归将会非常容易。所以我们现在面临的问题可以表述为“我们能在 Racket 中不使用 Racket 的魔术定义形式获得递归吗?”或者“我们能在我们的解释器中不使用突变获得递归吗?”。
无魔法递归 Tuesday, February 14th
PLAI §22.4(我们深入探讨了这个问题)
注意:这个解释类似于理查德·加布里埃尔所写的《Y 的奥秘》中的解释。
要实现无需define魔法的递归,我们首先观察到:在动态作用域语言中不会出现这个问题。考虑问题的let版本:
#lang pl dynamic(let ([fact (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))]) (fact 5))
这样做没有问题 —— 因为在我们评估函数体时,fact在当前动态作用域中已经绑定到了自身。(这是动态作用域被认为是一种便捷方法的另一个原因。)
尽管如此,我们在词法作用域中遇到的问题仍然存在,但动态作用域的工作方式提示了我们现在可以使用的解决方案。就像在动态作用域情况下一样,当调用fact时,它确实有一个值 —— 唯一的问题是该值在其体的词法作用域中是不可访问的。
不要试图通过词法作用域来传递值,我们可以模仿动态作用域语言中发生的情况,通过将fact的值传递给自身,这样它就可以调用自己(回到破损作用域语言中的原始代码):
(define (fact self n) ;*** (if (zero? n) 1 (* n (self (- n 1)))))(fact fact 5) ;***
除了现在递归调用仍然应该发送自身以外:
(define (fact self n) (if (zero? n) 1 (* n (self self (- n 1))))) ;***(fact fact 5)
问题在于,这需要重写对fact的调用 —— 包括外部和内部递归调用。为了使这成为一个可接受的解决方案,来自两个地方的调用都不应该改变。最终,我们应该能够得到一个工作正常的fact定义,只使用了
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))
解决这个问题的第一步是对fact的定义进行柯里化。
(define (fact self) ;*** (lambda (n) ;*** (if (zero? n) 1 (* n ((self self) (- n 1)))))) ;***((fact fact) 5) ;***
现在fact不再是我们的阶乘函数 —— 它是一个构造它的函数。所以将其称为make-fact,并将fact绑定到实际的阶乘函数。
(define (make-fact self) ;*** (lambda (n) (if (zero? n) 1 (* n ((self self) (- n 1))))))(define fact (make-fact make-fact)) ;***(fact 5) ;***
我们可以尝试在阶乘函数的体中做同样的事情:不是调用(self self),而是将fact绑定到它自己:
(define (make-fact self) (lambda (n) (let ([fact (self self)]) ;*** (if (zero? n) 1 (* n (fact (- n 1))))))) ;***(define fact (make-fact make-fact))(fact 5)
这样做没有问题,但是如果考虑我们的原始目标,我们需要将本地的fact绑定移到(lambda (n) ...)之外 —— 因此我们得到一个使用阶乘表达式的定义。所以,交换这两行:
(define (make-fact self) (let ([fact (self self)]) ;*** (lambda (n) ;*** (if (zero? n) 1 (* n (fact (- n 1)))))))(define fact (make-fact make-fact))(fact 5)
但问题在于这会让我们陷入无限循环,因为我们试图太早地评估(self self)。事实上,如果我们忽略let的主体和其他细节,我们基本上是这样做的:
(define (make-fact self) (self self)) (make-fact make-fact)--reduce-sugar-->(define make-fact (lambda (self) (self self))) (make-fact make-fact)--replace-definition-->((lambda (self) (self self)) (lambda (self) (self self)))--rename-identifiers-->((lambda (x) (x x)) (lambda (x) (x x)))
而这个表达式有一个有趣的属性:它会归约为自身,因此评估它会陷入无限循环。
那么我们该如何解决这个问题呢?嗯,我们知道(self self) 应该是阶乘函数本身的相同值 —— 所以它必须是一个单参数函数。如果它是这样一个函数,我们可以使用一个等效的值,除非在函数被调用时它不会被评估,否则它不会被评估。这里的诀窍在于观察到(lambda (n) (add1 n))实际上与add1相同的函数,只是add1部分直到函数被调用时才被评估。将这个技巧应用到我们的代码中产生一个不会陷入相同无限循环的版本:
(define (make-fact self) (let ([fact (lambda (n) ((self self) n))]) ;*** (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define fact (make-fact make-fact))(fact 5)
从这里继续 —— 我们知道
(let ([x v]) e) is the same as ((lambda (x) e) v)
(记住我们是如何从with导出fun的),所以我们可以将那个let转换为等价的函数应用形式:
(define (make-fact self) ((lambda (fact) ;*** (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))) (lambda (n) ((self self) n)))) ;***(define fact (make-fact make-fact))(fact 5)
现在注意,(lambda (fact) …)表达式是我们需要的递归定义fact的一切 —— 它具有合适的阶乘体和一个普通的递归调用。它几乎就像我们想要将fact定义为的通常值,只是我们仍然需要对递归值本身进行抽象。所以让我们将这段代码移到一个单独的fact-core定义中:
(define fact-core ;*** (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define (make-fact self) (fact-core ;*** (lambda (n) ((self self) n))))(define fact (make-fact make-fact))(fact 5)
现在我们可以继续通过将(make-fact make-fact)自应用移到它自己的函数中,这就是创建真正阶乘的函数:
(define fact-core (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define (make-fact self) (fact-core (lambda (n) ((self self) n))))(define (make-real-fact) (make-fact make-fact)) ;***(define fact (make-real-fact)) ;***(fact 5)
使用显式的lambda重写make-fact定义:
(define fact-core (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define make-fact ;*** (lambda (self) ;*** (fact-core (lambda (n) ((self self) n)))))(define (make-real-fact) (make-fact make-fact))(define fact (make-real-fact))(fact 5)
并通过直接使用make-fact的值而不是通过定义来将make-fact和make-real-fact的功能合并到一个make-fact函数中:
(define fact-core (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define (make-real-fact) (let ([make (lambda (self) ;*** (fact-core ;*** (lambda (n) ((self self) n))))]) ;*** (make make)))(define fact (make-real-fact))(fact 5)
现在我们可以观察到make-real-fact没有任何特定于阶乘的东西 —— 我们可以将其作为“核心函数”作为参数:
(define fact-core (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define (make-real-fact core) ;*** (let ([make (lambda (self) (core ;*** (lambda (n) ((self self) n))))]) (make make)))(define fact (make-real-fact fact-core)) ;***(fact 5)
并将其称为make-recursive:
(define fact-core (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define (make-recursive core) ;*** (let ([make (lambda (self) (core (lambda (n) ((self self) n))))]) (make make)))(define fact (make-recursive fact-core)) ;***(fact 5)
现在我们几乎完成了 —— 没有一个单独的fact-core定义的真正需要,只需使用该值来定义fact:
(define (make-recursive core) (let ([make (lambda (self) (core (lambda (n) ((self self) n))))]) (make make)))(define fact (make-recursive (lambda (fact) ;*** (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))) ;***(fact 5)
将let转换为函数形式:
(define (make-recursive core) ((lambda (make) (make make)) ;*** (lambda (self) ;*** (core (lambda (n) ((self self) n)))))) ;***(define fact (make-recursive (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))))(fact 5)
进行一些重命名使事情变得更简单 —— make和self变成了x,core变成了f:
(define (make-recursive f) ;*** ((lambda (x) (x x)) ;*** (lambda (x) (f (lambda (n) ((x x) n)))))) ;***(define fact (make-recursive (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))))(fact 5)
或者我们可以手动展开第一个(lambda (x) (x x))应用,使对称性更明显(并不令人惊讶,因为它始于一个目的是执行自应用的let):
(define (make-recursive f) ((lambda (x) (f (lambda (n) ((x x) n)))) ;*** (lambda (x) (f (lambda (n) ((x x) n)))))) ;***(define fact (make-recursive (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))))(fact 5)
最后我们终于得到了我们正在寻找的东西:一种通用的方法来定义任何递归函数,而没有任何神奇的define技巧。这对于其他递归函数也适用:
#lang pl broken(define (make-recursive f) ((lambda (x) (f (lambda (n) ((x x) n)))) (lambda (x) (f (lambda (n) ((x x) n))))))(define fact (make-recursive (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))))(fact 5)(define fib (make-recursive (lambda (fib) (lambda (n) (if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2))))))))(fib 8)(define length (make-recursive (lambda (length) (lambda (l) (if (null? l) 0 (+ (length (rest l)) 1))))))(length '(x y z))
人们经常在纸上使用的一个方便的工具是进行一种句法抽象: “假设当我写(twice foo)时,我真的想写(foo foo)”。这通常可以作为普通的抽象完成(也就是使用函数),但在某些情况下 —— 例如,如果我们想要抽象定义 —— 我们只是想要这样一个重写规则。(在课程结束时更多讨论此问题。)破碎范围语言确实提供了这样一个工具 —— rewrite用一个重写规则扩展了语言。使用这个,以及我们的make-recursive,我们可以制定一个递归定义形式:
(rewrite (define/rec (f x) E) => (define f (make-recursive (lambda (f) (lambda (x) E)))))
换句话说,我们创建了自己的“魔法定义”形式。现在,上面的代码几乎可以以与在普通 Racket 中编写的方式相同的方式编写:
#lang pl broken(define (make-recursive f) ((lambda (x) (f (lambda (n) ((x x) n)))) (lambda (x) (f (lambda (n) ((x x) n))))))(rewrite (define/rec (f x) E) => (define f (make-recursive (lambda (f) (lambda (x) E)))));; examples(define/rec (fact n) (if (zero? n) 1 (* n (fact (- n 1)))))(fact 5)(define/rec (fib n) (if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2)))))(fib 8)(define/rec (length l) (if (null? l) 0 (+ (length (rest l)) 1)))(length '(x y z))
最后,请注意,由于对急切求值的保护,make-recursive 仅限于 1 个参数函数。无论如何,它可以按照您想要的任何方式使用,例如,
(make-recursive (lambda (f) (lambda (x) f)))
是一个函数,返回自身而不是调用自身。使用重写规则,这将是:
(define/rec (f x) f)
这与以下相同:
(define (f x) f)
在普通的 Racket 中。
make-recursive的核心星期二,二月 14 日
就像在 Racket 中一样,能够表达递归函数是语言的一个基本属性。这意味着我们的语言中可以有循环,这就是使语言强大到足以成为 TM 等价的本质——能够表达不可判定问题,我们不知道是否有答案。
这一切能够实现的核心是我们在推导中看到的表达式:
((lambda (x) (x x)) (lambda (x) (x x)))
它简化为自身,因此没有价值:试图评估它会陷入无限循环。(这个表达式通常被称为“Omega”。)
这是创建循环的关键——我们用它来实现递归。忽略我们最终的make-recursive定义,暂时不考虑我们需要防止过早陷入无限循环的“保护”:
(define (make-recursive f) ((lambda (x) (x x)) (lambda (x) (f (x x)))))
我们可以看到,这几乎与 Omega 表达式相同——唯一的区别在于f的应用。实际上,这个表达式(某些F的(make-recursive F)的结果)以类似的方式减少到 Omega:
((lambda (x) (x x)) (lambda (x) (F (x x))))((lambda (x) (F (x x))) (lambda (x) (F (x x))))(F ((lambda (x) (F (x x))) (lambda (x) (F (x x)))))(F (F ((lambda (x) (F (x x))) (lambda (x) (F (x x))))))(F (F (F ((lambda (x) (F (x x))) (lambda (x) (F (x x)))))))...
这意味着这个表达式的实际值是:
(F (F (F ...forever...)))
如果我们有一个惰性语言,这个定义就足够了,但是为了在严格的语言中使事情正常工作,我们需要重新引入保护。这使得事情有些不同——如果我们使用(protect f)作为保护技巧的简写,
(rewrite (protect f) => (lambda (x) (f x)))
然后我们有:
(define (make-recursive f) ((lambda (x) (x x)) (lambda (x) (f (protect (x x))))))
这使得(make-recursive F)的评估简化为
(F (protect (F (protect (F (protect (...forever...)))))))
而这仍然是相同的结果(只要F是一个单参数函数)。
(请注意,protect 不能被实现为普通函数!)
递归的 Denotational 解释 Tuesday, February 14th
注:此解释类似于您可以在《The Little Schemer》中找到的解释,作者是 Dan Friedman 和 Matthias Felleisen,名为“(Y Y) Works!”。
现在,我们对如何导出make-recursive定义的解释已经很好了 —— 毕竟,我们确实设法让它工作了。但这种解释是从一种操作性的角度出发的:我们知道一种可以使事情运作的技巧,然后我们将事情推来推去,直到我们按我们想要的方式让它工作。与其这样做,我们可以从更声明性的角度重新解决问题。
因此,从我们之前使用的相同破碎的代码重新开始(使用破碎的作用域语言):
(define fact (lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))
这与我们开始时一样混乱:函数主体中的fact出现是自由的,这意味着这段代码毫无意义。为了避免运行此代码时出现的编译错误,我们可以将任何东西替换为fact —— 最好使用一个会导致运行时错误的替代项:
(define fact (lambda (n) (if (zero? n) 1 (* n (777 (- n 1)))))) ;***
这个函数不会像原始函数那样工作 —— 但有一种情况它 会 工作:当输入值为0时(因为然后我们不会达到虚假的应用)。我们通过称此函数为fact0来注意到这一点:
(define fact0 ;*** (lambda (n) (if (zero? n) 1 (* n (777 (- n 1))))))
现在我们已经定义了这个函数,我们可以使用它来编写fact1,这是参数为0或1的阶乘函数:
(define fact0 (lambda (n) (if (zero? n) 1 (* n (777 (- n 1))))))(define fact1 (lambda (n) (if (zero? n) 1 (* n (fact0 (- n 1))))))
而且记住,这实际上只是以下的简写形式:
(define fact1 (lambda (n) (if (zero? n) 1 (* n ((lambda (n) (if (zero? n) 1 (* n (777 (- n 1))))) (- n 1))))))
我们可以继续这样做,并编写适用于 n<=2 的fact2:
(define fact2 (lambda (n) (if (zero? n) 1 (* n (fact1 (- n 1))))))
或者,以完整形式表达:
(define fact2 (lambda (n) (if (zero? n) 1 (* n ((lambda (n) (if (zero? n) 1 (* n ((lambda (n) (if (zero? n) 1 (* n (777 (- n 1))))) (- n 1))))) (- n 1))))))
如果我们继续这样做,我们 将 得到真正的阶乘函数,但问题是为了处理 任何 可能的整数参数,它将必须是一个无限的定义!这是它应该看起来的样子:
(define fact0 (lambda (n) (if (zero? n) 1 (* n (777 (- n 1))))))(define fact1 (lambda (n) (if (zero? n) 1 (* n (fact0 (- n 1))))))(define fact2 (lambda (n) (if (zero? n) 1 (* n (fact1 (- n 1))))))(define fact3 (lambda (n) (if (zero? n) 1 (* n (fact2 (- n 1))))))...
真正的阶乘函数是fact-infinity,具有无限大小。因此,我们又回到了最初的问题……
为了帮助使事情更简洁,我们可以观察上面的重复模式,并提取一个抽象化此模式的函数。这个函数与我们之前看到的fact-core相同:
(define fact-core (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define fact0 (fact-core 777))(define fact1 (fact-core fact0))(define fact2 (fact-core fact1))(define fact3 (fact-core fact2))...
实际上是:
(define fact-core (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define fact0 (fact-core 777))(define fact1 (fact-core (fact-core 777)))(define fact2 (fact-core (fact-core (fact-core 777))))...(define fact (fact-core (fact-core (fact-core (... (fact-core 777) ...)))))
稍微换个方式 —— 将fact0重写为:
(define fact0 ((lambda (mk) (mk 777)) fact-core))
类似地,fact1被写成:
(define fact1 ((lambda (mk) (mk (mk 777))) fact-core))
依此类推,直到真正的阶乘,这在这个阶段仍然是无限的:
(define fact ((lambda (mk) (mk (mk (... (mk 777) ...)))) fact-core))
现在,看看那个(lambda (mk) ...) —— 它是一个无限表达式,但对于结果阶乘函数的每一次实际应用,我们只需要有限数量的mk应用。我们可以猜测有多少,一旦我们达到777的应用,我们就知道我们的猜测太小了。所以,我们可以尝试使用制造函数来创建并使用下一个。
为了使事情更明确,这里是我们的fact0的表达式,没有定义形式:
((lambda (mk) (mk 777)) fact-core)
这个函数的猜测非常低 —— 对于 0,它能工作,但对于 1,它会遇到 777 应用。在这一点上,我们想要以某种方式再次调用 mk 以获得下一个级别 —— 而由于 777 确实被应用了,我们可以直接将其替换为 mk:
((lambda (mk) (mk mk)) fact-core)
结果函数对于输入 0 的效果完全相同,因为它不会尝试递归调用 —— 但如果我们给它 1,那么它不会遇到应用 777 的错误:
(* n (777 (- n 1)))
我们可以在那里应用 fact-core:
(* n (fact-core (- n 1)))
而这仍然是错误的,因为 fact-core 期望的是一个函数作为输入。为了更清楚地看到发生了什么,明确地写出 fact-core:
((lambda (mk) (mk mk)) (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
问题在于我们将传递给 fact-core 的内容 —— 其 fact 参数不会是阶乘函数,而是 mk 函数构造器。将 fact 参数重命名为 mk 将使这一点更加明显(但不改变含义):
((lambda (mk) (mk mk)) (lambda (mk) (lambda (n) (if (zero? n) 1 (* n (mk (- n 1)))))))
现在应该很明显,这个 mk 的应用不会起作用,相反,我们需要在某个函数上应用它,然后将结果应用在 (- n 1) 上。为了得到我们之前的结果,我们可以使用 777 作为一个虚构的函数:
((lambda (mk) (mk mk)) (lambda (mk) (lambda (n) (if (zero? n) 1 (* n ((mk 777) (- n 1)))))))
这将允许一个递归调用 —— 所以该定义对 0 和 1 的输入都有效 —— 但不再多。但现在 777 被用作一个制造函数,所以我们可以再次只使用 mk 本身:
((lambda (mk) (mk mk)) (lambda (mk) (lambda (n) (if (zero? n) 1 (* n ((mk mk) (- n 1)))))))
这是一个可工作的真实阶乘函数版本,所以将其转换为(非魔法的)定义:
(define fact ((lambda (mk) (mk mk)) (lambda (mk) (lambda (n) (if (zero? n) 1 (* n ((mk mk) (- n 1))))))))
但我们还没结束 —— 我们“闯入”了阶乘代码以插入那个 (mk mk) 应用 —— 这就是为什么我们引入了 fact-core 的实际值。我们现在需要修复这个问题。那最后一行的表达式
(lambda (n) (if (zero? n) 1 (* n ((mk mk) (- n 1)))))
足够接近 —— 它是 (fact-core (mk mk))。所以我们现在可以尝试重写我们的 fact:
(define fact-core (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define fact ((lambda (mk) (mk mk)) (lambda (mk) (fact-core (mk mk)))))
… 以熟悉的方式失败!如果不够熟悉,只需将所有的 mk 重命名为 x:
(define fact-core (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define fact ((lambda (x) (x x)) (lambda (x) (fact-core (x x)))))
我们再次遇到了语言的急切性,就像之前一样。解决方案是一样的 —— (x x) 就是阶乘函数,所以像之前一样保护它,我们就有了一个可工作的版本:
(define fact-core (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define fact ((lambda (x) (x x)) (lambda (x) (fact-core (lambda (n) ((x x) n))))))
其余部分现在不应该让人感到意外了… 在一个新的 make-recursive 函数中将递归生成部分抽象出来:
(define fact-core (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))(define (make-recursive f) ((lambda (x) (x x)) (lambda (x) (f (lambda (n) ((x x) n))))))(define fact (make-recursive fact-core))
现在我们可以在 make-recursive 中进行第一次简化,并明确地写出 fact-core 表达式:
#lang pl broken(define (make-recursive f) ((lambda (x) (f (lambda (n) ((x x) n)))) (lambda (x) (f (lambda (n) ((x x) n))))))(define fact (make-recursive (lambda (fact) (lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))))
而这正是我们之前的相同代码。
Y 组合子星期二,2 月 14 日
我们的make-recursive函数通常被称为不动点运算符或Y 组合子。
使用懒惰版本时看起来非常简单(记住:我们的版本是急切的):
(define Y (lambda (f) ((lambda (x) (f (x x))) (lambda (x) (f (x x))))))
请注意,如果我们允许对 Y 本身进行递归定义,那么定义可以遵循我们所见过的定义:
(define (Y f) (f (Y f)))
这一切都来自于由以下循环生成的:
((lambda (x) (x x)) (lambda (x) (x x)))
这个表达式,也被称为Omega((lambda (x) (x x))部分本身通常被称为omega,然后(omega omega)是Omega),也是许多深层数学事实背后的理念。作为它所做的示例,遵循下一个规则:
I will say the next sentence twice: "I will say the next sentence twice".
(注意第一个冒号和第二个引号的用法 — 在λ表达式中的等价物是什么?)
单独来看,这只会让你陷入无限循环,就像 Omega 一样,而 Y 组合子将F添加到其中以获得无限的应用链 — 这类似于:
I will say the next sentence twice: "I will hop on one foot and then say the next sentence twice".
Y 的主要属性星期二,2 月 14 日
fact-core是一个函数,给定任何有限阶乘,将生成一个适用于一个更多整数输入的阶乘。从777开始,这是一个对于任何东西都不好的阶乘(因为它不是一个函数),你可以得到fact0如下:
fact0 == (fact-core 777)
这只对输入为0时是一个好的阶乘函数。再次使用它与fact-core,你会得到
fact1 == (fact-core fact0) == (fact-core (fact-core 777))
当你只查看输入值为0或1时,这就是阶乘函数。
fact2 == (fact-core fact1)
对于0…2是有用的 — 我们可以继续尽可能多,除了我们需要有无限次应用 — 在一般情况下,我们有:
fact-n == (fact-core (fact-core (fact-core ... 777)))
这对于0…n是有用的。真正的阶乘将是在自身上无限运行fact-core的结果,它是fact-infinity。换句话说(这里fact是真正的阶乘):
fact = fact-infinity == (fact-core (fact-core ...infinitely...))
但请注意,由于这实际上是无穷大,那么
fact = (fact-core (fact-core ...infinitely...)) = (fact-core fact)
所以我们得到一个方程:
fact = (fact-core fact)
并且这个解决方案将是真正的阶乘。解决方案是fact-core函数的不动点,就像0是sin函数的不动点一样,因为
0 = (sin 0)
而 Y 组合子正是这样做的 — 它具有这个属性:
(make-recursive f) = (f (make-recursive f))
或者,使用更常见的名称:
(Y f) = (f (Y f))
这个属性封装了 Y 的真正魔力。你可以看到它是如何工作的:因为(Y f) = (f (Y f)),我们可以在两边都加上f应用,得到(f (Y f)) = (f (f (Y f))),所以我们得到:
(Y f) = (f (Y f)) = (f (f (Y f))) = (f (f (f (Y f)))) = ... = (f (f (f ...)))
我们可以得出结论
(Y fact-core) = (fact-core (fact-core ...infinitely...)) = fact
输入 Y 组合子星期二,2 月 14 日
输入 Y 组合子的类型是一个棘手的问题。例如,在标准 ML 中,你必须编写一个新的类型定义来实现这一点:
datatype 'a t = T of 'a t -> 'aval y = fn f => (fn (T x) => (f (fn a => x (T x) a))) (T (fn (T x) => (f (fn a => x (T x) a))))
你能找到
T被使用的地方的模式吗? — 粗略地说,该类型定义是;; `t' is the type name, `T' is the constructor (aka the variant)(define-type (RecType a) ; we don't really have polymorphic types [T ((RecType a) -> a)])首先注意两个
fn a => ...部分与我们的保护是相同的,所以忽略掉,我们得到:val y = fn f => (fn (T x) => (f (x (T x)))) (T (fn (T x) => (f (x (T x)))))如果你现在用
Quote替换T,事情就更清楚了:val y = fn f => (fn (Quote x) => (f (x (Quote x)))) (Quote (fn (Quote x) => (f (x (Quote x)))))而根据我们的语法,这将是:
(define (Y f) ((lambda (qx) (cases qx [(Quote x) (f (x (Quote x)))])) (Quote (lambda (qx) (cases qx [(Quote x) (f (x (Quote x)))])))))这并不是真正的引用 — 但这个类比应该有所帮助:它使用
Quote来区分作为值被应用的函数(x)和作为参数传递的函数。
在 OCaml 中,这看起来有些不同:
# type 'a t = T of ('a t -> 'a) ;;type 'a t = T of ('a t -> 'a)# let y f = (fun (T x) -> x (T x)) (T (fun (T x) -> fun z -> f (x (T x)) z)) ;;val y : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun># let fact = y (fun fact n -> if n<1 then 1 else n* fact(n-1)) ;;val fact : int -> int = <fun># fact 5 ;;- : int = 120
但是 OCaml 也有一个-rectypes命令行参数,它会自动推断类型:
# let y f = (fun x -> x x) (fun x -> fun z -> f (x x) z) ;;val y : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun># let fact = y (fun fact n -> if n<1 then 1 else n* fact(n-1)) ;;val fact : int -> int = <fun># fact 5 ;;- : int = 120
在 Typed Racket 中也可以写出这个表达式,但我们需要编写一个适当的类型定义。首先,Y 的类型应该很简单:它是一个不动点操作,所以它接受一个T -> T函数并产生它的不动点。不动点本身是一些T(应用该函数后结果仍然是它自己)。因此,这给我们带来了:
(: make-recursive : (T -> T) -> T)
然而,在我们的情况下,make-recursive计算一个函数不动点,对于一元的S -> T函数,因此我们应该缩小类型
(: make-recursive : ((S -> T) -> (S -> T)) -> (S -> T))
现在,在make-recursive的主体中,我们需要为x参数添加一个类型,因为它的行为有些奇怪:它既被用作函数,又被用作自己的参数。(记住 — 我会说下一句话两次:“我会说下一句话两次”。)我们需要一个递归类型定义:
(define-type (Tau S T) = (Rec this (this -> (S -> T))))
这种类型是为我们对x的使用量身定制的:给定一个类型T,x是一个将自身消耗掉(因此是Rec)并输出f参数消耗的值 — 一个S -> T函数。
代码的最终完整版本:
(: make-recursive : (All (S T) ((S -> T) -> (S -> T)) -> (S -> T)))(define-type (Tau S T) = (Rec this (this -> (S -> T))))(define (make-recursive f) ((lambda ([x : (Tau S T)]) (f (lambda (z) ((x x) z)))) (lambda ([x : (Tau S T)]) (f (lambda (z) ((x x) z))))))(: fact : Number -> Number)(define fact (make-recursive (lambda ([fact : (Number -> Number)]) (lambda ([n : Number]) (if (zero? n) 1 (* n (fact (- n 1))))))))(fact 5)
Lambda 演算 —— SchlacTuesday,February 14th
PLAI §22(我们做的远不止如此)
我们知道,许多通常被认为是原语的构造实际上是不需要的——只要给予足够的工具,我们就可以自己实现它们。问题是我们能走多远?
答案:我们可以走得很远。例如:
(define foo((lambda(f)((lambda(x)(x x))(lambda(x)(f(x x)))))(lambda(f)(lambda(x)(((x(lambda(x)(lambda(x y)y))(lambda(x y)x))(x(lambda(x)(lambda(x y)y))(lambda(x y)x))(((x(lambda (p)(lambda(s)(s(p(lambda(xy)y))(lambda(f x)(f((p(lambda(x y)y))f x))))))(lambda(s) (s(lambda(fx)x)(lambda(f x)x))))(lambda(x y)x))(lambda(x)(lambda(x y)y))(lambda(x y)x)))(lambda(f x)(f x))((f((x(lambda(p)(lambda(s)(s(p(lambda(x y)y))(lambda(f x)(f((p(lambda(x y)y))f x))))))(lambda(y s)(s(lambda(fx)x)(lambda(f x)x))))(lambda(x y)x)))(lambda(n)(lambda(f x)(f(n f x))))(f((((x(lambda(p)(lambda(s)(s(p (lambda(x y)y))(lambda(f x)(f((p(lambda(x y)y))f x))))))(lambda(s)(s(lambda(f x) x)(lambda(f x)x))))(lambda(x y)x))(lambda(p)(lambda(s)(s(p(lambda(x y)y))(lambda(f x)(f((p(lambda(x y)y))f x))))))(lambda(s)(s(lambda(f x)x)(lambda(f x)x))))(lambda(x y)x)))))))))
我们从一个非常基本的语言开始,这个语言基于 Lambda 演算。在这种语言中,我们获得了一组非常基本的构造和值。
在 DrRacket 中,我们将使用 Schlac 语言级别(代表“SchemeRacket 作为 Lambda 演算”)。这种语言具有类似 Racket 的语法,但不要混淆——它与 Racket 非常 不同。此语言中唯一可用的构造是:至少具有一个参数的 lambda 表达式、函数应用(同样,至少有一个参数)和类似于“破损的定义”语言中的简单定义形式——定义用作简写,不能用于递归函数定义。它们也只允许在顶层使用——没有本地助手,并且定义不是可以出现在任何地方的表达式。因此,BNF 是:
<SCHLAC> ::= <SCHLAC-TOP> ...<SCHLAC-TOP> ::= <SCHLAC-EXPR> | (define <id> <SCHLAC-EXPR>)<SCHLAC-EXPR> ::= <id> | (lambda (<id> <id> ...) <SCHLAC-EXPR>) | (<SCHLAC-EXPR> <SCHLAC-EXPR> <SCHLAC-EXPR> ...)
由于此语言没有原始值(除了函数之外),Racket 数字和布尔值也被视为标识符,并且没有随语言一起提供的内置值。此外,所有函数和函数调用都是柯里化的,所以
(lambda (x y z) (z y x))
实际上是上述的简写形式
(lambda (x) (lambda (y) (lambda (z) ((z y) x))))
评估规则很简单,其中有一个非常重要的评估规则称为“β-还原”:
((lambda (x) E1) E2) --> E1[E2/x]
在这种情况下,替换需要小心,以免捕获名称。这要求你能够进行另一种称为“α-转换”的转换,它基本上表示你可以重命名标识符,只要保持相同的绑定结构(例如,有效的重命名不会改变表达式的 de-Bruijn 形式)。还有一条可以使用的规则,eta 转换,它表示 (lambda (x) (f x)) 与 f 相同(我们在推导 Y 组合子时使用了这个规则)。
Schlac 和 Racket 之间的最后一个区别是 Schlac 是一个 惰性 语言。这一点很重要,因为我们没有像 if 这样的内置特殊形式。
这是对恒等函数的 Schlac 定义:
(define identity (lambda (x) x))
现在我们对此无能为力:
> identity#<procedure:identity>> (identity identity)#<procedure:identity>> (identity identity identity)#<procedure:identity>
(在最后一个表达式中,请注意 (id id id) 是 ((id id)id) 的简写,而且由于 (id id) 是恒等函数,将其应用于 id 再次返回它。)
Church 数周二,2 月 14 日
到目前为止,似乎在这种语言中无法做任何有用的事情,因为我们只有函数和应用。我们知道如何编写恒等函数,但其他值呢?例如,您能写出计算为零的代码吗?
零是什么?我只知道如何写函数!
(图灵机程序员:“什么是函数? — 我只知道如何写 0 和 1!”)
因此,我们首先需要能够将数字编码为函数。对于零,我们将使用一个简单返回其第二个值的两个参数的函数:
(define 0 (lambda (f) (lambda (x) x)))
或者,更简洁地说
(define 0 (lambda (f x) x))
这是已知为Church 数的编码的第一步:将自然数编码为函数。数字零被编码为一个接受函数和第二个值的函数,并在参数上零次应用函数(这实际上是上述定义在做什么)。根据这个观点,数字一将是一个两个参数的函数,将第一个参数应用于第二个一次:
(define 1 (lambda (f x) (f x)))
请注意,1就像恒等函数一样(只要您给它一个函数作为其第一个输入,但这在 Schlac 中始终如此)。列表中的下一个数字是二 — 它将第一个参数应用于第二个参数两次:
(define 2 (lambda (f x) (f (f x))))
我们可以继续这样做,但我们真正想要的是执行任意算术的方法。为此的第一个要求是一个add1函数,它将其输入(一个编码的自然数)增加一。为此,我们编写一个期望编码数字的函数:
(define add1 (lambda (n) ...))
这个函数预期返回一个编码的数字,它总是一个关于f和x的函数:
(define add1 (lambda (n) (lambda (f x) ...)))
现在,在主体中,我们需要将f应用于x n+1 次 — 但请记住,n是一个函数,将对其第一个参数在其第二个参数上进行n次应用:
(define add1 (lambda (n) (lambda (f x) ... (n f x) ...)))
现在我们只需再次应用f,得到add1的这个定义:
(define add1 (lambda (n) (lambda (f x) (f (n f x)))))
使用这个,我们可以定义一些有用的数字:
(define 1 (add1 0))(define 2 (add1 1))(define 3 (add1 2))(define 4 (add1 3))(define 5 (add1 4))
理论上这一切都很好,但我们如何确保它是正确的呢?好吧,Schlac 有一些额外的特殊形式,将 Church 数转换为 Racket 数。要尝试我们的定义,我们使用->nat(读作:转换为自然数):
(->nat 0)(->nat 5)(->nat (add1 (add1 5)))
现在,您可以验证恒等函数确实与数字 1 相同:
(->nat identity)
我们甚至可以编写一个测试用例,因为 Schlac 包含test特殊形式,但我们必须小心 — 首先,我们不能测试函数是否相等(为什么?),所以我们必须使用->nat,但
(test (->nat (add1 (add1 5))) => 7)
将不起作用,因为7未定义。为了克服这一点,Schlac 为原始 Racket 值提供了一个后门 — 只需使用引号:
(test (->nat (add1 (add1 5))) => '7)
教堂数(续)星期二,二月二十一日
现在我们可以定义自然数加法 — 一个简单的想法是得到两个编码数字 m 和 n,然后从 x 开始,通过将它用作函数,在结果上应用 n 次 f,然后以相同的方式在结果上再应用 m 次 f:
(define + (lambda (m n) (lambda (f x) (m f (n f x)))))
或等价地:
(define + (lambda (m n f x) (m f (n f x))))
另一个想法是使用 add1 并使用 add1 将 n 增加 m:
(define + (lambda (m n) (m add1 n)))(->nat (+ 4 5))
我们也可以很容易地定义 m 和 n 的乘法 — 从加法开始 — (lambda (x) (+ n x)) 是一个期望 x 并返回 (+ x n) 的函数 — 这是一个增量 n 的函数。但由于所有函数和应用都是柯里化的,这实际上与 (lambda (x) ((+ n) x)) 相同,这与 (+ n) 相同。现在,我们要做的是将此操作重复 m 次在零上,这将零加 n m 次,结果为 m * n。因此,定义是:
(define * (lambda (m n) (m (+ n) 0)))(->nat (* 4 5))(->nat (+ 4 (* (+ 2 5) 5)))
另一种方法是考虑
(lambda (x) (n f x))
对于某个编码数字 n 和一个函数 f — 这个函数就像 f^n(f 自己与自己组合 n 次)。但请记住,这只是简写形式
(lambda (x) ((n f) x))
我们知道 (lambda (x) (foo x)) 就像 foo(如果它是一个函数),所以这等价于
(n f)
因此 (n f) 是 f^n,以相同的方式 (m g) 是 g^m — 如果我们将 (n f) 用于 g,我们得到 (m (n f)),这是 f 的 n 次自组合,自组合 m 次。换句话说,(m (n f)) 是一个类似于 f 的 m*n 次应用的函数,因此我们可以将乘法定义为:
(define * (lambda (m n) (lambda (f) (m (n f)))))
这与之相同
(define * (lambda (m n f) (m (n f))))
相同的原理可以用来定义指数(但现在我们必须注意顺序,因为指数不是可交换的):
(define ^ (lambda (m n) (n (* m) 1)))(->nat (^ 3 4))
这里也有一个类似的替代方案 —
-
教堂数
m是 m-自组合函数, -
而
(1 m)就像m^1,与m相同(1=identity) -
而
(2 m)就像m^2— 它接受一个函数f,自我组合m次,然后将结果再自我组合m次 — 总共f^(m*m) -
而
(3 m)类似于f^(m*m*m) -
因此
(n m)是f^(m^n)(注意第一个^是自组合,第二个是数学上的指数) -
因此,
(n m)是一个返回输入函数的m^n自组合的函数,这意味着(n m)是m^n的教堂数,因此我们得到:(define ^ (lambda (m n) (n m)))
这基本上是说任何编码数字 n 也是 ?^n 运算。
所有这些都不是太复杂的事情 — 但到目前为止我们所做的一切都是以各种方式编写增加其输入的函数。sub1 呢?为此,我们需要做更多的工作 — 我们需要编码布尔值。
更多编码星期二,二月二十一日
我们选择编码数字的方式是有意义的 — 主要特征是一个自然数重复某事多少次。对于布尔值,我们正在寻找的主要属性是在两个值之间进行选择。因此,我们可以通过返回两个参数的函数来编码 true 和 false,这些函数返回第一个参数或第二个参数:
(define #t (lambda (x y) x))(define #f (lambda (x y) y))
请注意,#f的这种编码实际上与0的编码相同,因此我们必须知道期望的类型并使用适当的操作(这类似于 C,其中一切都只是整数)。现在我们有了这两个,我们可以定义if:
(define if (lambda (c t e) (c t e)))
它期望一个布尔值,这是一个两个参数的函数,并将这两个表达式传递给它。#t布尔值将简单地返回第一个,而#f布尔值将返回第二个。严格来说,我们实际上不需要这个定义,因为我们可以简单地写(c t e)而不是写(if c t e)。无论如何,我们需要语言是惰性的才能使其工作。为了证明这一点,我们故意使用引号后门使用非函数值,通常会导致错误:
(+ '1 '2)
但测试我们的if定义,一切都很顺利:
(if #t (+ 4 5) (+ 1 2))
我们看到 DrRacket 将第二个加法表达式标记为红色,这表明它没有被执行。我们还可以确保即使它被定义为一个函数,它仍然正常工作,因为这种语言是惰性的:
(if #f ((lambda (x) (x x)) (lambda (x) (x x))) 3)
and和or呢?简单,or接受两个参数,如果其中一个输入为真,则返回 true 或 false:
(define or (lambda (a b) (if a #t (if b #t #f))))
但(if b #t #f)实际上与b是一样的,因为它是一个布尔值:
(define or (lambda (a b) (if a #t b)))
同样,如果a为真,我们希望返回#t,但那恰好是a的值,所以:
(define or (lambda (a b) (if a a b)))
最后,我们可以摆脱if(实际上是破坏了if的抽象,如果我们以其他方式编码布尔值):
(define or (lambda (a b) (a a b)))
同样,让自己相信and的定义是:
(define and (lambda (a b) (a b a)))
Schlac 也有布尔值的 to-Racket 转换形式:
(->bool (or #f #f))(->bool (or #f #t))(->bool (or #t #f))(->bool (or #t #t))
和
(->bool (and #f #f))(->bool (and #f #t))(->bool (and #t #f))(->bool (and #t #t))
not函数很简单 — 一种选择是按照通常的方式从真和假中选择:
(define not (lambda (a) (a #f #t)))
另一个是返回一个将输入切换为输入布尔值的函数:
(define not (lambda (a) (lambda (x y) (a y x))))
这与
(define not (lambda (a x y) (a y x)))
现在我们可以将数字和布尔值结合起来:我们定义一个zero?函数。
(define zero? (lambda (n) (n (lambda (x) #f) #t)))(test (->bool (and (zero? 0) (not (zero? 3)))) => '#t)
(好问题:这个快吗?)
(请注意,最好测试该值是否明确为#t,如果我们只使用(test (->bool ...)),那么即使所讨论的表达式评估为某些虚假值,测试也会成功。)
这个想法很简单 — 如果n是零的编码,它将返回它的第二个参数,即#t:
(zero? 0) --> ((lambda (f n) n) (lambda (x) #f) #t) -> #t
如果n是一个更大数字的编码,那么它是一个自我组合,我们给它的函数是一个无论自我组合多少次都会始终返回#f的函数。例如尝试2:
(zero? 2) --> ((lambda (f n) (f (f n))) (lambda (x) #f) #t) --> ((lambda (x) #f) ((lambda (x) #f) #t)) --> #f
现在,如何对复合值进行编码呢?我们在 Racket 中使用的是一种最小化的方法 —— 一种生成对(cons)的方法,并将列表编码为具有特殊值的对链(null 结束)。我们之前见过对的自然编码 —— 一对是一个期望选择器的函数,并且将其应用于两个值:
(define cons (lambda (x y) (lambda (s) (s x y))))
或者,等价地:
(define cons (lambda (x y s) (s x y)))
要从一对中提取两个值,我们需要传递一个选择器,该选择器消耗两个值并返回其中一个。在我们的框架中,这正是两个布尔值所做的,所以我们得到了:
(define car (lambda (x) (x #t)))(define cdr (lambda (x) (x #f)))(->nat (+ (car (cons 2 3)) (cdr (cons 2 3))))
我们甚至可以这样做:
(define 1st car)(define 2nd (lambda (l) (car (cdr l))))(define 3rd (lambda (l) (car (cdr (cdr l)))))(define 4th (lambda (l) (car (cdr (cdr (cdr l))))))(define 5th (lambda (l) (car (cdr (cdr (cdr (cdr l)))))))
或者写一个 list-ref 函数:
(define list-ref (lambda (l n) (car (n cdr l))))
注意,我们不需要递归函数来实现这一点:我们对自然数的编码使得“迭代 N 次”变得容易。使用这种编码,我们得到的本质上是免费的自然数递归。
现在我们需要一个特殊的 null 值来标记列表的结束。这个值应该具有与 cons 值相同数量的参数(一个:选择器/布尔函数),并且应该能够将它与其他值区分开。我们选择
(define null (lambda (s) #t))
测试列表编码:
(define l123 (cons 1 (cons 2 (cons 3 null))))(->nat (2nd l123))
而且,与自然数和布尔值一样,Schlac 还具有将编码列表转换为 Racket 值的内置功能,只是这需要指定列表中值的类型,因此它是一个高阶函数:
((->listof ->nat) l123)
这可以(“像往常一样”)写成
(->listof ->nat l123)
我们甚至可以这样做:
(->listof (->listof ->nat) (cons l123 (cons l123 null)))
定义 null? 现在相对容易了(实际上它已经被上面的 ->listof 转换所使用)。以下是定义:
(define null? (lambda (x) (x (lambda (x y) #f))))
这个工作是因为如果 x 是空的,那么它简单地忽略它的参数并返回 #t,如果它是一对,那么它就使用输入选择器,该选择器在其轮次中始终返回 #f。使用一些任意的 A 和 B:
(null? (cons A B)) --> ((lambda (x) (x (lambda (x y) #f))) (lambda (s) (s A B))) --> ((lambda (s) (s A B)) (lambda (x y) #f)) --> ((lambda (x y) #f) A B) --> #f(null? null) --> ((lambda (x) (x (lambda (x y) #f))) (lambda (s) #t)) --> ((lambda (s) #t) (lambda (x y) #f)) --> #t
我们可以使用 Y 组合子创建递归函数 —— 我们甚至可以使用 Schlac 包含的重写规则功能(我们之前见过的那个):
(define Y (lambda (f) ((lambda (x) (x x)) (lambda (x) (f (x x))))))(rewrite (define/rec f E) => (define f (Y (lambda (f) E))))
并使用它:
(define/rec length (lambda (l) (if (null? l) 0 (add1 (length (cdr l))))))(->nat (length l123))
要完成这个,嗯,旅程 — 我们还缺少减法。解决减法问题的方法有很多种,如果你想挑战自己,可以试着自己想出一个解决方案。其中一个较为清晰的解决方案使用了一个简单的思想 — 从一对零 <0,0> 开始,并重复这个转换 n 次: <a,b> -> <b,b+1>。经过 n 步,我们会得到 <n-1,n> — 所以我们得到了:
(define inccons (lambda (p) (cons (cdr p) (add1 (cdr p)))))(define sub1 (lambda (n) (car (n inccons (cons 0 0)))))(->nat (sub1 5))
从这里到一般的减法的道路很短,m-n 简单地是在 m 上应用 sub1 n 次:
(define - (lambda (m n) (n sub1 m)))(test (->nat (- 3 2)) => '1)(test (->nat (- (* 4 (* 5 5)) 5)) => '95)
现在我们有了一个看起来很正常的语言,我们可以做任何我们想做的事情。以下是两个流行的例子:
(define/rec fact (lambda (x) (if (zero? x) 1 (* x (fact (sub1 x))))))(test (->nat (fact 5)) => '120)(define/rec fib (lambda (x) (if (or (zero? x) (zero? (sub1 x))) 1 (+ (fib (- x 1)) (fib (- x 2))))))(test (->nat (fib (* 5 2))) => '89)
为了获得广义算术能力,Schlac 还有另一个内置设施,用于将 Racket 自然数转换为 Church 数:
(->nat (fib (nat-> '10)))
… 要得到开头那可怕的表达式,你只需要一遍又一遍地替换fib定义中的所有定义,直到剩下的只有λ表达式和应用,然后将结果重新格式化成一些可爱的形状。为了增加乐趣,你可以寻找λ表达式的直接应用,并手动减少它们。
所有这些都在下面的代码中:
▶;; Making Schlac into a practical language (not an interpreter)#lang pl schlac(define identity (lambda (x) x));; Natural numbers(define 0 (lambda (f x) x))(define add1 (lambda (n) (lambda (f x) (f (n f x)))));; same as:;; (define add1 (lambda (n) (lambda (f x) (n f (f x)))))(define 1 (add1 0))(define 2 (add1 1))(define 3 (add1 2))(define 4 (add1 3))(define 5 (add1 4))(test (->nat (add1 (add1 5))) => '7)(define + (lambda (m n) (m add1 n)))(test (->nat (+ 4 5)) => '9);; (define * (lambda (m n) (m (+ n) 0)))(define * (lambda (m n f) (m (n f))))(test (->nat (* 4 5)) => '20)(test (->nat (+ 4 (* (+ 2 5) 5))) => '39);; (define ^ (lambda (m n) (n (* m) 1)))(define ^ (lambda (m n) (n m)))(test (->nat (^ 3 4)) => '81);; Booleans(define #t (lambda (x y) x))(define #f (lambda (x y) y))(define if (lambda (c t e) (c t e))) ; not really needed(test (->nat (if #t 1 2)) => '1)(test (->nat (if #t (+ 4 5) (+ '1 '2))) => '9)(define and (lambda (a b) (a b a)))(define or (lambda (a b) (a a b)));; (define not (lambda (a) (a #f #t)))(define not (lambda (a x y) (a y x)))(test (->bool (and #f #f)) => '#f)(test (->bool (and #t #f)) => '#f)(test (->bool (and #f #t)) => '#f)(test (->bool (and #t #t)) => '#t)(test (->bool (or #f #f)) => '#f)(test (->bool (or #t #f)) => '#t)(test (->bool (or #f #t)) => '#t)(test (->bool (or #t #t)) => '#t)(test (->bool (not #f)) => '#t)(test (->bool (not #t)) => '#f)(define zero? (lambda (n) (n (lambda (x) #f) #t)))(test (->bool (and (zero? 0) (not (zero? 3)))) => '#t);; Lists(define cons (lambda (x y s) (s x y)))(define car (lambda (x) (x #t)))(define cdr (lambda (x) (x #f)))(test (->nat (+ (car (cons 2 3)) (cdr (cons 2 3)))) => '5)(define 1st car)(define 2nd (lambda (l) (car (cdr l))))(define 3rd (lambda (l) (car (cdr (cdr l)))))(define 4th (lambda (l) (car (cdr (cdr (cdr l))))))(define 5th (lambda (l) (car (cdr (cdr (cdr (cdr l)))))))(define null (lambda (s) #t))(define null? (lambda (x) (x (lambda (x y) #f))))(define l123 (cons 1 (cons 2 (cons 3 null))));; Note that `->listof' is a H.O. converter(test ((->listof ->nat) l123) => '(1 2 3))(test (->listof ->nat l123) => '(1 2 3)) ; same as the above(test (->listof (->listof ->nat) (cons l123 (cons l123 null))) => '((1 2 3) (1 2 3)));; Subtraction is tricky(define inccons (lambda (p) (cons (cdr p) (add1 (cdr p)))))(define sub1 (lambda (n) (car (n inccons (cons 0 0)))))(test (->nat (sub1 5)) => '4)(define - (lambda (a b) (b sub1 a)))(test (->nat (- 3 2)) => '1)(test (->nat (- (* 4 (* 5 5)) 5)) => '95)(test (->nat (- 2 4)) => '0) ; this is "natural subtraction";; Recursive functions(define Y (lambda (f) ((lambda (x) (x x)) (lambda (x) (f (x x))))))(rewrite (define/rec f E) => (define f (Y (lambda (f) E))))(define/rec length (lambda (l) (if (null? l) 0 (add1 (length (cdr l))))))(test (->nat (length l123)) => '3)(define/rec fact (lambda (x) (if (zero? x) 1 (* x (fact (sub1 x))))))(test (->nat (fact 5)) => '120)(define/rec fib (lambda (x) (if (or (zero? x) (zero? (sub1 x))) 1 (+ (fib (sub1 x)) (fib (sub1 (sub1 x)))))))(test (->nat (fib (* 5 2))) => '89)#|;; Fully-expanded Fibonacci(define fib ((lambda (f) ((lambda (x) (x x)) (lambda (x) (f (x x))))) (lambda (f) (lambda (x) ((lambda (c t e) (c t e)) ((lambda (a b) (a a b)) ((lambda (n) (n (lambda (x) (lambda (x y) y)) (lambda (x y) x))) x) ((lambda (n) (n (lambda (x) (lambda (x y) y)) (lambda (x y) x))) ((lambda (n) ((lambda (x) (x (lambda (x y) x))) (n (lambda (p) ((lambda (x y s) (s x y)) ((lambda (x) (x (lambda (x y) y))) p) ((lambda (n) (lambda (f x) (f (n f x)))) ((lambda (x) (x (lambda (x y) y))) p)))) ((lambda (x y s) (s x y)) (lambda (f x) x) (lambda (f x) x))))) x))) ((lambda (n) (lambda (f x) (f (n f x)))) (lambda (f x) x)) ((lambda (x y) (x (lambda (n) (lambda (f x) (f (n f x)))) y)) (f ((lambda (n) ((lambda (x) (x (lambda (x y) x))) (n (lambda (p) ((lambda (x y s) (s x y)) ((lambda (x) (x (lambda (x y) y))) p) ((lambda (n) (lambda (f x) (f (n f x)))) ((lambda (x) (x (lambda (x y) y))) p)))) ((lambda (x y s) (s x y)) (lambda (f x) x) (lambda (f x) x))))) x)) (f ((lambda (n) ((lambda (x) (x (lambda (x y) x))) (n (lambda (p) ((lambda (x y s) (s x y)) ((lambda (x) (x (lambda (x y) y))) p) ((lambda (n) (lambda (f x) (f (n f x)))) ((lambda (x) (x (lambda (x y) y))) p)))) ((lambda (x y s) (s x y)) (lambda (f x) x) (lambda (f x) x))))) ((lambda (n) ((lambda (x) (x (lambda (x y) x))) (n (lambda (p) ((lambda (x y s) (s x y)) ((lambda (x) (x (lambda (x y) y))) p) ((lambda (n) (lambda (f x) (f (n f x)))) ((lambda (x) (x (lambda (x y) y))) p)))) ((lambda (x y s) (s x y)) (lambda (f x) x) (lambda (f x) x))))) x)))))))));; The same after reducing all immediate function applications(define fib ((lambda (f) ((lambda (x) (x x)) (lambda (x) (f (x x))))) (lambda (f) (lambda (x) (((x (lambda (x) (lambda (x y) y)) (lambda (x y) x)) (x (lambda (x) (lambda (x y) y)) (lambda (x y) x)) (((x (lambda (p) (lambda (s) (s (p (lambda (x y) y)) (lambda (f x) (f ((p (lambda (x y) y)) f x)))))) (lambda (s) (s (lambda (f x) x) (lambda (f x) x)))) (lambda (x y) x)) (lambda (x) (lambda (x y) y)) (lambda (x y) x))) (lambda (f x) (f x)) ((f ((x (lambda (p) (lambda (s) (s (p (lambda (x y) y)) (lambda (f x) (f ((p (lambda (x y) y)) f x)))))) (lambda (y s) (s (lambda (f x) x) (lambda (f x) x)))) (lambda (x y) x))) (lambda (n) (lambda (f x) (f (n f x)))) (f ((((x (lambda (p) (lambda (s) (s (p (lambda (x y) y)) (lambda (f x) (f ((p (lambda (x y) y)) f x)))))) (lambda (s) (s (lambda (f x) x) (lambda (f x) x)))) (lambda (x y) x)) (lambda (p) (lambda (s) (s (p (lambda (x y) y)) (lambda (f x) (f ((p (lambda (x y) y)) f x)))))) (lambda (s) (s (lambda (f x) x) (lambda (f x) x)))) (lambda (x y) x)))))))));; Cute reformatting of the above:(define fib((lambda(f)((lambda(x)(x x))(lambda(x)(f(x x)))))(lambda(f)(lambda(x)(((x(lambda(x)(lambda(x y)y))(lambda(x y)x))(x(lambda(x)(lambda(x y)y))(lambda(x y) x))(((x(lambda(p)(lambda(s)(s(p(lambda(xy)y))(lambda(f x)(f((p(lambda(x y)y))f x))))))(lambda(s) (s(lambda(fx)x)(lambda(f x)x))))(lambda(x y)x))(lambda(x)(lambda(x y)y))(lambda(x y)x)))(lambda(f x)(f x))((f((x(lambda(p)(lambda(s)(s(p(lambda(x y)y))(lambda(f x)(f((p(lambda(x y)y))f x))))))(lambda(y s)(s(lambda(fx)x)(lambda(f x)x))))(lambda(x y)x)))(lambda(n)(lambda(f x)(f(n f x))))(f((((x(lambda(p)(lambda(s)(s(p (lambda(x y)y))(lambda(f x)(f((p(lambda(x y) y))f x))))))(lambda(s)(s(lambda(f x)x)(lambda(f x)x))))(;; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^;; `---------------(cons 0 0)---------------'lambda(x y)x))(lambda(p)(lambda(s)(s(p(lambda(x y)y))(lambda(f x)(f((p(lambda(x y)y))f x))))))(lambda(s)(s(lambda(f x)x)(lambda(f x)x))))(lambda(x y)x)))))))))And for extra fun: (λ(f)(λ (x)(((x(λ( x)(λ(x y)y) )(λ(x y)x))( x(λ(x)(λ(x y) y))(λ(x y )x))((( x(λ(p)( λ(s)(s (p (λ( x y)y)) (λ(f x )(f((p( λ(x y) y))f x ))))))( λ(s)(s( λ(f x)x) (λ(f x)x) )))(λ(x y) x))(λ(x)(λ( x y)y)) (λ( x y) x)))(λ( f x)(f x))((f ((x(λ(p )(λ (s )(s(p( λ(x y) y))(λ ( f x)(f( (p (λ( x y)y) )f x))) )))(λ( y s)(s (λ (f x )x)(λ( f x)x) )))(λ( x y)x)) )(λ(n) (λ (f x)(f (n f x))) )(f((( (x(λ(p) (λ(s)(s (p( λ( x y )y ))(λ(f x) (f(( p(λ(x y )y)) f x))))) )(λ(s)( s(λ(f x )x)(λ( f x)x) ))) (λ (x y)x ))(λ(p )(λ(s)( s(p(λ( x y)y) )(λ (f x)(f(( p(λ (x y)y)) f x)))))) (λ(s)( s(λ (f x)x)(λ (f x)x) )))(λ( x y)x) ))))))|#
替代 Church 编码星期二,2 月 21 日
最后,请注意这只是编码的一种方式 — 还有其他可能的编码方式。另一种替代编码在以下代码中 — 它使用一个N个#f的列表作为N的编码。这种编码使得add1更容易(只需cons另一个#f),而sub1更容易(简单地cdr)。权衡是一些算术运算变得更加复杂,例如,+的定义需要不动点组合子。(正如预期的那样,有些人想看看在没有递归的语言下我们能做些什么,所以他们不喜欢太快地跳到 Y。)
▶;; An alternative "Church" encoding: use lists to encode numbers#lang pl schlac(define identity (lambda (x) x));; Booleans (same as before)(define #t (lambda (x y) x))(define #f (lambda (x y) y))(define if (lambda (c t e) (c t e))) ; not really needed(test (->bool (if #t #f #t)) => '#f)(test (->bool (if #f ((lambda (x) (x x)) (lambda (x) (x x))) #t)) => '#t)(define and (lambda (a b) (a b a)))(define or (lambda (a b) (a a b)))(define not (lambda (a x y) (a y x)))(test (->bool (and #f #f)) => '#f)(test (->bool (and #t #f)) => '#f)(test (->bool (and #f #t)) => '#f)(test (->bool (and #t #t)) => '#t)(test (->bool (or #f #f)) => '#f)(test (->bool (or #t #f)) => '#t)(test (->bool (or #f #t)) => '#t)(test (->bool (or #t #t)) => '#t)(test (->bool (not #f)) => '#t)(test (->bool (not #t)) => '#f);; Lists (same as before)(define cons (lambda (x y s) (s x y)))(define car (lambda (x) (x #t)))(define cdr (lambda (x) (x #f)))(define 1st car)(define 2nd (lambda (l) (car (cdr l))))(define 3rd (lambda (l) (car (cdr (cdr l)))))(define 4th (lambda (l) (car (cdr (cdr (cdr l))))))(define 5th (lambda (l) (car (cdr (cdr (cdr (cdr l)))))))(define null (lambda (s) #t))(define null? (lambda (x) (x (lambda (x y) #f))));; Natural numbers (alternate encoding)(define 0 identity)(define add1 (lambda (n) (cons #f n)))(define zero? car) ; tricky(define sub1 cdr) ; this becomes very simple;; Note that we could have used something more straightforward:;; (define 0 null);; (define add1 (lambda (n) (cons #t n))) ; cons anything;; (define zero? null?);; (define sub1 (lambda (l) (if (zero? l) l (cdr l))))(define 1 (add1 0))(define 2 (add1 1))(define 3 (add1 2))(define 4 (add1 3))(define 5 (add1 4))(test (->nat* (add1 (add1 5))) => '7)(test (->nat* (sub1 (sub1 (add1 (add1 5))))) => '5)(test (->bool (and (zero? 0) (not (zero? 3)))) => '#t)(test (->bool (zero? (sub1 (sub1 (sub1 3))))) => '#t);; list-of-numbers tests(define l123 (cons 1 (cons 2 (cons 3 null))))(test (->listof ->nat* l123) => '(1 2 3))(test (->listof (->listof ->nat*) (cons l123 (cons l123 null))) => '((1 2 3) (1 2 3)));; Recursive functions(define Y (lambda (f) ((lambda (x) (x x)) (lambda (x) (f (x x))))))(rewrite (define/rec f E) => (define f (Y (lambda (f) E))));; note that this example is doing something silly now(define/rec length (lambda (l) (if (null? l) 0 (add1 (length (cdr l))))))(test (->nat* (length l123)) => '3);; addition becomes hard since it requires a recursive definition;; (define/rec +;; (lambda (m n) (if (zero? n) m (+ (add1 m) (sub1 n)))));; (test (->nat* (+ 4 5)) => '9);; faster alternative:(define/rec + (lambda (m n) (if (zero? m) n (if (zero? n) m (add1 (add1 (+ (sub1 m) (sub1 n))))))))(test (->nat* (+ 4 5)) => '9);; subtraction is similar to addition;; (define/rec -;; (lambda (m n) (if (zero? n) m (- (sub1 m) (sub1 n)))));; (test (->nat* (- (+ 4 5) 4)) => '5);; but this is not "natural subtraction": doesn't work when n>m,;; because (sub1 0) does not return 0.;; a solution is like alternative form of +:(define/rec - (lambda (m n) (if (zero? m) 0 (if (zero? n) m (- (sub1 m) (sub1 n))))))(test (->nat* (- (+ 4 5) 4)) => '5)(test (->nat* (- 2 5)) => '0);; alternatively, could change sub1 above:;; (define sub1 (lambda (n) (if (zero? n) n (cdr n))));; we can do multiplication in a similar way(define/rec * (lambda (m n) (if (zero? m) 0 (+ n (* (sub1 m) n)))))(test (->nat* (* 4 5)) => '20)(test (->nat* (+ 4 (* (+ 2 5) 5))) => '39);; and the rest of the examples(define/rec fact (lambda (x) (if (zero? x) 1 (* x (fact (sub1 x))))))(test (->nat* (fact 5)) => '120)(define/rec fib (lambda (x) (if (or (zero? x) (zero? (sub1 x))) 1 (+ (fib (sub1 x)) (fib (sub1 (sub1 x)))))))(test (->nat* (fib (* 5 2))) => '89)#|;; Fully-expanded Fibonacci (note: much shorter than the previous;; encoding, but see how Y appears twice -- two "((lambda" pairs)(define fib((lambda(f)((lambda(x)(x x))(lambda(x)(f(x x)))))(lambda(f)(lambda(x)(((((x(lambda(x y)x))(x(lambda(x y)x)))((x(lambda(x y)y))(lambda(x y)x)))(lambda(s)(s(lambda(x y)y)(lambda(x)x))))((((lambda(f)((lambda(x)(x x))(lambda(x)(f(x x))))) (lambda(f)(lambda(m n)((m(lambda(x y)x))n (((n(lambda(x y)x)) m)(lambda(s)((s (lambda(x y)y))(lambda(s)((s (lambda(x y)y))((f(m(lambda(x y)y)))(n(lambda(x y)y))))))))))))(f(x(lambda(x y)y))))(f((x(lambda(x y)y))(lambda(x y)y)))))))))|#
实现列表的另一种有趣方法是遵循模式匹配方法,其中对对和空值都由充当一种match分发器的函数表示。此函数接受两个输入 — 如果它是空值的表示,则将返回第一个输入,如果是对,则将在对的两个部分上应用第二个输入。这是如何实现的:
(define null (lambda (n p) n))(define cons (lambda (x y) (lambda (n p) (p x y))))
这可能看起来有些奇怪,但它遵循了对对和空值作为匹配类似构造的预期用法。这是一个例子,并附上了等效的 Racket 代码:
;; Sums up a list of numbers(define (sum l) (l ; (match l 0 ; ['() 0] (lambda (x xs) ; [(cons x xs) (+ x (sum xs))))) ; (+ x (sum xs))])
实际上,使用这种方法实现我们的选择器和谓词很容易:
(define null? (lambda (l) (l #t (lambda (x xs) #f))))(define car (lambda (l) (l #f (lambda (x y) x))))(define cdr (lambda (l) (l #f (lambda (x y) y))));; in the above `#f' is really any value, since it;; should be an error alternatively:(define car (lambda (l) (l ((lambda (x) (x x)) (lambda (x) (x x))) ; "error" (lambda (x y) x))))
同样的方法可以用于以类似我们自己的define-type定义的方式定义任何类型的新数据类型。例如,考虑我们在学期初看到的 AE 类型的更简化的定义,以及用于使用cases的匹配eval定义作为示例:
(define-type AE [Num Number] [Add AE AE])(: eval : AE -> Number)(define (eval expr) (cases expr [(Num n) n] [(Add l r) (+ (eval l) (eval r))]))
现在我们可以按照上述方法编写 Schlac 代码,不仅等效,而且在性质上也非常相似。请注意,类型定义被两个构造函数的定义所替代:
(define Num (lambda (n) (lambda (num add) (num n ))))(define Add (lambda (l r) (lambda (num add) (add l r))))(define eval (lambda (expr) ; `expr` is always a (lambda (num add) ...), and it ; expects a unary `num` argument and a binary `add` (expr (lambda (n) n) (lambda (l r) (+ (eval l) (eval r))))))
递归环境星期二,2 月 21 日
PLAI §11.5
对于递归,我们真正需要的是一种特殊类型的环境,一种可以引用自身的环境。因此,我们不再这样做(注意:为了可读性而删除了call):
{with {fact {fun {n} {if {zero? n} 1 {* n {fact {- n 1}}}}}} {fact 5}}
这不适用于通常的原因,我们想使用一些
{rec {fact {fun {n} {if {zero? n} 1 {* n {fact {- n 1}}}}}} {fact 5}}
这将执行必要的魔法。
实现这一点的一种方法是使用我们之前看到的 Y 组合子——一种递归函数的“构造器”。我们可以以与我们在 Schlac 中看到的rewrite规则类似的方式进行——将上述表达式转换为:
{with {fact {make-rec {fun {fact} {fun {n} {if {zero? n} 1 {* n {fact {- n 1}}}}}}}} {fact 5}}
或者甚至:
{with {fact {{fun {f} {{fun {x} {f {x x}}} {fun {x} {f {x x}}}}} {fun {fact} {fun {n} {if {zero? n} 1 {* n {fact {- n 1}}}}}}}} {fact 5}}
现在,我们将看到如何在我们的代码中使用它来实现一个递归环境。
如果我们看一下with在
{with {fact {fun {n} {if {zero? n} 1 {* n {call fact {- n 1}}}}}} {call fact 5}}
那么我们可以说为了评估这个表达式,我们在一个扩展的环境中评估主体表达式,该环境包含fact,即使是一个仅适用于0的虚假环境——新环境是这样创建的:
extend("fact", make-fact-closure(), env)
所以我们可以把整个过程看作是对env的一个操作
add-fact(env) := extend("fact", make-fact-closure(), env)
这给了我们第一级的事实。但fact本身在env中仍然未定义,因此无法调用自身。我们可以尝试这样做:
add-fact(add-fact(env))
但这仍然不起作用,而且无论我们走多远,它永远都不会起作用:
add-fact(add-fact(add-fact(add-fact(add-fact(...env...)))))
我们真正想要的是无限:一个让add-fact工作且结果与我们开始的相同的地方——我们想要创建一个“神奇”的环境,使这成为可能:
let magic-env = ???such that: add-fact(magic-env) = magic-env
基本上给了我们处于无限点的幻觉。这个 magic-env 东西恰好是add-fact操作的不动点。我们可以使用:
magic-env = rec(add-fact)
并且根据 Y 组合子的主要属性,我们知道:
magic-env = rec(add-fact) ; def. of magic-env = add-fact(rec(add-fact)) ; Y(f) = f(Y(f)) = add-fact(magic-env) ; def. of magic-env
所有这一切意味着什么?这意味着如果我们在我们的环境实现层次上有一个不动点运算符,那么我们可以使用它来实现一个递归绑定器。在我们的情况下,这意味着 Racket 中的一个不动点可以用来实现一个递归语言。但我们有——Racket 确实有递归函数,所以我们应该能够使用它来实现我们的递归绑定器。
有两种方法可以在 Racket 中编写递归函数。一种是定义一个函数,并使用其名称进行递归调用——使用 Racket 的形式规则,我们可以看到,我们说过我们标记我们现在知道一个变量绑定到一个值。这本质上是一种副作用——我们修改了我们所知道的东西,这对应于修改全局环境。第二种方法是一种新形式:letrec。这个形式类似于let,只是建立的范围包括命名表达式——这正是我们希望rec能做的。第三种方法是使用递归局部定义,但这等价于使用letrec,稍后会详细讨论。
递归:Racket 的letrec
因此,我们希望在我们的语言中添加递归,实际上。我们已经知道 Racket 使得编写递归函数成为可能,这是因为它实现了它的“全局环境”的方式:我们的求值器只能扩展一个环境,而 Racket修改了它的全局环境。这意味着每当一个函数在全局环境中被定义时,生成的闭包将以其作为环境“指针”,但全局环境没有被扩展 — 它保持不变,并且只是用一个额外的绑定进行了修改。
但是,Racket 有另一种更有组织的使用递归的方式:有一种特殊的本地绑定结构,类似于let,但允许函数引用自身。它被称为letrec:
(letrec ([fact (lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))]) (fact 5))
有些人可能还记得创建递归函数的第三种方式:在函数体中使用局部定义。例如,我们曾见过这样的东西:
(define (length list) (define (helper list len) (if (null? list) len (helper (rest list) (+ len 1)))) (helper list 0))
这看起来像是在全局define中发生的相同类型的环境魔术 — 但实际上,Racket 使用letrec定义了内部定义的含义 — 因此上述代码与以下代码完全相同:
(define (length list) (letrec ([helper (lambda (list len) (if (null? list) len (helper (rest list) (+ len 1))))]) (helper list 0)))
对于letrec的作用域规则是,绑定名称的作用域覆盖了整个表达式和命名表达式。此外,可以将多个名称绑定到多个表达式,并且每个名称的作用域覆盖所有命名表达式以及主体。这使得定义相互递归函数变得容易,例如:
(letrec ([even? (lambda (n) (if (zero? n) #t (odd? (- n 1))))] [odd? (lambda (n) (if (zero? n) #f (even? (- n 1))))]) (even? 99))
但这不是必需的功能 — 它可以通过包含多个函数的单个递归绑定来完成:
(letrec ([even+odd (list (lambda (n) (if (zero? n) #t ((second even+odd) (- n 1)))) (lambda (n) (if (zero? n) #f ((first even+odd) (- n 1)))))]) ((first even+odd) 99))
这基本上是我们在想要为相互递归绑定使用 Y 组合子时所面临的相同问题。上述解决方案不太方便,但可以通过更多的let来改进,以便更轻松地访问名称。例如:
(letrec ([even+odd (list (lambda (n) (let ([even? (first even+odd)] [odd? (second even+odd)]) (if (zero? n) #t (odd? (- n 1))))) (lambda (n) (let ([even? (first even+odd)] [odd? (second even+odd)]) (if (zero? n) #f (even? (- n 1))))))]) (let ([even? (first even+odd)] [odd? (second even+odd)]) (even? 99)))
使用letrec实现递归 Tuesday,February 21st
我们将看到如何在我们的语言中添加一个类似的结构——为了简单起见,我们将添加一个处理单个绑定的rec形式:
{rec {fact {fun {n} {if {= 0 n} 1 {* n {fact {- n 1}}}}}} {fact 5}}
使用这个,事情可能会变得有点棘手。如果我们执行以下操作,我们应该得到什么:
{rec {x x} x}
? 目前,似乎除了函数表达式之外,使用任何表达式都没有意义,在rec表达式中,所以我们只会处理这些情况。
(顺便说一句,在什么情况下非函数值在letrec中会有用呢?)
实现这一点的一种方法是使用我们最近见过的相同技巧:不要重新实现语言功能,而是可以使用我们自己语言中的现有功能,希望它具有正确的功能形式,以便在我们的评估器中重复使用。
之前,我们已经看到了使用 Racket 闭包实现环境的方法:
;; Define a type for functional environments(define-type ENV = Symbol -> VAL)(: EmptyEnv : -> ENV)(define (EmptyEnv) (lambda (id) (error 'lookup "no binding for ~s" id)))(: lookup : Symbol ENV -> VAL)(define (lookup name env) (env name))(: Extend : Symbol VAL ENV -> ENV)(define (Extend id val rest-env) (lambda (name) (if (eq? name id) val (rest-env name))))
我们可以使用这个实现,并使用 Racket 的letrec创建循环环境。处理with表达式的代码如下:
[(With bound-id named-expr bound-body) (eval bound-body (Extend bound-id (eval named-expr env) env))]
看起来我们应该能以类似的方式处理rec(AST 构造器名称为WRec(“with-rec”),因此它与 TR 的递归类型的Rec构造器不冲突):
[(WRec bound-id named-expr bound-body) (eval bound-body (Extend bound-id (eval named-expr env) env))]
但这不起作用,因为命名表达式在先前的环境中过早地进行了评估。相反,我们将所有需要完成的工作,包括评估,移到一个单独的extend-rec函数中:
[(WRec bound-id named-expr bound-body) (eval bound-body (extend-rec bound-id named-expr env))]
现在,extend-rec 函数需要提供新的“神奇循环”环境。根据我们对extend-rec参数的了解以及它返回一个新环境(=一个查找函数)的事实,我们可以勾勒出一个粗略的定义:
(: extend-rec : Symbol FLANG ENV -> ENV) ; FLANG, not VAL!;; extend an environment with a new binding that is the result of;; evaluating an expression in the same environment as the extended;; result(define (extend-rec id expr rest-env) (lambda (name) (if (eq? name id) ... something that uses expr to get a value ... (rest-env name))))
遗失的表达式应该是什么?它可以简单地评估给定的对象本身:
(define (extend-rec id expr rest-env) (lambda (name) (if (eq? name id) (eval expr ...this environment...) (rest-env name))))
但在定义之前我们如何获得这个环境呢?好吧,环境本身是一个 Racket 函数,所以我们可以使用 Racket 的 letrec 让函数递归地引用自身:
(define (extend-rec id expr rest-env) (letrec ([rec-env (lambda (name) (if (eq? name id) (eval expr rec-env) (rest-env name)))]) rec-env))
使用内部定义会更方便,并且为了清晰起见添加一个类型:
(define (extend-rec id expr rest-env) (: rec-env : Symbol -> VAL) (define (rec-env name) (if (eq? name id) (eval expr rec-env) (rest-env name))) rec-env)
这样做是有效的,但存在几个问题:
-
首先,我们不再在新环境中进行简单的查找。相反,我们在每次查找时都会对表达式进行评估。这似乎是一个技术性的问题,因为在我们的语言中我们没有副作用(也因为我们说我们只想处理函数表达式)。尽管如此,它会浪费空间,因为每次评估都会分配一个新的闭包。
-
其次,一个相关的问题——如果我们尝试运行以下内容会发生什么:
{rec {x x} x}? 好吧,我们做那些事情来扩展当前环境,然后在新环境中评估主体,这个主体是一个单变量引用:
(eval (Id 'x) the-new-env)所以我们查找值:
(lookup 'x the-new-env)即:
(the-new-env 'x)进入实现此环境的函数,我们看到
name与name1相同,所以我们返回:(eval expr rec-env)但这里的
expr是原始的命名表达式,它本身是(Id 'x),我们处于无限循环中。
我们可以尝试使用另一个绑定来解决这些问题。Racket 允许在单个letrec表达式或多个内部函数定义中进行多个绑定,因此我们将extend-rec更改为使用新创建的环境:
(define (extend-rec id expr rest-env) (: rec-env : Symbol -> VAL) (define (rec-env name) (if (eq? name id) val (rest-env name))) (: val : VAL) (define val (eval expr rec-env)) rec-env)
这遇到了一个有趣的类型错误,它抱怨可能获得一些Undefined值。暂时,如果我们切换到无类型语言(使用#lang pl untyped),它确实可以工作 —— 而且似乎也可以正常运行。但它引发了更多问题,首先是:什么是:
(letrec ([x ...] [y ...x...]) ...)
或等效地,一个内部块
(define x ...)(define y ...x...)
?嗯,DrRacket 在这种情况下似乎做了“正确的事情”,但是怎么样:
(letrec ([y ...x...] [x ...]) ...)
?作为提示,看看当我们现在尝试评估有问题的时候会发生什么
{rec {x x} x}
表达式,并将其与您在 Racket 中得到的结果进行比较。这也澄清了我们收到的类型错误。
现在应该清楚为什么我们希望将使用限制在仅绑定递归函数。这种定义没有问题,因为当我们评估fun表达式时,不会评估体,体是唯一可能引用定义的相同函数的地方 —— 函数的体被延迟,仅在稍后应用函数时执行。
但仍然存在一个最大的问题:我们只是使用了 Racket 自己的循环环境实现来实现循环环境,并且这并不能解释它们实际上是如何实现的。我们实现的指针循环依赖于 Racket 使用的指针循环,这是一个我们想要打开的黑盒子。
供参考,完整的代码如下。请注意,由于类型错误而无法正常工作——要尝试它,您需要切换到无类型语言,或者避免额外的val内部绑定,并坚持使用引用时评估的方法。
#lang pl#|The grammar: <FLANG> ::= <num> | { + <FLANG> <FLANG> } | { - <FLANG> <FLANG> } | { * <FLANG> <FLANG> } | { / <FLANG> <FLANG> } | { with { <id> <FLANG> } <FLANG> } | { rec { <id> <FLANG> } <FLANG> } | <id> | { fun { <id> } <FLANG> } | { call <FLANG> <FLANG> }Evaluation rules: eval(N,env) = N eval({+ E1 E2},env) = eval(E1,env) + eval(E2,env) eval({- E1 E2},env) = eval(E1,env) - eval(E2,env) eval({* E1 E2},env) = eval(E1,env) * eval(E2,env) eval({/ E1 E2},env) = eval(E1,env) / eval(E2,env) eval(x,env) = lookup(x,env) eval({with {x E1} E2},env) = eval(E2,extend(x,eval(E1,env),env)) eval({rec {x E1} E2},env) = ??? eval({fun {x} E},env) = <{fun {x} E}, env> eval({call E1 E2},env1) = eval(Ef,extend(x,eval(E2,env1),env2)) if eval(E1,env1) = <{fun {x} Ef}, env2> = error! otherwise|#(define-type FLANG [Num Number] [Add FLANG FLANG] [Sub FLANG FLANG] [Mul FLANG FLANG] [Div FLANG FLANG] [Id Symbol] [With Symbol FLANG FLANG] [WRec Symbol FLANG FLANG] [Fun Symbol FLANG] [Call FLANG FLANG])(: parse-sexpr : Sexpr -> FLANG);; parses s-expressions into FLANGs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'with more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])] [(cons 'rec more) (match sexpr [(list 'rec (list (symbol: name) named) body) (WRec name (parse-sexpr named) (parse-sexpr body))] [else (error 'parse-sexpr "bad `rec' syntax in ~s" sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: name)) body) (Fun name (parse-sexpr body))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [(list 'call fun arg) (Call (parse-sexpr fun) (parse-sexpr arg))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> FLANG);; parses a string containing a FLANG expression to a FLANG AST(define (parse str) (parse-sexpr (string->sexpr str)));; Types for environments, values, and a lookup function(define-type VAL [NumV Number] [FunV Symbol FLANG ENV]);; Define a type for functional environments(define-type ENV = Symbol -> VAL)(: EmptyEnv : -> ENV)(define (EmptyEnv) (lambda (id) (error 'lookup "no binding for ~s" id)))(: lookup : Symbol ENV -> VAL);; lookup a symbol in an environment, return its value or throw an;; error if it isn't bound(define (lookup name env) (env name))(: Extend : Symbol VAL ENV -> ENV);; extend a given environment cache with a new binding(define (Extend id val rest-env) (lambda (name) (if (eq? name id) val (rest-env name))))(: extend-rec : Symbol FLANG ENV -> ENV);; extend an environment with a new binding that is the result of;; evaluating an expression in the same environment as the extended;; result(define (extend-rec id expr rest-env) (: rec-env : Symbol -> VAL) (define (rec-env name) (if (eq? name id) val (rest-env name))) (: val : VAL) (define val (eval expr rec-env)) rec-env)(: NumV->number : VAL -> Number);; convert a FLANG runtime numeric value to a Racket one(define (NumV->number val) (cases val [(NumV n) n] [else (error 'arith-op "expected a number, got: ~s" val)]))(: arith-op : (Number Number -> Number) VAL VAL -> VAL);; gets a Racket numeric binary operator, and uses it within a NumV;; wrapper(define (arith-op op val1 val2) (NumV (op (NumV->number val1) (NumV->number val2))))(: eval : FLANG ENV -> VAL);; evaluates FLANG expressions by reducing them to values(define (eval expr env) (cases expr [(Num n) (NumV n)] [(Add l r) (arith-op + (eval l env) (eval r env))] [(Sub l r) (arith-op - (eval l env) (eval r env))] [(Mul l r) (arith-op * (eval l env) (eval r env))] [(Div l r) (arith-op / (eval l env) (eval r env))] [(With bound-id named-expr bound-body) (eval bound-body (Extend bound-id (eval named-expr env) env))] [(WRec bound-id named-expr bound-body) (eval bound-body (extend-rec bound-id named-expr env))] [(Id name) (lookup name env)] [(Fun bound-id bound-body) (FunV bound-id bound-body env)] [(Call fun-expr arg-expr) (let ([fval (eval fun-expr env)]) (cases fval [(FunV bound-id bound-body f-env) (eval bound-body (Extend bound-id (eval arg-expr env) f-env))] [else (error 'eval "`call' expects a function, got: ~s" fval)]))]))(: run : String -> Number);; evaluate a FLANG program contained in a string(define (run str) (let ([result (eval (parse str) (EmptyEnv))]) (cases result [(NumV n) n] [else (error 'run "evaluation returned a non-number: ~s" result)])));; tests(test (run "{call {fun {x} {+ x 1}} 4}") => 5)(test (run "{with {add3 {fun {x} {+ x 3}}} {call add3 1}}") => 4)(test (run "{with {add3 {fun {x} {+ x 3}}} {with {add1 {fun {x} {+ x 1}}} {with {x 3} {call add1 {call add3 x}}}}}") => 7)(test (run "{with {identity {fun {x} x}} {with {foo {fun {x} {+ x 1}}} {call {call identity foo} 123}}}") => 124)(test (run "{with {x 3} {with {f {fun {y} {+ x y}}} {with {x 5} {call f 4}}}}") => 7)(test (run "{call {with {x 3} {fun {y} {+ x y}}} 4}") => 7)(test (run "{call {call {fun {x} {call x 1}} {fun {x} {fun {y} {+ x y}}}} 123}") => 124)
使用循环结构实现rec星期二,2 月 21 日
PLAI §10
查看环境图中的箭头,我们真正需要的是一个闭包,其环境指针与其定义所在的相同环境相同。这将使得fact可以绑定到一个闭包,该闭包可以引用自身,因为其环境与其定义所在的环境相同。然而,目前我们没有工具可以实现这一点。
我们需要创建一个“指针循环”,但目前我们还没有实现这一点:当我们创建一个闭包时,我们从保存在槽环境中的环境开始,但我们希望该闭包成为同一环境中绑定的值。
方框和突变星期二,2 月 21 日
为了实际实现一个循环结构,我们现在将使用副作用,使用一种支持突变的新型 Racket 值:方框。方框值是用box构造器构建的:
(define my-thing (box 7))
值是通过unbox函数检索的,
(* 6 (unbox my-thing))
最后,值可以通过set-box!函数更改。
(set-box! my-thing 17)(* 6 (unbox my-thing))
需要注意的一件重要事情是,set-box!很像display等,它返回一个值,该值不会在 Racket REPL 中打印出来,因为使用set-box!的结果没有意义,它是因为生成的副作用而调用的。(像 C 这样的语言会模糊返回值和副作用之间的区别,它的赋值语句与此有关。)
作为一个旁注,我们现在有两种副作用:状态的变异,和 I/O(至少是输出部分)。(实际上,还有无限循环,可以看作是另一种形式的副作用。)这意味着我们现在处于一个完全不同的世界,现在很多新事物都变得有意义了。以下是一些你应该知道的事情:
-
我们从未在函数主体中使用多个表达式,因为这没有意义,但现在有了。要评估一系列 Racket 表达式,你将它们包装在一个
begin表达式中。 -
在大多数地方,你实际上并不需要使用
begin——这些地方被称为隐式的begin:函数的主体(或任何 lambda 表达式)、let的主体(以及let的相关内容)、cond、match和cases子句中的结果位置等等。一个常见的使用begin的地方是在if表达式中(当存在多个表达式时,有些人更喜欢使用cond)。 -
在结尾没有
else的cond是有意义的,如果你只是用它来进行副作用的话。 -
if可以得到一个在条件为真时执行的单个表达式(否则使用未指定的值),但我们的语言(以及默认的 Racket 语言)总是禁止这样做——对于一个单边的if,有方便的特殊形式:when和unless,它们可以有任意数量的表达式(它们有一个隐式的begin)。它们有一个明显的优势,即更明确地表示“这里的代码进行了一些副作用”。 -
有一个称为
for-each的函数,它与map完全相同,只是它不收集结果列表,仅用于执行副作用。
当任何一个这些东西被使用时(在 Racket 或其他语言中),你可以确定涉及到了副作用,因为否则它们就没有任何意义。此外,任何以!(“bang”)结尾的名称都用于标记改变状态的函数(通常是仅改变状态的函数)。
那么我们如何创建一个循环呢?简单,方框可以有任何值,并且它们可以放在其他值中,所以我们可以这样做:
#lang pl untyped(define foo (list 1 (box 3)))(set-box! (second foo) foo)
我们得到了一个循环值。(注意它是如何打印的。)并且具有类型:
#lang pl(: foo : (List Number (Boxof Any)))(define foo (list 1 (box 3)))(set-box! (second foo) foo)
Boxes 的类型星期二,2 月 21 日
显然,Any 并不是太好——它是最通用的类型,因此提供的信息最少。例如,注意到
(unbox (second foo))
返回正确的列表,它等于 foo 本身——但是如果我们试图获取结果列表的一部分:
(second (unbox (second foo)))
我们会得到一个类型错误,因为 unbox 的结果是 Any,所以有类型的 Racket 对它一无所知,并且不允许你将其视为列表。也不太奇怪的是,在这种情况下可以帮助的类型构造函数是我们已经见过的 Rec——它允许引用自身的类型:
#lang pl(: foo : (Rec this (List Number (Boxof (U #f this)))))(define foo (list 1 (box #f)))(set-box! (second foo) foo)
注意,foo 或框中的值都以 Rec 类型打印出来——框中的值不能只是具有 (U #f this) 类型,因为 this 在其中没有任何意义,所以整个类型仍然需要存在。
还有一个需要注意的与 Boxof 类型相关的问题。对于大多数类型构造函数(如 Listof),如果 T1 是 T2 的子类型,那么我们也知道 (Listof T1) 是 (Listof T2) 的子类型。这使得以下代码可以通过类型检查:
#lang pl(: foo : (Listof Number) -> Number)(define (foo l) (first l))(: bar : Integer -> Number)(define (bar x) (foo (list x)))
由于 (Listof Integer) 是 foo 的输入的 (Listof Number) 的子类型,这个应用程序可以通过类型检查。但是对于输出类型来说并不是一样的,例如——如果我们将 bar 类型改为:
(: bar : Integer -> Integer)
我们会得到一个类型错误,因为 Number 不是 Integer 的子类型。因此,子类型需要在输入端“向上”传递,在另一端“向下”传递。因此,在某种意义上,框可变的事实意味着它们的内容可以被认为是箭头的另一侧,这就是为什么对于这样的 T1 是 T2 的子类型,(Boxof T2) 是 (Boxof T1) 的子类型,而不是通常的情况。例如,这不起作用:
#lang pl(: foo : (Boxof Number) -> Number)(define (foo b) (unbox b))(: bar : Integer -> Number)(define (bar x) (: b : (Boxof Integer)) (define b (box x)) (foo b))
你可以看到为什么会这样——标记的行对于一个 Number 内容是没问题的,所以如果类型检查器允许传递一个包含整数的框,那么该表达式将会改变内容并使其成为一个无效值。
然而,框不仅是可变的,它们还持有一个可以读取的值,这意味着它们在两侧箭头上,这意味着如果 T2 是 T1 的子类型 且 T1 是 T2 的子类型,那么 (Boxof T1) 是 (Boxof T2) 的子类型——换句话说,只有当 T1 和 T2 是相同的类型时才会发生这种情况。(有关所有这些的扩展演示,请参见下文。)
还要注意,此演示需要额外的 b 定义,如果跳过:
(define (bar x) (foo (box x)))
那么这将再次进行类型检查——有类型的 Racket 只会考虑需要一个包含 Number 的上下文,用 Integer 值初始化这样的框也是可以的。
作为一个旁注,这不总是有效的。在它的存在的早期阶段,有类型的 Racket 总是会为值选择一个特定的类型,这会导致与框有关的错误。例如,上述需要写成
(define (bar x) (foo (box (ann x : Number))))为了防止 Typed Racket 推断出特定类型。这不再是情况,但仍然可能会有一些意外。在持有自引用盒子的列表情况下,需要类似的注释,以避免初始的
#f被赋予一个特定但错误的类型。
Boxof的子类型缺乏星期二,2 月 21 日
不管 S 和 T 如何,(Boxof T) 与 (Boxof S) 之间都没有任何子类型关系,这大致可以解释为以下原因。
首先,一个盒子是一个容器,你可以从中取出一个值 — 这使它类似于列表。 在列表的情况下,我们有:
if: S subtype-of Tthen: (Listof S) subtype-of (Listof T)
对于所有这样的容器,你可以从中取出一个值:如果你期望取出一个 T 但给出的是一个子类型 S 的容器,则事情仍然是好的。 这样的“容器”包括生成值的函数 — 例如:
if: S subtype-of Tthen: Q -> S subtype-of Q -> T
然而,函数也有另一面,其中的情况不同 — 而不是某个 生成 值的一侧,而是 消耗 值的一侧。 我们在那里得到相反的规则:
if: T subtype-of Sthen: S -> Q subtype-of T -> Q
要看到这是正确的,使用 Number 和 Integer 分别作为 S 和 T:
if: Integer subtype-of Numberthen: Number -> Q subtype-of Integer -> Q
所以 — 如果你期望一个接受数字的函数是一个整数的 子类型;换句话说,每一个接受数字的函数也是一个接受整数的函数,但反之则不成立。
总结所有这些,当你使函数的输出类型“更小”(更受限制)时,结果类型就更小,但在输入方面的情况则不同 — 更大的输入类型意味着更受限制的函数。
现在,一个 (Boxof T) 当你从盒子中取出一个值时就是 T 的生产者,但当你把这样一个值放进去时它也是 T 的消费者。 这意味着 — 使用上述类比 — T 在箭头的两侧。 这意味着
if: S subtype-of T *and* T subtype-of Sthen: (Boxof S) subtype-of (Boxof T)
这实际上是:
if: S is-the-same-type-as Tthen: (Boxof S) is-the-same-type-as (Boxof T)
从另一个角度来看这个结论是考虑 (A -> A) 的函数类型是某种其他 (B -> B) 的子类型的情况:只有当 A 是 B 的子类型,而 B 又是 A 的子类型时才会发生,这意味着只有当 A 和 B 是相同类型时才会发生。
(旁注:这与逻辑中的 P => Q 大致等同于 not(P) or Q 有关 — 左侧的 P 在否定之中。 这也解释了为什么在 ((S -> T) -> Q) 中 S 遵守第一条规则,就好像它在右侧 — 因为它被否定了两次。)
以下代码片段更正式地将函数类型的类比关系。 盒子的行为就好像它们的内容同时位于函数箭头的两侧 — 右侧因为它们是可读的,左侧因为它们是可写的,这就得出结论 (Boxof A) 类型是它自身和没有其他 (Boxof B) 的子类型。
#lang pl;; a type for a "read-only" box(define-type (Boxof/R A) = (-> A));; Boxof/R constructor(: box/r : (All (A) A -> (Boxof/R A)))(define (box/r x) (lambda () x));; we can see that (Boxof/R T1) is a subtype of (Boxof/R T2);; if T1 is a subtype of T2 (this is not surprising, since;; these boxes are similar to any other container, like lists):(: foo1 : Integer -> (Boxof/R Integer))(define (foo1 b) (box/r b))(: bar1 : (Boxof/R Number) -> Number)(define (bar1 b) (b))(test (bar1 (foo1 123)) => 123);; a type for a "write-only" box(define-type (Boxof/W A) = (A -> Void));; Boxof/W constructor(: box/w : (All (A) A -> (Boxof/W A)))(define (box/w x) (lambda (new) (set! x new)));; in contrast to the above, (Boxof/W T1) is a subtype of;; (Boxof/W T2) if T2 is a subtype of T1, *not* the other way;; (and note how this is related to A being on the *left* side;; of the arrow in the `Boxof/W' type):(: foo2 : Number -> (Boxof/W Number))(define (foo2 b) (box/w b))(: bar2 : (Boxof/W Integer) Integer -> Void)(define (bar2 b new) (b new))(test (bar2 (foo2 123) 456));; combining the above two into a type for a "read/write" box(define-type (Boxof/RW A) = (A -> A));; Boxof/RW constructor(: box/rw : (All (A) A -> (Boxof/RW A)))(define (box/rw x) (lambda (new) (let ([old x]) (set! x new) old)));; this combines the above two: `A' appears on both sides of the;; arrow, so (Boxof/RW T1) is a subtype of (Boxof/RW T2) if T1;; is a subtype of T2 (because there's an A on the right) *and*;; if T2 is a subtype of T1 (because there's another A on the;; left) -- and that can happen only when T1 and T2 are the same;; type. So this is a type error:;; (: foo3 : Integer -> (Boxof/RW Integer));; (define (foo3 b) (box/rw b));; (: bar3 : (Boxof/RW Number) Number -> Number);; (define (bar3 b new) (b new));; (test (bar3 (foo3 123) 456) => 123);; ** Expected (Number -> Number), but got (Integer -> Integer);; And this a type error too:;; (: foo3 : Number -> (Boxof/RW Number));; (define (foo3 b) (box/rw b));; (: bar3 : (Boxof/RW Integer) Integer -> Integer);; (define (bar3 b new) (b new));; (test (bar3 (foo3 123) 456) => 123);; ** Expected (Integer -> Integer), but got (Number -> Number);; The two types must be the same for this to work:(: foo3 : Integer -> (Boxof/RW Integer))(define (foo3 b) (box/rw b))(: bar3 : (Boxof/RW Integer) Integer -> Integer)(define (bar3 b new) (b new))(test (bar3 (foo3 123) 456) => 123)
实现循环环境
现在我们使用这个来实现以下方式的rec:
-
更改环境,使其不再保存值,而是保存值的盒子:
(Boxof VAL)代替VAL,并且每当使用lookup时,结果的盒子值会被解封, -
在
WRec情况下,为标识符创建新环境,其中包含一些临时绑定 — 任何值都可以,因为它不应该被使用(当命名表达式总是fun表达式时), -
在新环境中评估表达式,
-
更改标识符(盒子)的绑定为此评估的结果。
结果定义为:
(: extend-rec : Symbol FLANG ENV -> ENV);; extend an environment with a new binding that is the result of;; evaluating an expression in the same environment as the extended;; result(define (extend-rec id expr rest-env) (let ([new-cell (box (NumV 42))]) (let ([new-env (Extend id new-cell rest-env)]) (let ([value (eval expr new-env)]) (set-box! new-cell value) new-env))))
Racket 还有另一个相对于多层嵌套let的情况的let — let*。这个形式是一个派生形式 — 它被定义为使用嵌套的let的简写。因此,上面的代码与以下代码完全相同:
(: extend-rec : Symbol FLANG ENV -> ENV);; extend an environment with a new binding that is the result of;; evaluating an expression in the same environment as the extended;; result(define (extend-rec id expr rest-env) (let* ([new-cell (box (NumV 42))] [new-env (Extend id new-cell rest-env)] [value (eval expr new-env)]) (set-box! new-cell value) new-env))
这个let*形式几乎可以被看作是一种类似 C/Java 的代码:
fun extend_rec(id, expr, rest_env) { new_cell = new NumV(42); new_env = Extend(id, new_cell, rest_env); value = eval(expr, new_env); *new_cell = value; return new_env;}
如果我们将评估合并到set-box!中(因为value只在那里使用),并且如果使用lookup来进行突变 — 因为这样就不需要保留盒子。这会稍微昂贵一些,但由于绑定保证是环境中的第一个绑定,所以添加只是一个快速步骤。我们唯一需要的绑定是新环境的绑定,我们可以将其作为内部定义来完成,留下:
(: extend-rec : Symbol FLANG ENV -> ENV)(define (extend-rec id expr rest-env) (define new-env (Extend id (box (NumV 42)) rest-env)) (set-box! (lookup id new-env) (eval expr new-env)) new-env)
一个完全重新修改的 FLANG 版本,带有一个rec绑定如下:
▶#lang pl(define-type FLANG [Num Number] [Add FLANG FLANG] [Sub FLANG FLANG] [Mul FLANG FLANG] [Div FLANG FLANG] [Id Symbol] [With Symbol FLANG FLANG] [WRec Symbol FLANG FLANG] [Fun Symbol FLANG] [Call FLANG FLANG])(: parse-sexpr : Sexpr -> FLANG);; parses s-expressions into FLANGs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons (or 'with 'rec) more) (match sexpr [(list 'with (list (symbol: name) named) body) (With name (parse-sexpr named) (parse-sexpr body))] [(list 'rec (list (symbol: name) named) body) (WRec name (parse-sexpr named) (parse-sexpr body))] [(cons x more) (error 'parse-sexpr "bad `~s' syntax in ~s" x sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: name)) body) (Fun name (parse-sexpr body))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))] [(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))] [(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))] [(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))] [(list 'call fun arg) (Call (parse-sexpr fun) (parse-sexpr arg))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> FLANG);; parses a string containing a FLANG expression to a FLANG AST(define (parse str) (parse-sexpr (string->sexpr str)));; Types for environments, values, and a lookup function(define-type ENV [EmptyEnv] [Extend Symbol (Boxof VAL) ENV])(define-type VAL [NumV Number] [FunV Symbol FLANG ENV])(: lookup : Symbol ENV -> (Boxof VAL));; lookup a symbol in an environment, return its value or throw an;; error if it isn't bound(define (lookup name env) (cases env [(EmptyEnv) (error 'lookup "no binding for ~s" name)] [(Extend id boxed-val rest-env) (if (eq? id name) boxed-val (lookup name rest-env))]))(: extend-rec : Symbol FLANG ENV -> ENV);; extend an environment with a new binding that is the result of;; evaluating an expression in the same environment as the extended;; result(define (extend-rec id expr rest-env) (define new-env (Extend id (box (NumV 42)) rest-env)) (set-box! (lookup id new-env) (eval expr new-env)) new-env)(: NumV->number : VAL -> Number);; convert a FLANG runtime numeric value to a Racket one(define (NumV->number val) (cases val [(NumV n) n] [else (error 'arith-op "expected a number, got: ~s" val)]))(: arith-op : (Number Number -> Number) VAL VAL -> VAL);; gets a Racket numeric binary operator, and uses it within a NumV;; wrapper(define (arith-op op val1 val2) (NumV (op (NumV->number val1) (NumV->number val2))))(: eval : FLANG ENV -> VAL);; evaluates FLANG expressions by reducing them to values(define (eval expr env) (cases expr [(Num n) (NumV n)] [(Add l r) (arith-op + (eval l env) (eval r env))] [(Sub l r) (arith-op - (eval l env) (eval r env))] [(Mul l r) (arith-op * (eval l env) (eval r env))] [(Div l r) (arith-op / (eval l env) (eval r env))] [(With bound-id named-expr bound-body) (eval bound-body (Extend bound-id (box (eval named-expr env)) env))] [(WRec bound-id named-expr bound-body) (eval bound-body (extend-rec bound-id named-expr env))] [(Id name) (unbox (lookup name env))] [(Fun bound-id bound-body) (FunV bound-id bound-body env)] [(Call fun-expr arg-expr) (let ([fval (eval fun-expr env)]) (cases fval [(FunV bound-id bound-body f-env) (eval bound-body (Extend bound-id (box (eval arg-expr env)) f-env))] [else (error 'eval "`call' expects a function, got: ~s" fval)]))]))(: run : String -> Number);; evaluate a FLANG program contained in a string(define (run str) (let ([result (eval (parse str) (EmptyEnv))]) (cases result [(NumV n) n] [else (error 'run "evaluation returned a non-number: ~s" result)])));; tests(test (run "{call {fun {x} {+ x 1}} 4}") => 5)(test (run "{with {add3 {fun {x} {+ x 3}}} {call add3 1}}") => 4)(test (run "{with {add3 {fun {x} {+ x 3}}} {with {add1 {fun {x} {+ x 1}}} {with {x 3} {call add1 {call add3 x}}}}}") => 7)(test (run "{with {identity {fun {x} x}} {with {foo {fun {x} {+ x 1}}} {call {call identity foo} 123}}}") => 124)(test (run "{with {x 3} {with {f {fun {y} {+ x y}}} {with {x 5} {call f 4}}}}") => 7)(test (run "{call {with {x 3} {fun {y} {+ x y}}} 4}") => 7)(test (run "{call {call {fun {x} {call x 1}} {fun {x} {fun {y} {+ x y}}}} 123}") => 124)
变量突变星期二,2 月 28 日
PLAI §12 和 PLAI §13(不同之处:向语言添加盒��)
PLAI §14(这就是我们所做的)
我们现在的代码通过改变绑定来实现递归,为了使这成为可能,我们让环境为所有绑定保存盒子,因此现在所有的绑定都是可变的。我们可以利用这一点为我们的求值器添加更多功能,通过允许更改任何变量 — 我们可以添加一个set!形式:
{set! <id> <FLANG>}
给评估器,它将修改变量的值。要实现这个功能,我们只需要使用lookup来检索一些盒子,然后评估表达式并将结果放入该盒子中。实际的实现留作家庭作业。
这里需要考虑的一件事是 — 我们语言中的所有表达式都会求值为某个值,问题是set!表达式的值应该是什么?有三个明显的选择:
-
返回一些虚假值,
-
返回被分配的值,
-
返回之前在盒子中的值。
这些中的每一个都有自己的优势 — 例如,C 使用第二个选项来chain赋值(例如,x = y = 0)并允许副作用在期望表达式的地方发生(例如,while (x = x-1) ...)。
第三种情况在你可能会使用被覆盖的旧值的情况下很有用 — 例如,如果 C 有这种行为,你可以使用类似以下方式从链表中pop一个值:
first(l = rest(l));
因为first的参数将是l的旧值,在它变为其rest之前。你也可以在单个表达式中交换两个变量:x = y = x。
(请注意,当使用选项(2)时,表达式x = x + 1的含义等同于 C 的++x,而当使用选项(3)时,则等同于x++。)
Racket 选择第一个选项,在我们的语言中我们也会这样做。这里的优势是你不会得到折扣,因此在没有明显选择的情况下,你必须明确指出你想要返回的值。这会导致更健壮的程序,因为你不会有其他程序员依赖于你没有计划的代码特性。
无论如何,引入突变的修改很小,但对我们的语言有巨大影响:对 Racket 是真的,对 FLANG 也是真的。我们已经看到突变如何影响我们使用的语言子集,在我们的 FLANG 扩展中,这种影响甚至更强烈:因为任何变量都可以改变(不需要显式的box值)。换句话说,一个绑定并不总是相同的 — 它可以因为set!表达式的结果而改变。当然,我们可以使用盒子扩展我们的语言(使用 Racket 盒子来实现 FLANG 盒子),但那会更加冗长。
请注意,Racket 确实有一个
set!形式,此外,结构体中的字段可以被修改。然而,我们目前并没有使用这些。至少目前还没有。
状态和环境星期二,2 月 28 日
如何使用突变的一个快速示例:
(define counter (let ([counter (box 0)]) (lambda () (set-box! counter (+ 1 (unbox counter))) (unbox counter))))
并将其与之比较:
(define (make-counter) (let ([counter (box 0)]) (lambda () (set-box! counter (+ 1 (unbox counter))) (unbox counter))))
如果您遵循确切的评估
(define foo (make-counter))(define bar (make-counter))
并且看看两个绑定是如何拥有各自独立的环境,因此每个都有自己的私有状态。使用set!扩展的作业解释器中的等效代码不需要盒子:
{with {make-counter {fun {} {with {counter 0} {fun {} {set! counter {+ counter 1}} counter}}}} {with {foo {call make-counter}} {with {bar {call make-counter}} ...}}}
(要从单个表达式中看到多个值,可以通过list绑定扩展语言。)注意我们不能用替换规则来描述这种行为!现在我们使用环境来使改变绑定成为可能——所以最终一个环境实际上是一个环境而不是替换缓存。
当您查看上述内容时,请注意我们仍然使用词法作用域——实际上,局部绑定实际上是一个没有人可以访问的私有状态。例如,如果我们写道:
(define counter (let ([counter (box 0)]) (lambda () (set-box! counter (+ 1 (unbox counter))) (if (zero? (modulo (unbox counter) 4)) 'tock 'tick))))
然后绑定到counter的结果函数将保持一个局部整数状态,其他代码无法访问——您无法修改它,重置它,甚至知道其中是否真的是一个整数。
使用状态实现对象
我们已经看到了如何将多个信息封装在一个 Racket 闭包中并保留它们所有;现在我们可以做更多 —— 我们实际上可以拥有可变状态,这导致了一种实现对象的自然方式。例如:
(define (make-point x y) (let ([xb (box x)] [yb (box y)]) (lambda (msg) (match msg ['getx (unbox xb)] ['gety (unbox yb)] ['incx (set-box! xb (add1 (unbox xb)))]))))
实现了一个构造函数用于创建 point 对象,这些对象保持两个值并可以移动其中一个。请注意,这些消息充当一种方法形式,并且值本身是隐藏的,只能通过这些消息生成的接口访问。例如,如果这些点对应于屏幕上的某个图形对象,我们可以轻松地合并必要的屏幕更新:
(define (make-point x y) (let ([xb (box x)] [yb (box y)]) (lambda (msg) (match msg ['getx (unbox xb)] ['gety (unbox yb)] ['incx (set-box! xb (add1 (unbox xb))) (update-screen)]))))
并确保当值发生变化时始终执行此操作 —— 因为除了通过此接口外没有改变值的方法。
更完整的示例将定义实际发送这些消息的函数 —— 这是一个更好的点对象实现以及相应的访问器和修改器的实现:
(define (make-point x y) (let ([xb (box x)] [yb (box y)]) (lambda (msg) (match msg ['getx (unbox xb)] ['gety (unbox yb)] [(list 'setx newx) (set-box! xb newx) (update-screen)] [(list 'sety newy) (set-box! yb newy) (update-screen)]))))(define (point-x p) (p 'getx))(define (point-y p) (p 'gety))(define (set-point-x! p x) (p (list 'setx x)))(define (set-point-y! p y) (p (list 'sety y)))
快速模拟继承可以通过委托给超类的实例来实现:
(define (make-colored-point x y color) (let ([p (make-point x y)]) (lambda (msg) (match msg ['getcolor color] [else (p msg)]))))
您可以看到所有这些都可以来自更常见的类定义形式的某种预处理,例如:
(defclass point (x y) (public (getx) x) (public (gety) y) (public (setx new) (set! x newx)) (public (setx new) (set! x newx)))(defclass colored-point point (c) (public (getcolor) c))
玩具语言星期二,2 月 28 日
不在 PLAI 中
一个快速的提示:从现在开始,我们将使用我们语言的一个变体 —— 它将改变语法,看起来有点像 Racket,并且我们将使用 Racket 值来表示我们语言中的值,使用 Racket 函数来表示我们语言中的内置函数。
主要亮点:
-
函数参数和局部
bind表单中可以有多个绑定 —— 名称必须是不同的。 -
现在有一些关键字像
bind这样的关键字以特殊的方式解析。其他形式被视为函数应用,这意味着没有特殊的解析规则(和 AST 条目)用于算术函数。它们现在是全局环境中的绑定,并且与所有绑定一样处理。例如,*是一个表达式,它求值为原始乘法函数,{bind {{+ *}} {+ 2 3}}求值为6。 -
由于原始函数和用户绑定函数现在的函数应用是相同的,所以不再需要
call关键字。请注意,解析器的函数调用部分必须是最后的,因为它只应用于输入不是其他已知形式的情况。 -
注意使用
make-untyped-list-function:它是一个库函数(包含在课程语言中),可以将一些已知的 Racket 函数转换为一个函数,该函数消耗一个任何 Racket 值的列表,并返回将给定的 Racket 函数应用于这些值的结果。例如:(define add (make-untyped-list-function +))(add (list 1 2 3 4))求值为
10。 -
这个的另一个重要方面是它的类型 —— 在前面的例子中,
add的类型是(List -> Any),因此生成的函数可以消耗任何输入值。如果它得到一个坏值,它将抛出一个适当的错误。这是一个技巧:它基本上意味着生成的add函数具有非常通用的类型(只需要一个列表),因此错误可以在运行时抛出。然而,在这种情况下,一个更好的解决方案不会让这些运行时错误消失,因为我们正在实现的语言不是静态类型的。 -
这样做的好处是我们可以通过让这些函数动态检查输入值来避免更冗长的代码的麻烦,因此我们可以在
VAL中使用一个RktV变体来包装任何 Racket 值。(否则我们需要为不同类型需要不同的包装器,并实现这些动态检查。)
以下是完整的实现。
▶#lang pl;;; ----------------------------------------------------------------;;; Syntax#| The BNF: <TOY> ::= <num> | <id> | { bind {{ <id> <TOY> } ... } <TOY> } | { fun { <id> ... } <TOY> } | { if <TOY> <TOY> <TOY> } | { <TOY> <TOY> ... }|#;; A matching abstract syntax tree datatype:(define-type TOY [Num Number] [Id Symbol] [Bind (Listof Symbol) (Listof TOY) TOY] [Fun (Listof Symbol) TOY] [Call TOY (Listof TOY)] [If TOY TOY TOY])(: unique-list? : (Listof Any) -> Boolean);; Tests whether a list is unique, guards Bind and Fun values.(define (unique-list? xs) (or (null? xs) (and (not (member (first xs) (rest xs))) (unique-list? (rest xs)))))(: parse-sexpr : Sexpr -> TOY);; parses s-expressions into TOYs(define (parse-sexpr sexpr) (match sexpr [(number: n) (Num n)] [(symbol: name) (Id name)] [(cons 'bind more) (match sexpr [(list 'bind (list (list (symbol: names) (sexpr: nameds)) ...) body) (if (unique-list? names) (Bind names (map parse-sexpr nameds) (parse-sexpr body)) (error 'parse-sexpr "duplicate `bind' names: ~s" names))] [else (error 'parse-sexpr "bad `bind' syntax in ~s" sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: names) ...) body) (if (unique-list? names) (Fun names (parse-sexpr body)) (error 'parse-sexpr "duplicate `fun' names: ~s" names))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(cons 'if more) (match sexpr [(list 'if cond then else) (If (parse-sexpr cond) (parse-sexpr then) (parse-sexpr else))] [else (error 'parse-sexpr "bad `if' syntax in ~s" sexpr)])] [(list fun args ...) ; other lists are applications (Call (parse-sexpr fun) (map parse-sexpr args))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))(: parse : String -> TOY);; Parses a string containing an TOY expression to a TOY AST.(define (parse str) (parse-sexpr (string->sexpr str)));;; ----------------------------------------------------------------;;; Values and environments(define-type ENV [EmptyEnv] [FrameEnv FRAME ENV]);; a frame is an association list of names and values.(define-type FRAME = (Listof (List Symbol VAL)))(define-type VAL [RktV Any] [FunV (Listof Symbol) TOY ENV] [PrimV ((Listof VAL) -> VAL)])(: extend : (Listof Symbol) (Listof VAL) ENV -> ENV);; extends an environment with a new frame.(define (extend names values env) (if (= (length names) (length values)) (FrameEnv (map (lambda ([name : Symbol] [val : VAL]) (list name val)) names values) env) (error 'extend "arity mismatch for names: ~s" names)))(: lookup : Symbol ENV -> VAL);; lookup a symbol in an environment, frame by frame,;; return its value or throw an error if it isn't bound(define (lookup name env) (cases env [(EmptyEnv) (error 'lookup "no binding for ~s" name)] [(FrameEnv frame rest) (let ([cell (assq name frame)]) (if cell (second cell) (lookup name rest)))]))(: unwrap-rktv : VAL -> Any);; helper for `racket-func->prim-val': unwrap a RktV wrapper in;; preparation to be sent to the primitive function(define (unwrap-rktv x) (cases x [(RktV v) v] [else (error 'racket-func "bad input: ~s" x)]))(: racket-func->prim-val : Function -> VAL);; converts a racket function to a primitive evaluator function;; which is a PrimV holding a ((Listof VAL) -> VAL) function.;; (the resulting function will use the list function as is,;; and it is the list function's responsibility to throw an error;; if it's given a bad number of arguments or bad input types.)(define (racket-func->prim-val racket-func) (define list-func (make-untyped-list-function racket-func)) (PrimV (lambda (args) (RktV (list-func (map unwrap-rktv args))))));; The global environment has a few primitives:(: global-environment : ENV)(define global-environment (FrameEnv (list (list '+ (racket-func->prim-val +)) (list '- (racket-func->prim-val -)) (list '* (racket-func->prim-val *)) (list '/ (racket-func->prim-val /)) (list '< (racket-func->prim-val <)) (list '> (racket-func->prim-val >)) (list '= (racket-func->prim-val =)) ;; values (list 'true (RktV #t)) (list 'false (RktV #f))) (EmptyEnv)));;; ----------------------------------------------------------------;;; Evaluation(: eval : TOY ENV -> VAL);; evaluates TOY expressions.(define (eval expr env) ;; convenient helper (: eval* : TOY -> VAL) (define (eval* expr) (eval expr env)) (cases expr [(Num n) (RktV n)] [(Id name) (lookup name env)] [(Bind names exprs bound-body) (eval bound-body (extend names (map eval* exprs) env))] [(Fun names bound-body) (FunV names bound-body env)] [(Call fun-expr arg-exprs) (let ([fval (eval* fun-expr)] [arg-vals (map eval* arg-exprs)]) (cases fval [(PrimV proc) (proc arg-vals)] [(FunV names body fun-env) (eval body (extend names arg-vals fun-env))] [else (error 'eval "function call with a non-function: ~s" fval)]))] [(If cond-expr then-expr else-expr) (eval* (if (cases (eval* cond-expr) [(RktV v) v] ; Racket value => use as boolean [else #t]) ; other values are always true then-expr else-expr))]))(: run : String -> Any);; evaluate a TOY program contained in a string(define (run str) (let ([result (eval (parse str) global-environment)]) (cases result [(RktV v) v] [else (error 'run "evaluation returned a bad value: ~s" result)])));;; ----------------------------------------------------------------;;; Tests(test (run "{{fun {x} {+ x 1}} 4}") => 5)(test (run "{bind {{add3 {fun {x} {+ x 3}}}} {add3 1}}") => 4)(test (run "{bind {{add3 {fun {x} {+ x 3}}} {add1 {fun {x} {+ x 1}}}} {bind {{x 3}} {add1 {add3 x}}}}") => 7)(test (run "{bind {{identity {fun {x} x}} {foo {fun {x} {+ x 1}}}} {{identity foo} 123}}") => 124)(test (run "{bind {{x 3}} {bind {{f {fun {y} {+ x y}}}} {bind {{x 5}} {f 4}}}}") => 7)(test (run "{{{fun {x} {x 1}} {fun {x} {fun {y} {+ x y}}}} 123}") => 124);; More tests for complete coverage(test (run "{bind x 5 x}") =error> "bad `bind' syntax")(test (run "{fun x x}") =error> "bad `fun' syntax")(test (run "{if x}") =error> "bad `if' syntax")(test (run "{}") =error> "bad syntax")(test (run "{bind {{x 5} {x 5}} x}") =error> "duplicate*bind*names")(test (run "{fun {x x} x}") =error> "duplicate*fun*names")(test (run "{+ x 1}") =error> "no binding for")(test (run "{+ 1 {fun {x} x}}") =error> "bad input")(test (run "{+ 1 {fun {x} x}}") =error> "bad input")(test (run "{1 2}") =error> "with a non-function")(test (run "{{fun {x} x}}") =error> "arity mismatch")(test (run "{if {< 4 5} 6 7}") => 6)(test (run "{if {< 5 4} 6 7}") => 7)(test (run "{if + 6 7}") => 6)(test (run "{fun {x} x}") =error> "returned a bad value");;; ----------------------------------------------------------------
编译和部分求值星期二,2 月 28 日
与其解释一个表达式,即执行完整的评估,我们可以考虑 编译 它:将其转换为另一种我们可以更容易、更高效地运行的语言,更多地在更多平台上等等。通常与编译相关的另一个特性是在编译阶段做了更多的工作,使得实际运行代码更快。
例如,将 AST 转换为具有 de-Bruijn 索引而不是标识符名称的 AST 是一种编译形式——它不仅将一种语言翻译成另一种语言,还在程序开始运行之前执行了名称查找所涉及的工作。
现在我们可以尝试一下这个。实现这一点的简单方法是从我们的评估函数开始:
(: eval : TOY ENV -> VAL);; evaluates TOY expressions.(define (eval expr env) ;; convenient helper (: eval* : TOY -> VAL) (define (eval* expr) (eval expr env)) (cases expr [(Num n) (RktV n)] [(Id name) (lookup name env)] [(Bind names exprs bound-body) (eval bound-body (extend names (map eval* exprs) env))] [(Fun names bound-body) (FunV names bound-body env)] [(Call fun-expr arg-exprs) (let ([fval (eval* fun-expr)] [arg-vals (map eval* arg-exprs)]) (cases fval [(PrimV proc) (proc arg-vals)] [(FunV names body fun-env) (eval body (extend names arg-vals fun-env))] [else (error 'eval "function call with a non-function: ~s" fval)]))] [(If cond-expr then-expr else-expr) (eval* (if (cases (eval* cond-expr) [(RktV v) v] ; Racket value => use as boolean [else #t]) ; other values are always true then-expr else-expr))]))
并将其更改为将给定表达式编译为 Racket 函数。(当然,这只是为了演示一个概念上的点,它只是编译器实际执行的一部分……)这意味着我们需要将其转换为一个接收 TOY 表达式并将其编译的函数。换句话说,eval 不再消耗环境参数,这是有道理的,因为环境是保存运行时值的地方,因此它是编译器的一部分,不是编译器的一部分(通常表示为调用堆栈)。
因此,我们将两个参数分成了编译时和运行时,只需简单地对 eval 函数进行柯里化即可——在这里已经完成了此操作,并且所有对 eval 的调用也已经被柯里化:
(: eval : TOY -> ENV -> VAL) ;*** note the curried type;; evaluates TOY expressions.(define (eval expr) (lambda (env) ;; convenient helper (: eval* : TOY -> VAL) (define (eval* expr) ((eval expr) env)) (cases expr [(Num n) (RktV n)] [(Id name) (lookup name env)] [(Bind names exprs bound-body) ((eval bound-body) (extend names (map eval* exprs) env))] [(Fun names bound-body) (FunV names bound-body env)] [(Call fun-expr arg-exprs) (let ([fval (eval* fun-expr)] [arg-vals (map eval* arg-exprs)]) (cases fval [(PrimV proc) (proc arg-vals)] [(FunV names body fun-env) ((eval body) (extend names arg-vals fun-env))] [else (error 'eval "function call with a non-function: ~s" fval)]))] [(If cond-expr then-expr else-expr) (eval* (if (cases (eval* cond-expr) [(RktV v) v] ; Racket value => use as boolean [else #t]) ; other values are always true then-expr else-expr))])))
我们还需要更改主要的 run 函数中的 eval 调用:
(: run : String -> Any);; evaluate a TOY program contained in a string(define (run str) (let ([result ((eval (parse str)) global-environment)]) (cases result [(RktV v) v] [else (error 'run "evaluation returned a bad value: ~s" result)])))
到目前为止,几乎没有太大变化。
注意,在编译器的一般情况下,我们需要运行程序多次,所以我们希望避免一遍又一遍地解析它。我们可以通过保持输入的单个解析的 AST 来实现这一点。现在我们更进一步,使得可以提前做更多的工作并保留第一阶段的评估结果(尽管“更多工作”目前真的不是什么了不起的事):
(: run : String -> Any);; evaluate a TOY program contained in a string(define (run str) (let* ([compiled (eval (parse str))] [result (compiled global-environment)]) (cases result [(RktV v) v] [else (error 'run "evaluation returned a bad value: ~s" result)])))
在这一点上,即使我们的“编译器”不过是同一功能稍微不同的表示形式,我们将 eval 重命名为 compile,这是对我们打算做的更合适的描述(所以我们也更改了目的说明):
(: compile : TOY -> ENV -> VAL);; compiles TOY expressions to Racket functions.(define (compile expr) (lambda (env) (: compile* : TOY -> VAL) (define (compile* expr) ((compile expr) env)) (cases expr [(Num n) (RktV n)] [(Id name) (lookup name env)] [(Bind names exprs bound-body) ((compile bound-body) (extend names (map compile* exprs) env))] [(Fun names bound-body) (FunV names bound-body env)] [(Call fun-expr arg-exprs) (let ([fval (compile* fun-expr)] [arg-vals (map compile* arg-exprs)]) (cases fval [(PrimV proc) (proc arg-vals)] [(FunV names body fun-env) ((compile body) (extend names arg-vals fun-env))] [else (error 'call ; this is *not* a compilation error "function call with a non-function: ~s" fval)]))] [(If cond-expr then-expr else-expr) (compile* (if (cases (compile* cond-expr) [(RktV v) v] ; Racket value => use as boolean [else #t]) ; other values are always true then-expr else-expr))])))(: run : String -> Any);; evaluate a TOY program contained in a string(define (run str) (let* ([compiled (compile (parse str))] [result (compiled global-environment)]) (cases result [(RktV v) v] [else (error 'run "evaluation returned a bad value: ~s" result)])))
没有太多变化,我们将 eval 函数改为柯里化,并将其重命名为 compile。但是当我们实际调用编译时,几乎什么都不会发生——它所做的就是创建一个 Racket 闭包,它将完成其余的工作。(这个闭包会固定给定的表达式。)
运行这个“编译”代码会非常类似于之前使用 eval 的用法,只是稍微 *慢 *一点,因为现在每个递归调用都涉及调用 compile 来生成一个闭包,然后立即使用它——所以我们只是在递归调用点添加了一些分配!(实际上,额外的成本很小,因为 Racket 编译器将优化掉这种立即闭包应用。)
说明这不真的是一个编译器的另一种方法是考虑何时调用 compile。一个正确的编译器是在运行代码之前完成所有工作的东西,这意味着一旦它吐出编译后的代码,它就不应该再次使用(除了编译其他代码,当然)。我们当前的代码并不真的是一个编译器,因为它打破了这个特性。(例如,如果 GCC 以这种方式行为,那么它将通过产生调用 GCC 来编译下一步的代码的代码来“编译”文件,然后在运行时再次调用 GCC,依此类推。)
然而,概念上的变化是重大的——我们现在有一个分两个阶段完成工作的函数——第一部分获取一个表达式并且可以在编译时进行一些工作,第二部分执行运行时的工作,并且包括(lambda (env) …)中的任何内容。问题是到目前为止,代码在编译阶段什么也没做(记住:只创建了一个闭包)。但是因为我们有两个阶段,我们现在可以将工作从第二阶段(运行时)转移到第一阶段(编译时)。
例如,考虑以下简单示例:
#lang pl(: foo : Number Number -> Number)(define (foo x y) (* x y))(: bar : Number -> Number)(define (bar c) (: loop : Number Number -> Number) (define (loop n acc) (if (< 0 n) (loop (- n 1) (+ (foo c n) acc)) acc)) (loop 40000000 0))(time (bar 0))
我们可以在这里做同样的事情——使用柯里化将 foo 分成两个阶段,并适当修改 bar:
#lang pl(: foo : Number -> Number -> Number)(define (foo x) (lambda (y) (* x y)))(: bar : Number -> Number)(define (bar c) (: loop : Number Number -> Number) (define (loop n acc) (if (< 0 n) (loop (- n 1) (+ ((foo c) n) acc)) acc)) (loop 40000000 0))(time (bar 0))
现在,不再简单地进行乘法运算,让我们稍微扩展一下,例如,对 x 为 0、1 或 2 的常见情况进行案例分析:
(: foo : Number -> Number -> Number)(define (foo x) (lambda (y) (cond [(= x 0) 0] [(= x 1) y] [(= x 2) (+ y y)] ; assume that this is faster [else (* x y)])))
这并没有快多少,因为 Racket 已经以类似的方式优化了乘法。
现在真正的魔法来了:决定采取 cond 的哪个分支仅取决于 x,因此我们可以将 lambda 推进:
(: foo : Number -> Number -> Number)(define (foo x) (cond [(= x 0) (lambda (y) 0)] [(= x 1) (lambda (y) y)] [(= x 2) (lambda (y) (+ y y))] [else (lambda (y) (* x y))]))
我们刚刚做了一点改进——对于常见情况的比较现在在调用 (foo x) 时立即进行,它们不会延迟到使用结果函数时。现在回到在 bar 中使用的方式,并让它根据给定的 c 调用一次 foo:
#lang pl(: foo : Number -> Number -> Number)(define (foo x) (cond [(= x 0) (lambda (y) 0)] [(= x 1) (lambda (y) y)] [(= x 2) (lambda (y) (+ y y))] [else (lambda (y) (* x y))]))(: bar : Number -> Number)(define (bar c) (define foo-c (foo c)) (: loop : Number Number -> Number) (define (loop n acc) (if (< 0 n) (loop (- n 1) (+ (fooc n) acc)) acc)) (loop 40000000 0))(time (bar 0))
现在 foo-c 只生成一次,如果 c 恰好是三种常见情况之一(如最后一个表达式中所示),我们可以避免进行任何乘法运算。(如果我们遇到默认情况,则与之前执行的操作相同。)
[然而,结果运行速度稍慢!原因是当编译器无法“简化闭包”时处理函数会产生更高的成本——这正是最后一个版本中发生的情况。额外的开销远高于我们节省的乘法(Racket 编译器内联了乘法,因此它们的成本接近于执行单个机器码指令)。]
下面是另一个有用的例子,演示了这一点:
(define (foo list) (map (lambda (n) (if ...something... E1 E2)) list))-->(define (foo list) (map (if ...something... (lambda (n) E1) (lambda (n) E2)) list))
(问题:你什么时候能做到这一点?)
这不是 Racket 特有的,它可以发生在任何语言中。Racket(或任何具有一级函数值的语言)只是使得创建一个专门用于标志的本地函数变得容易。


浙公网安备 33010602011771号