Coursera Programming Languages, Part B 华盛顿大学 Week 1

来上 programming language 的第二 part 了!这一部分介绍的语言是 Racket,之前就听说过它独特的括号语法,这次来具体了解一下

Racket definitions, functions and conditionals

  • definition
(define x 3)
(define y (+ x 3))    ; 在 racket 中,+ 是一个函数,后面接着函数的两个参数
  • functions
(define cube1
  lambda (x) (* x x x))   ; lambda 关键字类似于 ML 中的 fn, 创建一个匿名函数,格式为 lambda(args) (function body)
(define (cube2 x)
  (* x x x))              ; cube1 的 syntactic sugar 写法
(define (pow1 x y)
  (if (= y 0) 
      1
      (* x (pow1 x (- y 1)))))
  • conditionals
    racket 中的条件语句格式为 (if e1 e2 e3)
    翻译为 C 即 if (e1) then e2 else e3

Racket Lists

(list e1 e2 ... en)   ; build a list
null                  ; empty list
cons                  ; connect 2 lists
car                   ; get the head of a list
cdr                   ; get the tail of a list
null?                 ; check whether a list is empty

syntax and parenthetes

Racket 的整个语法结构是建立在括号之上的,而且非常简洁!
Racket 中的一个语素 (term) 有以下几种类型:

  • Atom (不可再分,原子):如 #t,#f,"hi", null (和线性表中的定义一样!)
  • Special form : 如 lambda,define,if
  • A sequence of terms in parens: (t1 t2 ... tn)
    注意,如果 t1 属于 special form 那么整个序列的格式需满足 t1 的要求
    否则序列为一个函数调用 (function call)

Racket 语言其实是一个前序序列,我们可以对一条条语句建立对应的语法树
由于采取前序序列,可以保证每一条语句不会产生歧义 (ambiguity),且不需要讨论优先级 (operator precedence) 问题
在这个语法树中:

  • Atom 是叶子节点
  • Sequence 的头元素是内部结点,而其余的元素代表的结点都在该结点的子树中

Dynamic typing

与 Static typing 不同,在函数实际运行之前不能发现类似 (n * x) 的错误
但是可以定义许多灵活的的数据结构,不会受到类型的束缚,例如,一个 List 的元素可以不是同一类型的
(list (list 4 5) 5 "hi" (list foo "hello") 4)


Cond

cond 语法是多个嵌套 if 语句的语法糖
对于嵌套 if 语句:

(if e1 e2
  (if e3 e4 
    (if e5 e6 e7)))

可以运用语法糖 cond 写成

(cond [e1 e2]
      [e3 e4]
      [e5 e6]
      [#t e7])  ; the 1st element in cond's last bracket should always be #t

值得注意的是,在 Racket 中,除了 #f 之外的所有 term 都被 evaluate 成 #t
所以,if 或者 cond 的条件语句可以是除了 #f#t 以外的 term


local bindings

引入用于定义本地变量的 special form -> let let* letrec
格式如下:

(let ([x1 e1] [x2 e2] ...[xn en]) body)

本地变量在 let 后的第一个括号中进行定义,并且每一对变量-值对都被包在中括号里
与 ML 中我们已熟知的 let 不同,所有在 let 环境中的表达都是在 let 之前的环境中进行 evaluate 的
例如:

(define (silly-double x)
  (let ([x (+ x 3)]
        [y (+ x 3)])
  (+ x y -5)))

若传入的参数 x 为 3,若按照传统的 dynamic type 语言,let 环境中的 x 值应为 6,而 y 则是 9 (因为第二个 x 将传入的参数 x shadow 掉了)
但实际上 x 与 y 都为 6,evaluate 它们表达的环境是 let 之前的环境,也就是 x 为 3 的环境;至于 let 环境中的 x 不参与对 y 的 evaluate
let* 则与 ML 中 let 的 evaluate 原则相同

(define (silly-double x)
  (let* ([x (+ x 3)]
         [y (+ x 3)])
  (+ x y -5)))

这里,let* expression 中的 x 为 6,y 为 9
最后的 letrec 则在定义 mutual recursion 时非常常用 (除了定义 mutual recursion 之外,其他地方最好不用 letrec)
letrec 环境定义新 binding 时,你既可以使用之前的 bindings,也可以使用之后的,但只能在函数体 (body) 中使用之后的 binding 进行定义

(define (silly-triple x)
  (letrec ([y + x 2]
           [f (lambda(z) (+ z y w x))]  ; 在定义函数体中使用了之后的 binding w
           [w (+ x 7)])
  (f -9)))

在这里,w 定义在了使用 w 的函数 f 之后,但是却不会出现问题
这是因为只有我们在调用函数时 ((f -9)),racket 才会 evaluate 函数的 body 部分,而此时 w 已经 evaluate 为 x + 7 了
本质上来讲,racket 仍然是按照次序进行 evaluate 的,所以以下这个函数会出现问题

(define (bad-letric x)
  letric ([y z]     ; z 在之后才形成 binding,所以这里对 y 的 binding 是失败的
          [z 13])
  if (x y z)))

除了用 let 定义本地变量,还可以使用 define;不过只能在函数 body 的起始处进行定义
有人认为使用 define 时 good style
格式如下

(define (silly-mod2 x)
  (define (even? x) (if (zero? x) #t (odd? (- x 1))))
  (define (odd? x) (if (zero? x) #f (even? (- x 1)))))
  (if (even? x) 0 1))

Toplevel binding

Racket 中进行 binding 的方式与 letrec 等定义 local binding 的规则一样
按顺序进行 evaluate (保证了 early reference: 使用之前的 binding)
且函数直到调用时才对函数体进行 evaluate (保证了在定义函数体时可以进行 back reference: 使用之后的 binding)
而这也造成了 Racket 中不会出现 shadow 的现象:

(define (f x) (+ x 3))
(define f 17)   ; not okay, f already defined in this module

以上代码会出现错误:因为在一个环境里不可能出现两个 f


Delayed evaluation and Thunks

延时求值(delayed evaluation),顾名思义就是不立即求值,而是在值不得不被使用的时候再进行求值。
在一次求值之后,后面的求值会直接使用第一次求值得到的值,而无需再次对原表达式进行求值。

我们利用 Racket 中函数直到调用时才对函数体进行 evaluate 的特性,将表达式用 lambda 包起来创建一个匿名函数(即为创建了一个 thunk)

e ; e will be evaluate immediately
(define x (lambda () e)) ; wrap e in a thunk
(x) ; e will be evaluate only when calling the function x(thunk) 

只有在调用 thunk 的时候才会对 e 求值,这就实现了延时求值
然而,若一个函数中多次调用 thunk,就会出现许多重复计算,如下例子

(define my-mult x y-thunk
  (cond [(= x 0) 0]
        [(= x 1) (y-thunk)]
        [#t (+ (y-thunk) (my-mult (- x 1) y-thunk))]))

这个自定义的乘法函数用递归实现,每次递归一层都要对 y-thunk 进行一次调用
为了优化这个过程,在调用函数时我们利用 let 提前存储 y-thunk 的结果

(my-mult x (lambda () (+ 3 4))) ; ordinary call
(my-mult x (let ([z (+ 3 4)]) (lambda () z)))  ; store the result in z
(my-mult x (lambda () (let ([z (+ 3 4)]) z)))  ; the same as ordinary call

注意第三种写法与第一种其实是一样的,因为 let 中计算的过程仍然被包在了 thunk 中


Delay and force

依然是延时求值内容的扩展,采取一种更加可扩展化的操作来对 thunk 进行处理

(define (my-delay th)
  (mcons #f th))          ; create a promise
(define (my-force p)
  (if (mcar p) (mcdr p)
    (begin (set-mcar! p #t)
           (set-mcdr! p ((mcdr p))
           (mcdr p))))    ; extract/modify the value in promise

delay 操作是将 thunk 包装入一个可变 pair 中,这个可变 pair 被称为 promise
在包装的过程中,由于没有调用 thunk,所以表达式不会被计算
promise 的第一个元素初始化为 false,用来标记 thunk 有没有被调用过
而 force 操作则是替换原本朴素的 (thunk) 操作,保证 thunk 最多只被调用一次


Streams

  • streams
    流 (streams) 是一个长度无限的值序列
    流是以 thunk 的形式表示的,当调用该 thunk 时返回一个 pair <val, next-thunk>
    val 为 stream 序列中当前元素的值,next-thunk 为接下来的元素序列形成的 stream

无限序列可由 cons 单元的嵌套结构表示,cons 单元的 carcdr 分别是最终值与延时对象 (promise),后一个 cons 单元通过强制求值后的 cdr 部分产生,这个过程可以不断重复,从而形成无限序列

  • using streams
    powers-of-two 是 Racket 中自带的一个 stream,值为 \(\{2^1,2^2,2^3,2^4,2^5...\}\)
    我们知道 stream 是一个 thunk,所以需要进行调用来求值
> (car (powers-of-two))
2
> (car ((cdr (power-of-two))))
4

这里定义一个计算函数,计算满足条件的元素在 stream 中的位置

(define (number-until stream tester)
  (letrec ([f (lambda (stream ans)
                (let ([pr (stream)]) 
                  (if (tester (car pr))
                      ans
                      (f (cdr pr) (+ ans 1)))))])
  (f stream 1)))
> (number-until powers-of-two (lambda (x) (= x 16)))
4
  • defining streams
    下面定义一个全 \(1\) 序列
(define ones (lambda () (cons 1 (lambda () (cons 1 (lambda () (cons 1 ......))))))) ; infinite definitions
(define ones (lambda () (cons 1 ones)))

第一句是无限循环递归下去定义的,但我们可以发现,其实后面的部分就是 ones 本身
下面我们自己定义一个 powers-of-two

(define powers-of-two
  (letrec ([f (lambda (x) (cons x (lambda () (f (* x 2)))))]) (lambda () (f 2))))

同样也是利用的递归定义,有点难理解
f 是一个本地函数,其输入一个参数 x,输出一个 pair <x, thunk> 其中 thunk 包装的是以 \(2x\) 为参的 f 函数
那么整个 stream 就可以表示为 thunk(f(2))
我们要知道,对 stream 的定义,核心还是利用了延时求值 (delay evaluation) 的性质
例如在对 ones 的定义中我们又用到了 ones,但由于其被 thunk 包装起来,所以并不会被立即 evaluate 从而保证了定义的完整性
如果这样定义:

(define ones (cons 1 ones))

这样就会出现无限循环的错误:因为没有 thunk 来延迟 ones 主体部分的 evaluation,程序在定义 ones 时使用了还未完成定义的 ones 本身,因此会无限循环下去


Memoization

记忆化(memoization) 的英文是一个生造词,即采用 memo 进行记忆化
贴一段 Racket 实现斐波那契数列递归记忆化代码实现

(define fibonacci
  (letrec ([memo null]
           [f (lambda (x)
                (let ([ans (assoc x memo)])
                  (if ans
                      (cdr ans)
                      (let ([new-ans (if (or (= x 1) (= x 2))
                                         1
                                         (+ (f (- x 1)) (f (- x 2))))])
                        (begin (set! memo (cons (cons x new-ans) memo)) new-ans)))))])
    f))

这里的 memo 是一个元素为 pair 的 list,<x, ans> 代表斐波那契数列中的第 \(x\) 个元素的值为 \(ans\)
其中 (assoc x list) 为内置函数,用来查询 list 中首个 pair 第一关键字为 \(x\) 的第二关键字
若不存在第一关键字为 \(x\) 的元素,则返回 #f
对 memo 的修改是采用 set! 进行的


Macros

Racket 中的 (Macros) 可以说是 Racket 语言的特色
简单的说, Macro 就是能让你在正式编译之前的,先对你的代码进行一次预编译,在这个时候把你代码中的一些符合条件的规则,全部替换成你需要的代码,或者说去生成你想要的代码
和 C++ 中的宏意义很相似
例如以下代码,用宏实现 delay 操作 (即将一个表达式封装为 promise)

(define-syntax my-delay
  syntax-rules ()
    [(my-delay e)
     (mcons #f (lambda () e))]))
> (my-delay (begin (print("hi") (* 3 4))))
; will not print hi

在以上例子中,对 begin 表达式进行 delay 操作并没有输出 hi
这说明宏进行的是词义替换,并不会对 e 进行求值
若采用以下的函数方式来实现 my-delay

(define (my-delay e)
  (mcons #f (lambda () e)))
> (my-delay (begin (print("hi") (* 3 4))))
hi

对同样的 begin 表达式进行 delay 操作,用函数实现的 my-delay 则会输出 hi
这是因为 Racket 在调用函数时一定会对所有的参数进行求值
这就是宏实现与函数实现的区别:宏实现进行纯粹的语义替换,而函数则会创建一个新的函数栈,且会对所有参数进行求值
语义替换是把双刃剑,在 C 系列语言中,不规范的使用宏会出现很多问题

(define-syntax db1
  (syntax-tules () 
     [(db1 x) (let ([y 1]) (* 2 x y))])) ; macros
(let ([y 7]) (db1 7)) ; use

db1 的作用是将某个数乘 \(2\),那么 use 语句结果的期望肯定是 \(14\)
对以上的 use 语句进行 macros 词义替换,得到以下代码

(let ([y 7]) (let ([y 1]) (* 2 y y))) ; naive expansion for use

可以发现,经过词义替换后的函数输出的是 \(2\),而不是期望中的 \(14\)
这就是为什么在 C 系列语言中,宏一般不定义本地变量,且变量名都故意设置的很奇怪
但在 Racket 中并不会出现这样的问题,也就是说,程序会输出正确的结果 \(14\)
这一特性被称为卫生宏 (hygiene macros)
具体表现在:

  1. racket 将 macros 中本地变量名称改成其他的名称
  2. 在 macros 被定义处寻找对应的变量
posted @ 2022-06-21 17:25  四季夏目天下第一  阅读(68)  评论(0编辑  收藏  举报