来上 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 被定义处寻找对应的变量

Coursera Programming Languages, Part B 华盛顿大学 Week 1的更多相关文章

  1. Coursera课程 Programming Languages, Part A 总结

    Coursera CSE341: Programming Languages 感谢华盛顿大学 Dan Grossman 老师 以及 Coursera . 碎言碎语 这只是 Programming La ...

  2. Coursera课程 Programming Languages 总结

    课程 Programming Languages, Part A Programming Languages, Part B Programming Languages, Part C CSE341: ...

  3. Coursera课程 Programming Languages, Part B 总结

    Programming Languages, Part A Programming Languages, Part B Part A 笔记 碎言碎语 很多没有写过 Lisp 程序的人都会对 Lisp ...

  4. The history of programming languages.(transshipment) + Personal understanding and prediction

    To finish this week's homework that introduce the history of programming languages , I surf the inte ...

  5. Natural language style method declaration and usages in programming languages

    More descriptive way to declare and use a method in programming languages At present, in most progra ...

  6. The future of programming languages

    In this video from JAOO Aarhus 2008 Anders Hejlsberg takes a look at the future of programming langu ...

  7. Hex Dump In Many Programming Languages

    Hex Dump In Many Programming Languages See also: ArraySumInManyProgrammingLanguages, CounterInManyPr ...

  8. 在西雅图华盛顿大学 (University of Washington) 就读是怎样一番体验?

    http://www.zhihu.com/question/20811431   先说学校.优点: 如果你是个文青/装逼犯,你来对地方了.连绵不断的雨水会一下子让写诗的感觉将你充满. 美丽的校园.尤其 ...

  9. ESSENTIALS OF PROGRAMMING LANGUAGES (THIRD EDITION) :编程语言的本质 —— (一)

    # Foreword> # 序 This book brings you face-to-face with the most fundamental idea in computer prog ...

  10. Comparison of programming languages

    The following table compares general and technical information for a selection of commonly used prog ...

随机推荐

  1. docker部署flask+uwsgi+nginx+postgresql,解耦拆分到多个容器,实现多容器互访

    本人承诺,本博客内容,亲测有效. dockerfile文件, FROM centos:7 RUN mkdir /opt/flask_app COPY ./show_data_from_jira /op ...

  2. Spring事务的四大特性

    1.事务(Transaction) 事务一般是指数据库事务, 是基于关系型数据库(RDBMS)的企业应用的重要组成部分.在软件开发领域,事务扮演者十分重要的角色,用来确保应用程序数据的完整性和一致性. ...

  3. 常见报错——Uncaught TypeError: document.getElementsByClassName(...).addEventListener is not a function

    这是因为选择器没有正确选择元素对象 document.getElementsByClassName(...)捕捉到的是该类名元素的数组 正确的访问方式应该是: document.getElements ...

  4. AFNI 教程 步骤5:统计和建模

    第一部分 时间序列 用AFNI打开fMRI数据, Graph按钮可以打开信号界面,中心的信号是该像素的信号随着时间的变化图,m 可以显示更少的体素,M可以显示更多的体素.V 可以浏览整个图像,+ 可以 ...

  5. 51电子-STC89C51开发板:安装KEIL

    全部内容,请点击: 51电子-STC89C51开发板:<目录> ---------------------------  正文开始  --------------------------- ...

  6. [笔记]windows cmd常用命令

    1.返回上一级目录  目前似乎没有直接的命令,参考  https://stackoverflow.com/questions/48189935/how-can-i-return-to-the-prev ...

  7. easyExcel-modle

    package com.alibaba.easyexcel.test.model;import com.alibaba.excel.annotation.ExcelProperty;import co ...

  8. 兼容ie8的Html+Css+Js

    1 <!DOCTYPE html> 2 <html lang="en"> 3 4 <head> 5 <meta charset=" ...

  9. 微信小程序JS遇到【object object 】怎么打印?js如何打印object对象

    console.log(JSON.stringify(user)):或者打印的时候直接 console.log(user):不要出现'""'+这些符号

  10. 对mvc模式的理解

    Model-View-Controller MVC模式是个威力强大的复合模式,是由数个设计模式结合起来的模式: 我们先看一下一个mp3播放器的设计,来由浅至深地了解这个设计模式的精髓所在: 从最直观的 ...